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