77abb43879fd8838b5bdbb6c1fb7fa0a7304fd93
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / plugin / PagePackageInstallationPlugin.class.php
1 <?php
2
3 namespace wcf\system\package\plugin;
4
5 use wcf\data\language\Language;
6 use wcf\data\package\PackageCache;
7 use wcf\data\page\Page;
8 use wcf\data\page\PageAction;
9 use wcf\data\page\PageEditor;
10 use wcf\data\page\PageList;
11 use wcf\data\page\PageNode;
12 use wcf\data\page\PageNodeTree;
13 use wcf\page\IPage;
14 use wcf\system\devtools\pip\IDevtoolsPipEntryList;
15 use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
16 use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
17 use wcf\system\exception\SystemException;
18 use wcf\system\form\builder\container\FormContainer;
19 use wcf\system\form\builder\container\TabFormContainer;
20 use wcf\system\form\builder\container\TabMenuFormContainer;
21 use wcf\system\form\builder\field\BooleanFormField;
22 use wcf\system\form\builder\field\ClassNameFormField;
23 use wcf\system\form\builder\field\dependency\ValueFormFieldDependency;
24 use wcf\system\form\builder\field\ItemListFormField;
25 use wcf\system\form\builder\field\MultilineTextFormField;
26 use wcf\system\form\builder\field\option\OptionFormField;
27 use wcf\system\form\builder\field\RadioButtonFormField;
28 use wcf\system\form\builder\field\SingleSelectionFormField;
29 use wcf\system\form\builder\field\TextFormField;
30 use wcf\system\form\builder\field\TitleFormField;
31 use wcf\system\form\builder\field\user\group\option\UserGroupOptionFormField;
32 use wcf\system\form\builder\field\validation\FormFieldValidationError;
33 use wcf\system\form\builder\field\validation\FormFieldValidator;
34 use wcf\system\form\builder\field\validation\FormFieldValidatorUtil;
35 use wcf\system\form\builder\IFormDocument;
36 use wcf\system\language\LanguageFactory;
37 use wcf\system\page\handler\IMenuPageHandler;
38 use wcf\system\request\RouteHandler;
39 use wcf\system\search\SearchIndexManager;
40 use wcf\system\WCF;
41 use wcf\util\StringUtil;
42
43 /**
44 * Installs, updates and deletes CMS pages.
45 *
46 * @author Alexander Ebert, Matthias Schmidt
47 * @copyright 2001-2019 WoltLab GmbH
48 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
49 * @package WoltLabSuite\Core\Acp\Package\Plugin
50 * @since 3.0
51 */
52 class PagePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements
53 IGuiPackageInstallationPlugin
54 {
55 use TXmlGuiPackageInstallationPlugin;
56
57 /**
58 * @inheritDoc
59 */
60 public $className = PageEditor::class;
61
62 /**
63 * page content
64 * @var mixed[]
65 */
66 protected $content = [];
67
68 /**
69 * pages objects
70 * @var Page[]
71 */
72 protected $pages = [];
73
74 /**
75 * @inheritDoc
76 */
77 public $tagName = 'page';
78
79 /**
80 * @inheritDoc
81 */
82 protected function handleDelete(array $items)
83 {
84 $pages = [];
85 foreach ($items as $item) {
86 $page = Page::getPageByIdentifier($item['attributes']['identifier']);
87 if ($page !== null && $page->pageID && $page->packageID == $this->installation->getPackageID()) {
88 $pages[] = $page;
89 }
90 }
91
92 if (!empty($pages)) {
93 $pageAction = new PageAction($pages, 'delete');
94 $pageAction->executeAction();
95 }
96 }
97
98 /**
99 * @inheritDoc
100 */
101 protected function getElement(\DOMXPath $xpath, array &$elements, \DOMElement $element)
102 {
103 $nodeValue = $element->nodeValue;
104
105 // read content
106 if ($element->tagName === 'content') {
107 if (!isset($elements['content'])) {
108 $elements['content'] = [];
109 }
110
111 $children = [];
112 /** @var \DOMElement $child */
113 foreach ($xpath->query('child::*', $element) as $child) {
114 $children[$child->tagName] = $child->nodeValue;
115 }
116
117 $elements[$element->tagName][$element->getAttribute('language')] = $children;
118 } elseif ($element->tagName === 'name') {
119 // <name> can occur multiple times using the `language` attribute
120 if (!isset($elements['name'])) {
121 $elements['name'] = [];
122 }
123
124 $elements['name'][$element->getAttribute('language')] = $element->nodeValue;
125 } else {
126 $elements[$element->tagName] = $nodeValue;
127 }
128 }
129
130 /**
131 * @inheritDoc
132 * @throws SystemException
133 */
134 protected function prepareImport(array $data)
135 {
136 $pageType = $data['elements']['pageType'];
137
138 if (!empty($data['elements']['content'])) {
139 $content = [];
140 foreach ($data['elements']['content'] as $language => $contentData) {
141 if ($pageType != 'system' && !RouteHandler::isValidCustomUrl($contentData['customURL'])) {
142 throw new SystemException("Invalid custom url for page content '" . $language . "', page identifier '" . $data['attributes']['identifier'] . "'");
143 }
144
145 $content[$language] = [
146 'content' => (!empty($contentData['content'])) ? StringUtil::trim($contentData['content']) : '',
147 'customURL' => (!empty($contentData['customURL'])) ? StringUtil::trim($contentData['customURL']) : '',
148 'metaDescription' => (!empty($contentData['metaDescription'])) ? StringUtil::trim($contentData['metaDescription']) : '',
149 'title' => (!empty($contentData['title'])) ? StringUtil::trim($contentData['title']) : '',
150 ];
151 }
152
153 $data['elements']['content'] = $content;
154 }
155
156 // pick the display name by choosing the default language, or 'en' or '' (empty string)
157 $defaultLanguageCode = LanguageFactory::getInstance()->getDefaultLanguage()->getFixedLanguageCode();
158 if (isset($data['elements']['name'][$defaultLanguageCode])) {
159 // use the default language
160 $name = $data['elements']['name'][$defaultLanguageCode];
161 } elseif (isset($data['elements']['name']['en'])) {
162 // use the value for English
163 $name = $data['elements']['name']['en'];
164 } elseif (isset($data['elements']['name'][''])) {
165 // fallback to the display name without/empty language attribute
166 $name = $data['elements']['name'][''];
167 } else {
168 // use whichever value is present, regardless of the language
169 $name = \reset($data['elements']['name']);
170 }
171
172 $parentPageID = null;
173 if (!empty($data['elements']['parent'])) {
174 $sql = "SELECT pageID
175 FROM wcf" . WCF_N . "_" . $this->tableName . "
176 WHERE identifier = ?";
177 $statement = WCF::getDB()->prepareStatement($sql, 1);
178 $statement->execute([$data['elements']['parent']]);
179 $row = $statement->fetchSingleRow();
180 if ($row === false) {
181 throw new SystemException("Unknown parent page '" . $data['elements']['parent'] . "' for page identifier '" . $data['attributes']['identifier'] . "'");
182 }
183
184 $parentPageID = $row['pageID'];
185 }
186
187 // validate page type
188 $controller = '';
189 $handler = '';
190 $controllerCustomURL = '';
191 $identifier = $data['attributes']['identifier'];
192 $isMultilingual = 0;
193 switch ($pageType) {
194 case 'system':
195 if (empty($data['elements']['controller'])) {
196 throw new SystemException("Missing required element 'controller' for 'system'-type page '{$identifier}'");
197 }
198 $controller = $data['elements']['controller'];
199
200 if (!empty($data['elements']['handler'])) {
201 $handler = $data['elements']['handler'];
202 }
203
204 // @deprecated
205 if (!empty($data['elements']['controllerCustomURL'])) {
206 $controllerCustomURL = $data['elements']['controllerCustomURL'];
207 if ($controllerCustomURL && !RouteHandler::isValidCustomUrl($controllerCustomURL)) {
208 throw new SystemException("Invalid custom url for page identifier '" . $data['attributes']['identifier'] . "'");
209 }
210 }
211
212 break;
213
214 case 'html':
215 case 'text':
216 case 'tpl':
217 if (empty($data['elements']['content'])) {
218 throw new SystemException("Missing required 'content' element(s) for page '{$identifier}'");
219 }
220
221 if (\count($data['elements']['content']) === 1) {
222 if (!isset($data['elements']['content'][''])) {
223 throw new SystemException("Expected one 'content' element without a 'language' attribute for page '{$identifier}'");
224 }
225 } else {
226 $isMultilingual = 1;
227 if (isset($data['elements']['content'][''])) {
228 throw new SystemException("Cannot mix 'content' elements with and without 'language' attribute for page '{$identifier}'");
229 }
230 }
231
232 break;
233
234 default:
235 throw new SystemException("Unknown type '{$pageType}' for page '{$identifier}");
236 break;
237 }
238
239 // get application package id
240 $applicationPackageID = 1;
241 if ($this->installation->getPackage()->isApplication) {
242 $applicationPackageID = $this->installation->getPackageID();
243 }
244 if (!empty($data['elements']['application'])) {
245 $application = PackageCache::getInstance()->getPackageByIdentifier($data['elements']['application']);
246 if ($application === null || !$application->isApplication) {
247 throw new SystemException("Unknown application '" . $data['elements']['application'] . "' for page '{$identifier}");
248 }
249 $applicationPackageID = $application->packageID;
250 }
251
252 return [
253 'pageType' => $pageType,
254 'content' => (!empty($data['elements']['content'])) ? $data['elements']['content'] : [],
255 'controller' => $controller,
256 'handler' => $handler,
257 'controllerCustomURL' => $controllerCustomURL,
258 'identifier' => $identifier,
259 'isMultilingual' => $isMultilingual,
260 'lastUpdateTime' => TIME_NOW,
261 'name' => $name,
262 'originIsSystem' => 1,
263 'parentPageID' => $parentPageID,
264 'applicationPackageID' => $applicationPackageID,
265 'requireObjectID' => (!empty($data['elements']['requireObjectID'])) ? 1 : 0,
266 'options' => $data['elements']['options'] ?? '',
267 'permissions' => $data['elements']['permissions'] ?? '',
268 'hasFixedParent' => ($pageType == 'system' && !empty($data['elements']['hasFixedParent'])) ? 1 : 0,
269 'cssClassName' => $data['elements']['cssClassName'] ?? '',
270 'availableDuringOfflineMode' => (!empty($data['elements']['availableDuringOfflineMode'])) ? 1 : 0,
271 'allowSpidersToIndex' => (!empty($data['elements']['allowSpidersToIndex'])) ? 1 : 0,
272 'excludeFromLandingPage' => (!empty($data['elements']['excludeFromLandingPage'])) ? 1 : 0,
273 ];
274 }
275
276 /**
277 * @inheritDoc
278 */
279 protected function findExistingItem(array $data)
280 {
281 $sql = "SELECT *
282 FROM wcf" . WCF_N . "_" . $this->tableName . "
283 WHERE identifier = ?
284 AND packageID = ?";
285 $parameters = [
286 $data['identifier'],
287 $this->installation->getPackageID(),
288 ];
289
290 return [
291 'sql' => $sql,
292 'parameters' => $parameters,
293 ];
294 }
295
296 /**
297 * @inheritDoc
298 */
299 protected function import(array $row, array $data)
300 {
301 // extract content
302 $content = $data['content'];
303 unset($data['content']);
304
305 /** @var Page $page */
306 if (!empty($row)) {
307 // allow update of `controller`, `handler` and `excludeFromLandingPage`
308 // only, prevents user modifications form being overwritten
309 if (!empty($data['controller'])) {
310 $allowSpidersToIndex = $row['allowSpidersToIndex'] ?? 0;
311 if ($allowSpidersToIndex == 2) {
312 // The value `2` resolves to be true-ish, eventually resulting in the same behavior
313 // when setting it to `1`. This value is special to the 3.0 -> 3.1 upgrade, because
314 // it force-enables the visibility, while also being some sort of indicator for non-
315 // user-modified values. The page edit form will set it to either `1` or `0`, there-
316 // fore `2` means that we can safely update the value w/o breaking the user's choice.
317 $allowSpidersToIndex = $data['allowSpidersToIndex'];
318 }
319
320 $page = parent::import($row, [
321 'controller' => $data['controller'],
322 'handler' => $data['handler'] ?? '',
323 'options' => $data['options'] ?? '',
324 'permissions' => $data['permissions'] ?? '',
325 'excludeFromLandingPage' => $data['excludeFromLandingPage'] ?? 0,
326 'allowSpidersToIndex' => $allowSpidersToIndex,
327 'requireObjectID' => $data['requireObjectID'],
328 ]);
329 } else {
330 $baseClass = \call_user_func([$this->className, 'getBaseClass']);
331 $page = new $baseClass(null, $row);
332 }
333 } else {
334 // import
335 $page = parent::import($row, $data);
336 }
337
338 // store content for later import
339 $this->pages[$page->pageID] = $page;
340 $this->content[$page->pageID] = $content;
341
342 return $page;
343 }
344
345 /**
346 * @inheritDoc
347 */
348 protected function postImport()
349 {
350 if (!empty($this->content)) {
351 $sql = "SELECT COUNT(*) AS count
352 FROM wcf" . WCF_N . "_page_content
353 WHERE pageID = ?
354 AND languageID IS NULL";
355 $statement = WCF::getDB()->prepareStatement($sql);
356
357 $sql = "INSERT IGNORE INTO wcf" . WCF_N . "_page_content
358 (pageID, languageID, title, content, metaDescription, customURL)
359 VALUES (?, ?, ?, ?, ?, ?)";
360 $insertStatement = WCF::getDB()->prepareStatement($sql);
361
362 WCF::getDB()->beginTransaction();
363 foreach ($this->content as $pageID => $contentData) {
364 foreach ($contentData as $languageCode => $content) {
365 $languageID = null;
366 if ($languageCode != '') {
367 $language = LanguageFactory::getInstance()->getLanguageByCode($languageCode);
368 if ($language === null) {
369 continue;
370 }
371
372 $languageID = $language->languageID;
373 }
374
375 if ($languageID === null) {
376 $statement->execute([$pageID]);
377 if ($statement->fetchColumn()) {
378 continue;
379 }
380 }
381
382 $insertStatement->execute([
383 $pageID,
384 $languageID,
385 $content['title'],
386 $content['content'],
387 $content['metaDescription'],
388 $content['customURL'],
389 ]);
390
391 // generate template if page's type is 'tpl'
392 $page = new Page($pageID);
393 if ($page->pageType == 'tpl') {
394 (new PageEditor($page))->updateTemplate($languageID, $content['content']);
395 }
396 }
397 }
398 WCF::getDB()->commitTransaction();
399
400 // create search index tables
401 SearchIndexManager::getInstance()->createSearchIndices();
402
403 // update search index
404 foreach ($this->pages as $pageID => $page) {
405 if ($page->pageType == 'text' || $page->pageType == 'html') {
406 foreach ($page->getPageContents() as $languageID => $pageContent) {
407 SearchIndexManager::getInstance()->set(
408 'com.woltlab.wcf.page',
409 $pageContent->pageContentID,
410 $pageContent->content,
411 $pageContent->title,
412 0,
413 null,
414 '',
415 $languageID ?: null
416 );
417 }
418 }
419 }
420 }
421 }
422
423 /**
424 * @inheritDoc
425 * @since 3.1
426 */
427 public static function getSyncDependencies()
428 {
429 return ['language'];
430 }
431
432 /**
433 * @inheritDoc
434 * @since 5.2
435 */
436 protected function addFormFields(IFormDocument $form)
437 {
438 $tabContainter = TabMenuFormContainer::create('tabMenu');
439 $form->appendChild($tabContainter);
440
441 $dataTab = TabFormContainer::create('dataTab')
442 ->label('wcf.global.form.data');
443 $tabContainter->appendChild($dataTab);
444 $dataContainer = FormContainer::create('dataTabData');
445 $dataTab->appendChild($dataContainer);
446
447 $contentTab = TabFormContainer::create('contentTab')
448 ->label('wcf.acp.pip.page.content');
449 $tabContainter->appendChild($contentTab);
450 $contentContainer = FormContainer::create('contentTabContent');
451 $contentTab->appendChild($contentContainer);
452
453 $dataContainer->appendChildren([
454 TextFormField::create('identifier')
455 ->label('wcf.acp.pip.page.identifier')
456 ->description('wcf.acp.pip.page.identifier.description')
457 ->required()
458 ->addValidator(FormFieldValidatorUtil::getDotSeparatedStringValidator(
459 'wcf.acp.pip.page.identifier',
460 4
461 ))
462 ->addValidator(new FormFieldValidator('uniqueness', function (TextFormField $formField) {
463 if (
464 $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE
465 || $this->editedEntry->getAttribute('identifier') !== $formField->getValue()
466 ) {
467 $pageList = new PageList();
468 $pageList->getConditionBuilder()->add('identifier = ?', [$formField->getValue()]);
469
470 if ($pageList->countObjects() > 0) {
471 $formField->addValidationError(
472 new FormFieldValidationError(
473 'notUnique',
474 'wcf.acp.pip.page.identifier.error.notUnique'
475 )
476 );
477 }
478 }
479 })),
480
481 RadioButtonFormField::create('pageType')
482 ->label('wcf.acp.pip.page.pageType')
483 ->description('wcf.acp.pip.page.pageType.description')
484 ->options(\array_combine(Page::$availablePageTypes, Page::$availablePageTypes))
485 ->addClass('floated'),
486
487 TextFormField::create('name')
488 ->label('wcf.acp.pip.page.name')
489 ->description('wcf.acp.pip.page.name.description')
490 ->required()
491 ->i18n()
492 ->i18nRequired()
493 ->languageItemPattern('__NONE__'),
494
495 ClassNameFormField::create('controller')
496 ->label('wcf.acp.pip.page.controller')
497 ->implementedInterface(IPage::class)
498 ->required(),
499
500 ClassNameFormField::create('handler')
501 ->label('wcf.acp.pip.page.handler')
502 ->implementedInterface(IMenuPageHandler::class),
503
504 BooleanFormField::create('requireObjectID')
505 ->label('wcf.acp.pip.page.requireObjectID')
506 ->description('wcf.acp.pip.page.requireObjectID.description'),
507
508 SingleSelectionFormField::create('parent')
509 ->label('wcf.acp.pip.page.parent')
510 ->required()
511 ->filterable()
512 ->options(function () {
513 $pageNodeList = (new PageNodeTree())->getNodeList();
514
515 $nestedOptions = [
516 [
517 'depth' => 0,
518 'label' => 'wcf.global.noSelection',
519 'value' => '',
520 ],
521 ];
522
523 $packageIDs = \array_merge(
524 [$this->installation->getPackage()->packageID],
525 \array_keys($this->installation->getPackage()->getAllRequiredPackages())
526 );
527
528 /** @var PageNode $pageNode */
529 foreach ($pageNodeList as $pageNode) {
530 if (\in_array($pageNode->packageID, $packageIDs)) {
531 $nestedOptions[] = [
532 'depth' => $pageNode->getDepth() - 1,
533 'label' => $pageNode->name,
534 'value' => $pageNode->identifier,
535 ];
536 }
537 }
538
539 return $nestedOptions;
540 }, true)
541 ->addValidator(new FormFieldValidator(
542 'selfParent',
543 static function (SingleSelectionFormField $formField) {
544 /** @var TextFormField $identifier */
545 $identifier = $formField->getDocument()->getNodeById('identifier');
546
547 if ($identifier->getSaveValue() === $formField->getValue()) {
548 $formField->addValidationError(
549 new FormFieldValidationError(
550 'selfParent',
551 'wcf.acp.pip.page.parent.error.selfParent'
552 )
553 );
554 }
555 }
556 )),
557
558 BooleanFormField::create('hasFixedParent')
559 ->label('wcf.acp.pip.page.hasFixedParent')
560 ->description('wcf.acp.pip.page.hasFixedParent.description'),
561
562 OptionFormField::create()
563 ->description('wcf.acp.pip.page.options.description')
564 ->packageIDs(\array_merge(
565 [$this->installation->getPackage()->packageID],
566 \array_keys($this->installation->getPackage()->getAllRequiredPackages())
567 )),
568
569 UserGroupOptionFormField::create()
570 ->description('wcf.acp.pip.page.permissions.description')
571 ->packageIDs(\array_merge(
572 [$this->installation->getPackage()->packageID],
573 \array_keys($this->installation->getPackage()->getAllRequiredPackages())
574 )),
575
576 ItemListFormField::create('cssClassName')
577 ->label('wcf.acp.pip.page.cssClassName')
578 ->description('wcf.acp.pip.page.cssClassName.description'),
579
580 BooleanFormField::create('allowSpidersToIndex')
581 ->label('wcf.acp.pip.page.allowSpidersToIndex'),
582
583 BooleanFormField::create('excludeFromLandingPage')
584 ->label('wcf.acp.pip.page.excludeFromLandingPage'),
585
586 BooleanFormField::create('availableDuringOfflineMode')
587 ->label('wcf.acp.pip.page.availableDuringOfflineMode'),
588 ]);
589
590 $contentContainer->appendChildren([
591 TitleFormField::create('contentTitle')
592 ->objectProperty('title')
593 ->label('wcf.acp.pip.page.contentTitle')
594 ->i18n()
595 ->i18nRequired()
596 ->languageItemPattern('__NONE__'),
597
598 MultilineTextFormField::create('contentContent')
599 ->objectProperty('content')
600 ->label('wcf.acp.pip.page.contentContent')
601 ->i18n()
602 ->i18nRequired()
603 ->languageItemPattern('__NONE__'),
604
605 TextFormField::create('contentCustomURL')
606 ->objectProperty('customURL')
607 ->label('wcf.acp.pip.page.contentCustomURL')
608 ->i18n()
609 ->i18nRequired()
610 ->languageItemPattern('__NONE__'),
611
612 TextFormField::create('contentMetaDescription')
613 ->objectProperty('metaDescription')
614 ->label('wcf.acp.pip.page.contentMetaDescription')
615 ->i18n()
616 ->i18nRequired()
617 ->languageItemPattern('__NONE__'),
618 ]);
619
620 // dependencies
621
622 /** @var RadioButtonFormField $pageType */
623 $pageType = $form->getNodeById('pageType');
624 foreach (['controller', 'handler', 'requireObjectID'] as $systemElement) {
625 $form->getNodeById($systemElement)->addDependency(
626 ValueFormFieldDependency::create('pageType')
627 ->field($pageType)
628 ->values(['system'])
629 );
630 }
631
632 foreach (['contentContent', 'contentCustomURL', 'contentMetaDescription'] as $nonSystemElement) {
633 $form->getNodeById($nonSystemElement)->addDependency(
634 ValueFormFieldDependency::create('pageType')
635 ->field($pageType)
636 ->values(['system'])
637 ->negate()
638 );
639 }
640 }
641
642 /**
643 * @inheritDoc
644 * @since 5.2
645 */
646 protected function fetchElementData(\DOMElement $element, $saveData)
647 {
648 $data = [
649 'identifier' => $element->getAttribute('identifier'),
650 'originIsSystem' => 1,
651 'packageID' => $this->installation->getPackageID(),
652 'pageType' => $element->getElementsByTagName('pageType')->item(0)->nodeValue,
653 'name' => [],
654 'title' => [],
655 'content' => [],
656 'customURL' => [],
657 'metaDescription' => [],
658 ];
659
660 /** @var \DOMElement $name */
661 foreach ($element->getElementsByTagName('name') as $name) {
662 $data['name'][LanguageFactory::getInstance()->getLanguageByCode($name->getAttribute('language'))->languageID] = $name->nodeValue;
663 }
664
665 $optionalElements = [
666 'controller',
667 'handler',
668 'hasFixedParent',
669 'parent',
670 'options',
671 'permissions',
672 'cssClassName',
673 'allowSpidersToIndex',
674 'excludeFromLandingPage',
675 'availableDuringOfflineMode',
676 'requireObjectID',
677 ];
678
679 $zeroDefaultOptions = [
680 'hasFixedParent',
681 'allowSpidersToIndex',
682 'excludeFromLandingPage',
683 'availableDuringOfflineMode',
684 'requireObjectID',
685 ];
686
687 foreach ($optionalElements as $optionalElementName) {
688 $optionalElement = $element->getElementsByTagName($optionalElementName)->item(0);
689 if ($optionalElement !== null) {
690 $data[$optionalElementName] = $optionalElement->nodeValue;
691 } elseif ($saveData) {
692 if (\in_array($optionalElementName, $zeroDefaultOptions)) {
693 $data[$optionalElementName] = 0;
694 } else {
695 $data[$optionalElementName] = '';
696 }
697 }
698 }
699
700 $readData = static function ($languageID, \DOMElement $content) use (&$data, $saveData) {
701 foreach (['title', 'content', 'customURL', 'metaDescription'] as $contentElementName) {
702 $contentElement = $content->getElementsByTagName($contentElementName)->item(0);
703 if (!isset($data[$contentElementName])) {
704 $data[$contentElementName] = [];
705 }
706
707 if ($contentElement) {
708 $data[$contentElementName][$languageID] = $contentElement->nodeValue;
709 } elseif ($saveData) {
710 $data[$contentElementName][$languageID] = '';
711 }
712 }
713 };
714
715 /** @var \DOMElement $content */
716 foreach ($element->getElementsByTagName('content') as $content) {
717 $languageCode = $content->getAttribute('language');
718 if ($languageCode === '') {
719 foreach (LanguageFactory::getInstance()->getLanguages() as $language) {
720 $readData($language->languageID, $content);
721 }
722 } else {
723 $readData(
724 LanguageFactory::getInstance()->getLanguageByCode($languageCode)->languageID,
725 $content
726 );
727 }
728 }
729
730 if ($saveData) {
731 if ($this->editedEntry !== null) {
732 unset($data['name']);
733 } else {
734 $titles = [];
735 foreach ($data['name'] as $languageID => $title) {
736 $titles[LanguageFactory::getInstance()->getLanguage($languageID)->languageCode] = $title;
737 }
738
739 if (isset($data['name'][LanguageFactory::getInstance()->getDefaultLanguage()->languageID])) {
740 // use the default language
741 $data['name'] = $data['name'][LanguageFactory::getInstance()->getDefaultLanguage()->languageID];
742 } else {
743 $english = LanguageFactory::getInstance()->getLanguageByCode('en');
744 if ($english !== null && isset($data['name'][$english->languageID])) {
745 $data['name'] = $data['name'][$english->languageID];
746 } else {
747 $data['name'] = \reset($data['name']);
748 }
749 }
750 }
751
752 $content = [];
753
754 foreach (['title', 'content', 'customURL', 'metaDescription'] as $contentProperty) {
755 if (!empty($data[$contentProperty])) {
756 foreach ($data[$contentProperty] as $languageID => $value) {
757 $languageCode = LanguageFactory::getInstance()->getLanguage($languageID)->languageCode;
758
759 if (!isset($content[$languageCode])) {
760 $content[$languageCode] = [];
761 }
762
763 $content[$languageCode][$contentProperty] = $value;
764 }
765 }
766
767 unset($data[$contentProperty]);
768 }
769
770 foreach ($content as $languageCode => $values) {
771 foreach (['title', 'content', 'customURL', 'metaDescription'] as $contentProperty) {
772 if (!isset($values[$contentProperty])) {
773 $content[$languageCode][$contentProperty] = '';
774 }
775 }
776 }
777
778 $data['content'] = $content;
779
780 if (isset($data['parent'])) {
781 $parent = $data['parent'];
782 unset($data['parent']);
783
784 if (!empty($parent)) {
785 $data['parentPageID'] = Page::getPageByIdentifier($parent)->pageID;
786 }
787 }
788 }
789
790 return $data;
791 }
792
793 /**
794 * @inheritDoc
795 * @since 5.2
796 */
797 public function getElementIdentifier(\DOMElement $element)
798 {
799 return $element->getAttribute('identifier');
800 }
801
802 /**
803 * @inheritDoc
804 * @since 5.2
805 */
806 protected function setEntryListKeys(IDevtoolsPipEntryList $entryList)
807 {
808 $entryList->setKeys([
809 'identifier' => 'wcf.acp.pip.page.identifier',
810 'pageType' => 'wcf.acp.pip.page.pageType',
811 ]);
812 }
813
814 /**
815 * @inheritDoc
816 * @since 5.2
817 */
818 protected function prepareXmlElement(\DOMDocument $document, IFormDocument $form)
819 {
820 $formData = $form->getData();
821 $data = $formData['data'];
822
823 $page = $document->createElement($this->tagName);
824 $page->setAttribute('identifier', $data['identifier']);
825
826 $page->appendChild($document->createElement('pageType', $data['pageType']));
827
828 $this->appendElementChildren(
829 $page,
830 ['controller' => ''],
831 $form
832 );
833
834 foreach ($formData['name_i18n'] as $languageID => $name) {
835 $name = $document->createElement('name', $this->getAutoCdataValue($name));
836 $name->setAttribute('language', LanguageFactory::getInstance()->getLanguage($languageID)->languageCode);
837
838 $page->appendChild($name);
839 }
840
841 $this->appendElementChildren(
842 $page,
843 [
844 'handler' => '',
845 'hasFixedParent' => 0,
846 'parent' => '',
847 'options' => '',
848 'permissions' => '',
849 'cssClassName' => '',
850 'allowSpidersToIndex' => 0,
851 'excludeFromLandingPage' => 0,
852 'availableDuringOfflineMode' => 0,
853 'requireObjectID' => 0,
854 ],
855 $form
856 );
857
858 $languages = LanguageFactory::getInstance()->getLanguages();
859
860 // sort languages by language code but keep English first
861 \uasort($languages, static function (Language $language1, Language $language2) {
862 if ($language1->languageCode === 'en') {
863 return -1;
864 } elseif ($language2->languageCode === 'en') {
865 return 1;
866 }
867
868 return $language1->languageCode <=> $language2->languageCode;
869 });
870
871 foreach ($languages as $language) {
872 $content = null;
873
874 foreach (['title', 'content', 'customURL', 'metaDescription'] as $property) {
875 if (!empty($formData[$property . '_i18n'][$language->languageID])) {
876 if ($content === null) {
877 $content = $document->createElement('content');
878 $content->setAttribute('language', $language->languageCode);
879
880 $page->appendChild($content);
881 }
882
883 if ($property === 'content') {
884 $contentContent = $document->createElement('content');
885 $contentContent->appendChild(
886 $document->createCDATASection(
887 StringUtil::escapeCDATA(StringUtil::unifyNewlines(
888 $formData[$property . '_i18n'][$language->languageID]
889 ))
890 )
891 );
892
893 $content->appendChild($contentContent);
894 } else {
895 $content->appendChild(
896 $document->createElement(
897 $property,
898 $formData[$property . '_i18n'][$language->languageID]
899 )
900 );
901 }
902 }
903 }
904 }
905
906 return $page;
907 }
908 }