Merge pull request #5989 from WoltLab/wsc-rpc-api-const
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / language / LanguageEditor.class.php
CommitLineData
11ade432 1<?php
a9229942 2
11ade432 3namespace wcf\data\language;
a9229942
TD
4
5use wcf\data\DatabaseObjectEditor;
6use wcf\data\IEditableCachedObject;
11ade432
AE
7use wcf\data\language\category\LanguageCategory;
8use wcf\data\language\category\LanguageCategoryEditor;
9use wcf\data\language\item\LanguageItemEditor;
10use wcf\data\language\item\LanguageItemList;
4f456324 11use wcf\data\page\PageEditor;
b4ae02cc 12use wcf\event\language\LanguageContentCopying;
b401cd0d 13use wcf\system\cache\builder\LanguageCacheBuilder;
11ade432 14use wcf\system\database\util\PreparedStatementConditionBuilder;
f855889a 15use wcf\system\event\EventHandler;
6286572b 16use wcf\system\exception\SystemException;
3b6c3ca4 17use wcf\system\io\AtomicWriter;
11ade432 18use wcf\system\language\LanguageFactory;
ac9b0f6e 19use wcf\system\Regex;
11ade432 20use wcf\system\WCF;
25e5c0cf 21use wcf\util\DirectoryUtil;
37a3683e 22use wcf\util\FileUtil;
6286572b
AE
23use wcf\util\StringUtil;
24use wcf\util\XML;
11ade432
AE
25
26/**
27 * Provides functions to edit languages.
a9229942
TD
28 *
29 * @author Alexander Ebert
30 * @copyright 2001-2019 WoltLab GmbH
31 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
a9229942
TD
32 *
33 * @method static Language create(array $parameters = [])
34 * @method Language getDecoratedObject()
35 * @mixin Language
11ade432 36 */
a9229942
TD
37class LanguageEditor extends DatabaseObjectEditor implements IEditableCachedObject
38{
39 /**
40 * @inheritDoc
41 */
42 protected static $baseClass = Language::class;
43
44 /**
45 * @inheritDoc
46 */
47 public function delete()
48 {
49 parent::delete();
50
51 self::deleteLanguageFiles($this->languageID);
52 }
53
54 /**
55 * Updates the language files for the given category.
56 *
57 * @param LanguageCategory $languageCategory
58 */
59 public function updateCategory(LanguageCategory $languageCategory)
60 {
61 $this->writeLanguageFiles([$languageCategory->languageCategoryID]);
62 }
63
64 /**
65 * Write the languages files.
66 *
67 * @param int[] $languageCategoryIDs
68 */
69 protected function writeLanguageFiles(array $languageCategoryIDs)
70 {
71 $conditions = new PreparedStatementConditionBuilder();
72 $conditions->add("languageID = ?", [$this->languageID]);
73 $conditions->add("languageCategoryID IN (?)", [$languageCategoryIDs]);
74
75 // get language items
76 $sql = "SELECT languageItem, languageItemValue, languageCustomItemValue,
77 languageUseCustomValue, languageCategoryID
78 FROM wcf" . WCF_N . "_language_item
79 " . $conditions;
80 $statement = WCF::getDB()->prepareStatement($sql);
81 $statement->execute($conditions->getParameters());
82 $items = [];
83 while ($row = $statement->fetchArray()) {
84 $languageCategoryID = $row['languageCategoryID'];
85 if (!isset($items[$languageCategoryID])) {
86 $items[$languageCategoryID] = [];
87 }
88
89 $items[$languageCategoryID][$row['languageItem']] = $row['languageUseCustomValue'] ? $row['languageCustomItemValue'] : $row['languageItemValue'];
90 }
91
92 foreach ($items as $languageCategoryID => $languageItems) {
93 $category = LanguageFactory::getInstance()->getCategoryByID($languageCategoryID);
94 if ($category === null) {
95 continue;
96 }
97
98 $filename = WCF_DIR . 'language/' . $this->languageID . '_' . $category->languageCategory . '.php';
99 $writer = new AtomicWriter($filename);
100
101 $writer->write("<?php\n/**\n* WoltLab Suite\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");
102 foreach ($languageItems as $languageItem => $languageItemValue) {
c45740ff 103 $writer->write("\$this->items['" . $languageItem . "'] = " . \var_export($languageItemValue, true) . ";\n");
a9229942
TD
104
105 // compile dynamic language variables
106 if ($category->languageCategory != 'wcf.global' && \strpos($languageItemValue, '{') !== false) {
107 try {
108 $output = LanguageFactory::getInstance()->getScriptingCompiler()->compileString(
109 $languageItem,
110 $languageItemValue
111 );
112 } catch (SystemException $e) {
113 continue;
114 } // ignore compiler errors
115
c45740ff 116 $writer->write("\$this->dynamicItems['" . $languageItem . "'] = " . \var_export($output['template'], true) . ";\n");
a9229942
TD
117 }
118 }
119
120 $writer->flush();
121 $writer->close();
122 FileUtil::makeWritable($filename);
123 }
124 }
125
126 /**
127 * Exports this language.
a9229942 128 */
a2f9f32d 129 public function export(int $packageID, bool $exportCustomValues = false)
a9229942
TD
130 {
131 $conditions = new PreparedStatementConditionBuilder();
132 $conditions->add("language_item.languageID = ?", [$this->languageID]);
a2f9f32d 133 $conditions->add("language_item.packageID = ?", [$packageID]);
a9229942 134
a9229942 135 // header
99951f50 136 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/6.0/language.xsd\" languagecode=\"" . $this->languageCode . "\" languagename=\"" . $this->languageName . "\" countrycode=\"" . $this->countryCode . "\">\n";
a9229942
TD
137
138 // get items
a9229942
TD
139 $sql = "SELECT languageItem,
140 " . ($exportCustomValues ? "CASE WHEN languageUseCustomValue > 0 THEN languageCustomItemValue ELSE languageItemValue END AS languageItemValue" : "languageItemValue") . ",
141 languageCategory
142 FROM wcf" . WCF_N . "_language_item language_item
143 LEFT JOIN wcf" . WCF_N . "_language_category language_category
c240c98a 144 ON language_category.languageCategoryID = language_item.languageCategoryID
a9229942
TD
145 " . $conditions;
146 $statement = WCF::getDB()->prepareStatement($sql);
147 $statement->execute($conditions->getParameters());
a2f9f32d 148 $items = [];
a9229942
TD
149 while ($row = $statement->fetchArray()) {
150 $items[$row['languageCategory']][$row['languageItem']] = $row['languageItemValue'];
151 }
152
153 // sort categories
154 \ksort($items);
155
156 foreach ($items as $category => $categoryItems) {
157 // sort items
158 \ksort($categoryItems);
159
160 // category header
161 echo "\t<category name=\"" . $category . "\">\n";
162
163 // items
164 foreach ($categoryItems as $item => $value) {
165 echo "\t\t<item name=\"" . $item . "\"><![CDATA[" . StringUtil::escapeCDATA($value) . "]]></item>\n";
166 }
167
168 // category footer
169 echo "\t</category>\n";
170 }
171
172 // add shadow values (pages)
173 $pages = [];
174 $conditions = new PreparedStatementConditionBuilder();
175 $conditions->add("page_content.pageID = page.pageID");
176 $conditions->add("page_content.languageID = ?", [$this->languageID]);
a2f9f32d 177 $conditions->add("page.packageID = ?", [$packageID]);
a9229942
TD
178 $conditions->add("page.originIsSystem = ?", [1]);
179 $sql = "SELECT page.identifier, page_content.title, page_content.content
180 FROM wcf" . WCF_N . "_page page,
181 wcf" . WCF_N . "_page_content page_content
182 " . $conditions . "
183 ORDER BY page.identifier";
184 $statement = WCF::getDB()->prepareStatement($sql);
185 $statement->execute($conditions->getParameters());
186 while ($row = $statement->fetchArray()) {
187 $pages[] = $row;
188 }
189
190 if (!empty($pages)) {
191 echo "\t<category name=\"shadow.invalid.page\">\n";
192
193 foreach ($pages as $page) {
194 if ($page['title']) {
195 echo "\t\t<item name=\"shadow.invalid.page." . $page['identifier'] . ".title\"><![CDATA[" . StringUtil::escapeCDATA($page['title']) . "]]></item>\n";
196 }
197 if ($page['content']) {
198 echo "\t\t<item name=\"shadow.invalid.page." . $page['identifier'] . ".content\"><![CDATA[" . StringUtil::escapeCDATA($page['content']) . "]]></item>\n";
199 }
200 }
201
202 echo "\t</category>\n";
203 }
204
205 // add shadow values (boxes)
206 $boxes = [];
207 $conditions = new PreparedStatementConditionBuilder();
208 $conditions->add("box_content.boxID = box.boxID");
209 $conditions->add("box_content.languageID = ?", [$this->languageID]);
a2f9f32d 210 $conditions->add("box.packageID = ?", [$packageID]);
a9229942
TD
211 $conditions->add("box.originIsSystem = ?", [1]);
212 $sql = "SELECT box.identifier, box_content.title, box_content.content
213 FROM wcf" . WCF_N . "_box box,
214 wcf" . WCF_N . "_box_content box_content
215 " . $conditions . "
216 ORDER BY box.identifier";
217 $statement = WCF::getDB()->prepareStatement($sql);
218 $statement->execute($conditions->getParameters());
219 while ($row = $statement->fetchArray()) {
220 $boxes[] = $row;
221 }
222
223 if (!empty($pages)) {
224 echo "\t<category name=\"shadow.invalid.box\">\n";
225
226 foreach ($boxes as $box) {
227 if ($box['title']) {
228 echo "\t\t<item name=\"shadow.invalid.box." . $box['identifier'] . ".title\"><![CDATA[" . StringUtil::escapeCDATA($box['title']) . "]]></item>\n";
229 }
230 if ($box['content']) {
231 echo "\t\t<item name=\"shadow.invalid.box." . $box['identifier'] . ".content\"><![CDATA[" . StringUtil::escapeCDATA($box['content']) . "]]></item>\n";
232 }
233 }
234
235 echo "\t</category>\n";
236 }
237
238 // footer
239 echo "</language>";
240 }
241
0d8c77e5
MS
242 /**
243 * Deletes the language items from the given XML document.
244 *
245 * @throws \InvalidArgumentException if given XML document is invalid with regard to deleting language items
246 * @since 5.5
247 */
248 protected function deleteFromXML(XML $xml, int $packageID): void
249 {
250 $xpath = $xml->xpath();
251
252 $items = $xpath->query('/ns:language/ns:delete/ns:item');
253 if (empty($items)) {
254 return;
255 }
256
257 $languageItems = [];
258
259 /** @var \DOMElement $item */
260 foreach ($items as $item) {
261 $itemName = $item->getAttribute('name');
262
263 if (empty($itemName)) {
264 throw new \InvalidArgumentException("The 'name' attribute is missing or empty.");
265 }
266
267 if (StringUtil::trim($itemName) !== $itemName) {
268 throw new \InvalidArgumentException("The name '{$itemName}' contains leading or trailing whitespaces.");
269 }
270
271 $languageItems[] = $itemName;
272 }
273
274 if (empty($languageItems)) {
275 return;
276 }
277
278 $conditions = new PreparedStatementConditionBuilder();
279 $conditions->add('packageID = ?', [$packageID]);
280 $conditions->add('languageItem IN (?)', [$languageItems]);
7436b993 281 $conditions->add('languageID = ?', [$this->languageID]);
0d8c77e5
MS
282 $sql = "DELETE FROM wcf1_language_item
283 {$conditions}";
284 $statement = WCF::getDB()->prepare($sql);
285 $statement->execute($conditions->getParameters());
286 }
287
8c957031
MS
288 /**
289 * Checks the structure of the XML file to ensure that either the old, deprecated structure
290 * with direct `category` children is used or the new structure with `import` and `delete`
291 * children.
292 *
293 * @throws \InvalidArgumentException if old and new structure is mixed
294 * @since 5.5
295 */
296 protected function validateXMLStructure(XML $xml): void
297 {
298 $xpath = $xml->xpath();
299
300 $hasImport = $xpath->query('/ns:language/ns:import')->length !== 0;
301 $hasDelete = $xpath->query('/ns:language/ns:delete')->length !== 0;
302 $hasDirectCategories = $xpath->query('/ns:language/ns:category')->length !== 0;
303
304 if (($hasImport || $hasDelete) && $hasDirectCategories) {
305 throw new \InvalidArgumentException("'category' elements cannot be used next to 'import' and 'delete' elements.");
306 }
307 }
308
a9229942
TD
309 /**
310 * Imports language items from an XML file into this language.
311 * Updates the relevant language files automatically.
312 *
313 * @param XML $xml
314 * @param int $packageID
315 * @param bool $updateFiles
316 * @param bool $updateExistingItems
317 * @throws \InvalidArgumentException if given XML file is invalid
318 */
319 public function updateFromXML(XML $xml, $packageID, $updateFiles = true, $updateExistingItems = true)
320 {
8c957031
MS
321 $this->validateXMLStructure($xml);
322
0d8c77e5
MS
323 $this->deleteFromXML($xml, $packageID);
324
a9229942
TD
325 $xpath = $xml->xpath();
326 $usedCategories = [];
327
328 // fetch categories
0d8c77e5
MS
329 $categories = $xpath->query('/ns:language/ns:import/ns:category');
330 if ($categories->length === 0) {
331 // Fallback for the old, deprecated version before WSC 5.5, which only supported imports.
332 $categories = $xpath->query('/ns:language/ns:category');
333 }
a9229942
TD
334
335 /** @var \DOMElement $category */
336 foreach ($categories as $category) {
337 $usedCategories[$category->getAttribute('name')] = 0;
338 }
339
340 if (empty($usedCategories)) {
341 return;
342 }
343
344 // select existing categories
345 $conditions = new PreparedStatementConditionBuilder();
346 $conditions->add("languageCategory IN (?)", [\array_keys($usedCategories)]);
347
348 $sql = "SELECT languageCategoryID, languageCategory
349 FROM wcf" . WCF_N . "_language_category
350 " . $conditions;
351 $statement = WCF::getDB()->prepareStatement($sql);
352 $statement->execute($conditions->getParameters());
353 while ($row = $statement->fetchArray()) {
354 $usedCategories[$row['languageCategory']] = $row['languageCategoryID'];
355 }
356
357 // create new categories
358 foreach ($usedCategories as $categoryName => $categoryID) {
359 if ($categoryID) {
360 continue;
361 }
362 if (\strpos($categoryName, 'shadow.invalid') === 0) {
363 continue; // ignore shadow items
364 }
365
366 /** @var LanguageCategory $category */
367 $category = LanguageCategoryEditor::create([
368 'languageCategory' => $categoryName,
369 ]);
370 $usedCategories[$categoryName] = $category->languageCategoryID;
371 }
372
373 // loop through categories to import items
374 $itemData = $pageContents = $boxContents = [];
375 $languageItemValues = [];
376
377 /** @var \DOMElement $category */
378 foreach ($categories as $category) {
379 $categoryName = $category->getAttribute('name');
380 $elements = $xpath->query('child::*', $category);
381
382 if ($categoryName == 'shadow.invalid.page') {
383 /** @var \DOMElement $element */
384 foreach ($elements as $element) {
385 if (
386 \preg_match(
387 '/^shadow\.invalid\.page\.(.*)\.(title|content)/',
388 $element->getAttribute('name'),
389 $match
390 )
391 ) {
392 if (!isset($pageContents[$match[1]])) {
393 $pageContents[$match[1]] = [];
394 }
395 $pageContents[$match[1]][$match[2]] = $element->nodeValue;
396 }
397 }
398 } elseif ($categoryName == 'shadow.invalid.box') {
399 /** @var \DOMElement $element */
400 foreach ($elements as $element) {
401 if (
402 \preg_match(
403 '/^shadow\.invalid\.box\.(.*)\.(title|content)/',
404 $element->getAttribute('name'),
405 $match
406 )
407 ) {
408 if (!isset($boxContents[$match[1]])) {
409 $boxContents[$match[1]] = [];
410 }
411 $boxContents[$match[1]][$match[2]] = $element->nodeValue;
412 }
413 }
414 } else {
415 $categoryID = $usedCategories[$categoryName];
416
417 /** @var \DOMElement $element */
418 foreach ($elements as $element) {
419 $itemName = $element->getAttribute('name');
420
c419de9d 421 self::validateItemName($itemName, $categoryName);
a9229942
TD
422
423 $itemValue = $element->nodeValue;
424
425 $itemData[] = $this->languageID;
426 $itemData[] = $itemName;
427 $itemData[] = $itemValue;
428 $itemData[] = $categoryID;
429 if ($packageID) {
c7e1ee71
TD
430 if ($packageID == -1) {
431 throw new \BadMethodCallException('Specifying `-1` as the packageID is no longer supported.');
432 }
433
a08921a9 434 $itemData[] = $packageID;
a9229942
TD
435 }
436
437 if ($updateExistingItems) {
438 $languageItemValues[$itemName] = $itemValue;
439 }
440 }
441 }
442 }
443
444 // save items
445 if (!empty($itemData)) {
446 // select phrases that have custom versions that might get disabled during the update
447 if ($updateExistingItems) {
448 $conditions = new PreparedStatementConditionBuilder();
449 $conditions->add("languageItem IN (?)", [\array_keys($languageItemValues)]);
450 $conditions->add("languageID = ?", [$this->languageID]);
451 if ($packageID > 0) {
452 $conditions->add("packageID = ?", [$packageID]);
453 }
454 $conditions->add("(languageUseCustomValue = ? OR isCustomLanguageItem = ?)", [1, 1]);
455
456 $sql = "SELECT languageItemID, languageItem, languageItemValue, isCustomLanguageItem
457 FROM wcf" . WCF_N . "_language_item
458 " . $conditions;
459 $statement = WCF::getDB()->prepareStatement($sql);
460 $statement->execute($conditions->getParameters());
461 $updateValues = $customLanguageItemIDs = [];
462 while ($row = $statement->fetchArray()) {
463 if ($row['isCustomLanguageItem']) {
464 $customLanguageItemIDs[] = $row['languageItemID'];
465 }
466
467 // also save old values of custom language items
468 if ($row['isCustomLanguageItem'] || $row['languageItemValue'] != $languageItemValues[$row['languageItem']]) {
469 $updateValues[] = $row['languageItemID'];
470 }
471 }
472
473 if (!empty($updateValues)) {
474 $sql = "UPDATE wcf" . WCF_N . "_language_item
475 SET languageItemOldValue = languageItemValue,
476 languageCustomItemDisableTime = ?,
477 languageUseCustomValue = ?
478 WHERE languageItemID = ?";
479 $statement = WCF::getDB()->prepareStatement($sql);
480
481 WCF::getDB()->beginTransaction();
482 foreach ($updateValues as $languageItemID) {
483 $statement->execute([
484 TIME_NOW,
485 0,
486 $languageItemID,
487 ]);
488 }
489 WCF::getDB()->commitTransaction();
490 }
491
492 // make custom language items normal ones
493 if (!empty($customLanguageItemIDs)) {
494 $sql = "UPDATE wcf" . WCF_N . "_language_item
495 SET isCustomLanguageItem = ?,
496 languageItemOriginIsSystem = ?,
497 packageID = ?
498 WHERE languageItemID = ?";
499 $statement = WCF::getDB()->prepareStatement($sql);
500
501 WCF::getDB()->beginTransaction();
502 foreach ($updateValues as $languageItemID) {
503 $statement->execute([
504 0,
505 1,
506 $packageID,
507 $languageItemID,
508 ]);
509 }
510 WCF::getDB()->commitTransaction();
511 }
512 }
513
514 // insert/update a maximum of 50 items per run (prevents issues with max_allowed_packet)
515 $step = $packageID ? 5 : 4;
516 WCF::getDB()->beginTransaction();
517 for ($i = 0, $length = \count($itemData); $i < $length; $i += 50 * $step) {
518 $parameters = \array_slice($itemData, $i, 50 * $step);
519 $repeat = \count($parameters) / $step;
520
521 $placeholders = \substr(
522 \str_repeat('(?, ?, ?, ?' . ($packageID ? ', ?' : '') . '),', $repeat),
523 0,
524 -1
525 );
526 $sql = "INSERT" . (!$updateExistingItems ? " IGNORE" : "") . " INTO wcf" . WCF_N . "_language_item
527 (languageID, languageItem, languageItemValue, languageCategoryID" . ($packageID ? ", packageID" : "") . ")
528 VALUES {$placeholders}";
529
530 if ($updateExistingItems) {
531 if ($packageID > 0) {
532 // do not update anything if language item is owned by a different package
533 $sql .= "
534 ON DUPLICATE KEY UPDATE languageItemValue = IF(packageID = " . $packageID . ", IF(languageItemOriginIsSystem = 0, languageItemValue, VALUES(languageItemValue)), languageItemValue),
535 languageCategoryID = IF(packageID = " . $packageID . ", VALUES(languageCategoryID), languageCategoryID)";
536 } else {
537 // skip package id check during WCFSetup (packageID = 0) or if using the ACP form (packageID = -1)
538 $sql .= "
539 ON DUPLICATE KEY UPDATE languageItemValue = IF(languageItemOriginIsSystem = 0, languageItemValue, VALUES(languageItemValue)),
540 languageCategoryID = VALUES(languageCategoryID)";
541 }
542 }
543
544 $statement = WCF::getDB()->prepareStatement($sql);
545 $statement->execute($parameters);
546 }
547 WCF::getDB()->commitTransaction();
548 }
549
550 // save page content
551 if (!empty($pageContents)) {
552 // get page ids
553 $pageIDs = [];
554 $conditions = new PreparedStatementConditionBuilder();
555 $conditions->add("identifier IN (?)", [\array_keys($pageContents)]);
556 $sql = "SELECT pageID, identifier
557 FROM wcf" . WCF_N . "_page
558 " . $conditions;
559 $statement = WCF::getDB()->prepareStatement($sql);
560 $statement->execute($conditions->getParameters());
561 while ($row = $statement->fetchArray()) {
562 $pageIDs[$row['identifier']] = $row['pageID'];
563 }
564
e4a0c76f
MW
565 $sql = "INSERT IGNORE INTO wcf1_page_content
566 (pageID, languageID, title, content, metaDescription, customURL, hasEmbeddedObjects)
567 SELECT pageID, ?, title, content, metaDescription, CASE WHEN customURL <> '' THEN CONCAT(customURL, '_', ?) ELSE '' END, hasEmbeddedObjects
568 FROM wcf1_page_content
569 WHERE pageID = ?
570 AND languageID = ?";
571 $createLanguageVersionStatement = WCF::getDB()->prepare($sql);
a9229942
TD
572 $sql = "UPDATE wcf" . WCF_N . "_page_content
573 SET title = ?
574 WHERE pageID = ?
575 AND languageID = ?";
576 $updateTitleStatement = WCF::getDB()->prepareStatement($sql);
577 $sql = "UPDATE wcf" . WCF_N . "_page_content
578 SET content = ?
579 WHERE pageID = ?
580 AND languageID = ?";
581 $updateContentStatement = WCF::getDB()->prepareStatement($sql);
582
583 foreach ($pageContents as $identifier => $pageContent) {
584 if (!isset($pageIDs[$identifier])) {
585 continue; // unknown page
586 }
587
e4a0c76f
MW
588 $createLanguageVersionStatement->execute([
589 $this->languageID,
590 $this->languageID,
591 $pageIDs[$identifier],
592 LanguageFactory::getInstance()->getDefaultLanguageID(),
593 ]);
a9229942
TD
594 if (isset($pageContent['title'])) {
595 $updateTitleStatement->execute([$pageContent['title'], $pageIDs[$identifier], $this->languageID]);
596 }
597 if (isset($pageContent['content'])) {
598 $updateContentStatement->execute([
599 $pageContent['content'],
600 $pageIDs[$identifier],
601 $this->languageID,
602 ]);
603 }
604 }
605 }
606
607 // save box content
608 if (!empty($boxContents)) {
609 // get box ids
610 $boxIDs = [];
611 $conditions = new PreparedStatementConditionBuilder();
612 $conditions->add("identifier IN (?)", [\array_keys($boxContents)]);
613 $sql = "SELECT boxID, identifier
614 FROM wcf" . WCF_N . "_box
615 " . $conditions;
616 $statement = WCF::getDB()->prepareStatement($sql);
617 $statement->execute($conditions->getParameters());
618 while ($row = $statement->fetchArray()) {
619 $boxIDs[$row['identifier']] = $row['boxID'];
620 }
621
622 $sql = "INSERT IGNORE INTO wcf" . WCF_N . "_box_content
623 (boxID, languageID)
624 VALUES (?, ?)";
625 $createLanguageVersionStatement = WCF::getDB()->prepareStatement($sql);
626 $sql = "UPDATE wcf" . WCF_N . "_box_content
627 SET title = ?
628 WHERE boxID = ?
629 AND languageID = ?";
630 $updateTitleStatement = WCF::getDB()->prepareStatement($sql);
631 $sql = "UPDATE wcf" . WCF_N . "_box_content
632 SET content = ?
633 WHERE boxID = ?
634 AND languageID = ?";
635 $updateContentStatement = WCF::getDB()->prepareStatement($sql);
636
637 foreach ($boxContents as $identifier => $boxContent) {
638 if (!isset($boxIDs[$identifier])) {
639 continue; // unknown box
640 }
641
642 $createLanguageVersionStatement->execute([$boxIDs[$identifier], $this->languageID]);
643 if (isset($boxContent['title'])) {
644 $updateTitleStatement->execute([$boxContent['title'], $boxIDs[$identifier], $this->languageID]);
645 }
646 if (isset($boxContent['content'])) {
647 $updateContentStatement->execute([$boxContent['content'], $boxIDs[$identifier], $this->languageID]);
648 }
649 }
650 }
651
652 // update the relevant language files
653 if ($updateFiles) {
654 self::deleteLanguageFiles($this->languageID);
655 }
656
657 // delete relevant template compilations
658 $this->deleteCompiledTemplates();
659 }
660
c419de9d
TD
661 /**
662 * Verifies that the given variable is a valid variable within the given category.
663 * Throws an exception otherwise.
664 *
665 * @since 5.4
666 */
667 final public static function validateItemName(string $itemName, string $categoryName): void
668 {
669 // Safeguard against malformed phrases, an empty name has a strange side effect.
670 if (empty($itemName)) {
671 throw new \InvalidArgumentException("The name attribute is missing or empty.");
672 }
673
674 if ($itemName !== $categoryName && \strpos($itemName, $categoryName . '.') !== 0) {
675 throw new \InvalidArgumentException(WCF::getLanguage()->getDynamicVariable(
676 'wcf.acp.language.import.error.categoryMismatch',
677 [
678 'categoryName' => $categoryName,
679 'languageItem' => $itemName,
680 ]
681 ));
682 }
683
684 if (StringUtil::trim($itemName) !== $itemName) {
685 throw new \InvalidArgumentException("The name '{$itemName}' contains leading or trailing whitespaces.");
686 }
687 }
688
a9229942
TD
689 /**
690 * Deletes the language cache.
691 *
692 * @param string $languageID
693 * @param string $category
694 */
695 public static function deleteLanguageFiles($languageID = '.*', $category = '.*')
696 {
697 if ($category != '.*') {
698 $category = \preg_quote($category, '~');
699 }
700 if ($languageID != '.*') {
701 $languageID = \intval($languageID);
702 }
703
704 DirectoryUtil::getInstance(WCF_DIR . 'language/')->removePattern(new Regex($languageID . '_' . $category . '\.php$'));
705 }
706
707 /**
708 * Deletes relevant template compilations.
709 */
710 public function deleteCompiledTemplates()
711 {
712 // templates
713 DirectoryUtil::getInstance(WCF_DIR . 'templates/compiled/')->removePattern(new Regex('.*_' . $this->languageID . '_.*\.php$'));
714 // acp templates
715 DirectoryUtil::getInstance(WCF_DIR . 'acp/templates/compiled/')->removePattern(new Regex('.*_' . $this->languageID . '_.*\.php$'));
716 }
717
718 /**
719 * Updates all language files of the given package id.
720 */
721 public static function updateAll()
722 {
723 self::deleteLanguageFiles();
724 }
725
726 /**
727 * Takes an XML object and returns the specific language code.
728 *
729 * @param XML $xml
730 * @return string
731 * @throws SystemException
732 */
733 public static function readLanguageCodeFromXML(XML $xml)
734 {
735 $rootNode = $xml->xpath()->query('/ns:language')->item(0);
736 $attributes = $xml->xpath()->query('attribute::*', $rootNode);
737 foreach ($attributes as $attribute) {
738 if ($attribute->name == 'languagecode') {
739 return $attribute->value;
740 }
741 }
742
743 throw new SystemException("missing attribute 'languagecode' in language file");
744 }
745
746 /**
747 * Takes an XML object and returns the specific language name.
748 *
749 * @param XML $xml
750 * @return string language name
751 * @throws SystemException
752 */
753 public static function readLanguageNameFromXML(XML $xml)
754 {
755 $rootNode = $xml->xpath()->query('/ns:language')->item(0);
756 $attributes = $xml->xpath()->query('attribute::*', $rootNode);
757 foreach ($attributes as $attribute) {
758 if ($attribute->name == 'languagename') {
759 return $attribute->value;
760 }
761 }
762
763 throw new SystemException("missing attribute 'languagename' in language file");
764 }
765
766 /**
767 * Takes an XML object and returns the specific country code.
768 *
769 * @param XML $xml
770 * @return string country code
771 * @throws SystemException
772 */
773 public static function readCountryCodeFromXML(XML $xml)
774 {
775 $rootNode = $xml->xpath()->query('/ns:language')->item(0);
776 $attributes = $xml->xpath()->query('attribute::*', $rootNode);
777 foreach ($attributes as $attribute) {
778 if ($attribute->name == 'countrycode') {
779 return $attribute->value;
780 }
781 }
782
783 throw new SystemException("missing attribute 'countrycode' in language file");
784 }
785
786 /**
787 * Imports language items from an XML file into a new or a current language.
788 * Updates the relevant language files automatically.
789 *
790 * @param XML $xml
791 * @param int $packageID
792 * @param Language $source
793 * @return LanguageEditor
794 */
795 public static function importFromXML(XML $xml, $packageID, ?Language $source = null)
796 {
797 $languageCode = self::readLanguageCodeFromXML($xml);
798
799 // try to find an existing language with the given language code
800 $language = LanguageFactory::getInstance()->getLanguageByCode($languageCode);
801
802 // create new language
803 if ($language === null) {
804 $countryCode = self::readCountryCodeFromXML($xml);
805 $languageName = self::readLanguageNameFromXML($xml);
806 $language = self::create([
807 'countryCode' => $countryCode,
808 'languageCode' => $languageCode,
809 'languageName' => $languageName,
810 ]);
811
812 if ($source) {
813 $sourceEditor = new self($source);
814 $sourceEditor->copy($language);
815 }
816 }
817
818 // import xml
819 $languageEditor = new self($language);
820 $languageEditor->updateFromXML($xml, $packageID);
821
822 // return language object
823 return $languageEditor;
824 }
825
826 /**
827 * Copies all language variables from current language to language specified as $destination.
828 * Caution: This method expects that target language does not have any items!
829 *
830 * @param Language $destination
831 */
832 public function copy(Language $destination)
833 {
834 $sql = "INSERT INTO wcf" . WCF_N . "_language_item
835 (languageID, languageItem, languageItemValue, languageItemOriginIsSystem, languageCategoryID, packageID)
836 SELECT ?, languageItem, languageItemValue, languageItemOriginIsSystem, languageCategoryID, packageID
837 FROM wcf" . WCF_N . "_language_item
838 WHERE languageID = ?";
839 $statement = WCF::getDB()->prepareStatement($sql);
840 $statement->execute([
841 $destination->languageID,
842 $this->languageID,
843 ]);
844 }
845
846 /**
847 * Updates the language items of a language category.
848 *
849 * @param array $items
850 * @param LanguageCategory $category
851 * @param int $packageID
852 * @param array $useCustom
853 */
854 public function updateItems(
855 array $items,
856 LanguageCategory $category,
857 $packageID = PACKAGE_ID,
858 array $useCustom = []
859 ) {
860 if (empty($items)) {
861 return;
862 }
863
864 // find existing language items
865 $languageItemList = new LanguageItemList();
866 $languageItemList->getConditionBuilder()->add("language_item.languageItem IN (?)", [\array_keys($items)]);
867 $languageItemList->getConditionBuilder()->add("languageID = ?", [$this->languageID]);
868 $languageItemList->readObjects();
869
870 foreach ($languageItemList->getObjects() as $languageItem) {
871 $languageItemEditor = new LanguageItemEditor($languageItem);
872 $languageItemEditor->update([
873 'languageCustomItemValue' => $items[$languageItem->languageItem],
874 'languageUseCustomValue' => isset($useCustom[$languageItem->languageItem]) ? 1 : 0,
875 ]);
876
877 // remove updated items, leaving items to be created within
878 unset($items[$languageItem->languageItem]);
879 }
880
881 // create remaining items
882 if (!empty($items)) {
883 // bypass LanguageItemEditor::create() for performance reasons
884 $sql = "INSERT INTO wcf" . WCF_N . "_language_item
885 (languageID, languageItem, languageItemValue, languageItemOriginIsSystem, languageCategoryID, packageID)
886 VALUES (?, ?, ?, ?, ?, ?)";
887 $statement = WCF::getDB()->prepareStatement($sql);
888
889 foreach ($items as $itemName => $itemValue) {
890 $statement->execute([
891 $this->languageID,
892 $itemName,
893 $itemValue,
894 0,
895 $category->languageCategoryID,
896 $packageID,
897 ]);
898 }
899 }
900
901 // update the relevant language files
902 self::deleteLanguageFiles($this->languageID, $category->languageCategory);
903
904 // delete relevant template compilations
905 $this->deleteCompiledTemplates();
906 }
907
908 /**
909 * Sets current language as default language.
910 */
911 public function setAsDefault()
912 {
913 // remove default flag from all languages
914 $sql = "UPDATE wcf" . WCF_N . "_language
915 SET isDefault = ?";
916 $statement = WCF::getDB()->prepareStatement($sql);
917 $statement->execute([0]);
918
919 // set current language as default language
920 $this->update(['isDefault' => 1]);
921
922 $this->clearCache();
923 }
924
925 /**
926 * Clears language cache.
927 */
928 public function clearCache()
929 {
930 LanguageCacheBuilder::getInstance()->reset();
931 }
932
933 /**
934 * Enables the multilingualism feature for given languages.
935 *
936 * @param array $languageIDs
937 */
938 public static function enableMultilingualism(array $languageIDs = [])
939 {
940 $sql = "UPDATE wcf" . WCF_N . "_language
941 SET hasContent = ?";
942 $statement = WCF::getDB()->prepareStatement($sql);
943 $statement->execute([0]);
944
945 if (!empty($languageIDs)) {
946 $sql = '';
947 $statementParameters = [];
948 foreach ($languageIDs as $languageID) {
949 if (!empty($sql)) {
950 $sql .= ',';
951 }
952 $sql .= '?';
953 $statementParameters[] = $languageID;
954 }
955
956 $sql = "UPDATE wcf" . WCF_N . "_language
957 SET hasContent = ?
958 WHERE languageID IN (" . $sql . ")";
959 $statement = WCF::getDB()->prepareStatement($sql);
960 \array_unshift($statementParameters, 1);
961 $statement->execute($statementParameters);
962 }
963 }
964
965 /**
966 * @inheritDoc
967 */
968 public static function resetCache()
969 {
970 LanguageFactory::getInstance()->clearCache();
971 }
972
973 /**
974 * Copies all cms contents (article, box, media, page) from given source language to language specified as $destinationLanguageID.
975 *
976 * @param int $sourceLanguageID
977 * @param int $destinationLanguageID
978 */
979 public static function copyLanguageContent($sourceLanguageID, $destinationLanguageID)
980 {
981 // article content
982 $sql = "INSERT IGNORE INTO wcf" . WCF_N . "_article_content
983 (articleID, languageID, title, teaser, content, imageID, hasEmbeddedObjects)
984 SELECT articleID, ?, title, teaser, content, imageID, hasEmbeddedObjects
985 FROM wcf" . WCF_N . "_article_content
986 WHERE languageID = ?";
987 $statement = WCF::getDB()->prepareStatement($sql);
988 $statement->execute([$destinationLanguageID, $sourceLanguageID]);
989
990 // box content
991 $sql = "INSERT IGNORE INTO wcf" . WCF_N . "_box_content
992 (boxID, languageID, title, content, imageID, hasEmbeddedObjects)
993 SELECT boxID, ?, title, content, imageID, hasEmbeddedObjects
994 FROM wcf" . WCF_N . "_box_content
995 WHERE languageID = ?";
996 $statement = WCF::getDB()->prepareStatement($sql);
997 $statement->execute([$destinationLanguageID, $sourceLanguageID]);
998
999 // create tpl files
1000 $sql = "SELECT *
1001 FROM wcf" . WCF_N . "_box_content
1002 WHERE boxID IN (
1003 SELECT boxID
1004 FROM wcf" . WCF_N . "_box
1005 WHERE boxType = ?
1006 )
1007 AND languageID = ?";
1008 $statement = WCF::getDB()->prepareStatement($sql);
1009 $statement->execute(['tpl', $destinationLanguageID]);
1010 while ($row = $statement->fetchArray()) {
1011 \file_put_contents(
1012 WCF_DIR . 'templates/__cms_box_' . $row['boxID'] . '_' . $destinationLanguageID . '.tpl',
1013 $row['content']
1014 );
1015 }
1016
1017 // media content
1018 $sql = "INSERT IGNORE INTO wcf" . WCF_N . "_media_content
1019 (mediaID, languageID, title, caption, altText)
1020 SELECT mediaID, ?, title, caption, altText
1021 FROM wcf" . WCF_N . "_media_content
1022 WHERE languageID = ?";
1023 $statement = WCF::getDB()->prepareStatement($sql);
1024 $statement->execute([$destinationLanguageID, $sourceLanguageID]);
1025
1026 // page content
1027 $sql = "INSERT IGNORE INTO wcf" . WCF_N . "_page_content
1028 (pageID, languageID, title, content, metaDescription, customURL, hasEmbeddedObjects)
1029 SELECT pageID, ?, title, content, metaDescription, CASE WHEN customURL <> '' THEN CONCAT(customURL, '_', ?) ELSE '' END, hasEmbeddedObjects
1030 FROM wcf" . WCF_N . "_page_content
1031 WHERE languageID = ?";
1032 $statement = WCF::getDB()->prepareStatement($sql);
1033 $statement->execute([$destinationLanguageID, $destinationLanguageID, $sourceLanguageID]);
1034
1035 // create tpl files
1036 $sql = "SELECT *
1037 FROM wcf" . WCF_N . "_page_content
1038 WHERE pageID IN (
1039 SELECT pageID
1040 FROM wcf" . WCF_N . "_page
1041 WHERE pageType = ?
1042 )
1043 AND languageID = ?";
1044 $statement = WCF::getDB()->prepareStatement($sql);
1045 $statement->execute(['tpl', $destinationLanguageID]);
1046 while ($row = $statement->fetchArray()) {
1047 \file_put_contents(
1048 WCF_DIR . 'templates/__cms_page_' . $row['pageID'] . '_' . $destinationLanguageID . '.tpl',
1049 $row['content']
1050 );
1051 }
1052
1053 PageEditor::resetCache();
f855889a 1054
1055 EventHandler::getInstance()->fire(new LanguageContentCopying(
1056 new Language($sourceLanguageID),
1057 new Language($destinationLanguageID)
1058 ));
a9229942 1059 }
11ade432 1060}