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