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