Remove unused local variables
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / plugin / PagePackageInstallationPlugin.class.php
index 9a0c0b29fbc84c466ab2a869c4c3a1be7e4dc960..76d6050df7b45abc57ad510134274ad96fe20012 100644 (file)
@@ -1,5 +1,7 @@
 <?php
+
 namespace wcf\system\package\plugin;
+
 use wcf\data\language\Language;
 use wcf\data\package\PackageCache;
 use wcf\data\page\Page;
@@ -40,857 +42,867 @@ use wcf\util\StringUtil;
 
 /**
  * Installs, updates and deletes CMS pages.
- * 
- * @author     Alexander Ebert, Matthias Schmidt
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package    WoltLabSuite\Core\Acp\Package\Plugin
- * @since      3.0
+ *
+ * @author  Alexander Ebert, Matthias Schmidt
+ * @copyright   2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Package\Plugin
+ * @since   3.0
  */
-class PagePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin {
-       use TXmlGuiPackageInstallationPlugin;
-       
-       /**
-        * @inheritDoc
-        */
-       public $className = PageEditor::class;
-       
-       /**
-        * page content
-        * @var mixed[]
-        */
-       protected $content = [];
-       
-       /**
-        * pages objects
-        * @var Page[]
-        */
-       protected $pages = [];
-       
-       /**
-        * @inheritDoc
-        */
-       public $tagName = 'page';
-       
-       /**
-        * @inheritDoc
-        */
-       protected function handleDelete(array $items) {
-               $pages = [];
-               foreach ($items as $item) {
-                       $page = Page::getPageByIdentifier($item['attributes']['identifier']);
-                       if ($page !== null && $page->pageID && $page->packageID == $this->installation->getPackageID()) $pages[] = $page;
-               }
-               
-               if (!empty($pages)) {
-                       $pageAction = new PageAction($pages, 'delete');
-                       $pageAction->executeAction();
-               }
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       protected function getElement(\DOMXPath $xpath, array &$elements, \DOMElement $element) {
-               $nodeValue = $element->nodeValue;
-               
-               // read content
-               if ($element->tagName === 'content') {
-                       if (!isset($elements['content'])) $elements['content'] = [];
-                       
-                       $children = [];
-                       /** @var \DOMElement $child */
-                       foreach ($xpath->query('child::*', $element) as $child) {
-                               $children[$child->tagName] = $child->nodeValue;
-                       }
-                       
-                       $elements[$element->tagName][$element->getAttribute('language')] = $children;
-               }
-               else if ($element->tagName === 'name') {
-                       // <name> can occur multiple times using the `language` attribute
-                       if (!isset($elements['name'])) $elements['name'] = [];
-                       
-                       $elements['name'][$element->getAttribute('language')] = $element->nodeValue;
-               }
-               else {
-                       $elements[$element->tagName] = $nodeValue;
-               }
-       }
-       
-       /**
-        * @inheritDoc
-        * @throws      SystemException
-        */
-       protected function prepareImport(array $data) {
-               $pageType = $data['elements']['pageType'];
-               
-               if (!empty($data['elements']['content'])) {
-                       $content = [];
-                       foreach ($data['elements']['content'] as $language => $contentData) {
-                               if ($pageType != 'system' && !RouteHandler::isValidCustomUrl($contentData['customURL'])) {
-                                       throw new SystemException("Invalid custom url for page content '" . $language . "', page identifier '" . $data['attributes']['identifier'] . "'");
-                               }
-                               
-                               $content[$language] = [
-                                       'content' => (!empty($contentData['content'])) ? StringUtil::trim($contentData['content']) : '',
-                                       'customURL' => (!empty($contentData['customURL'])) ? StringUtil::trim($contentData['customURL']) : '',
-                                       'metaDescription' => (!empty($contentData['metaDescription'])) ? StringUtil::trim($contentData['metaDescription']) : '',
-                                       'metaKeywords' => (!empty($contentData['metaKeywords'])) ? StringUtil::trim($contentData['metaKeywords']) : '',
-                                       'title' => (!empty($contentData['title'])) ? StringUtil::trim($contentData['title']) : ''
-                               ];
-                       }
-                       
-                       $data['elements']['content'] = $content;
-               }
-               
-               // pick the display name by choosing the default language, or 'en' or '' (empty string)
-               $defaultLanguageCode = LanguageFactory::getInstance()->getDefaultLanguage()->getFixedLanguageCode();
-               if (isset($data['elements']['name'][$defaultLanguageCode])) {
-                       // use the default language
-                       $name = $data['elements']['name'][$defaultLanguageCode];
-               }
-               else if (isset($data['elements']['name']['en'])) {
-                       // use the value for English
-                       $name = $data['elements']['name']['en'];
-               }
-               else if (isset($data['elements']['name'][''])) {
-                       // fallback to the display name without/empty language attribute
-                       $name = $data['elements']['name'][''];
-               }
-               else {
-                       // use whichever value is present, regardless of the language
-                       $name = reset($data['elements']['name']);
-               }
-               
-               $parentPageID = null;
-               if (!empty($data['elements']['parent'])) {
-                       $sql = "SELECT  pageID
-                               FROM    wcf".WCF_N."_".$this->tableName."
-                               WHERE   identifier = ?";
-                       $statement = WCF::getDB()->prepareStatement($sql, 1);
-                       $statement->execute([$data['elements']['parent']]);
-                       $row = $statement->fetchSingleRow();
-                       if ($row === false) {
-                               throw new SystemException("Unknown parent page '" . $data['elements']['parent'] . "' for page identifier '" . $data['attributes']['identifier'] . "'");
-                       }
-                       
-                       $parentPageID = $row['pageID'];
-               }
-               
-               // validate page type
-               $controller = '';
-               $handler = '';
-               $controllerCustomURL = '';
-               $identifier = $data['attributes']['identifier'];
-               $isMultilingual = 0;
-               switch ($pageType) {
-                       case 'system':
-                               if (empty($data['elements']['controller'])) {
-                                       throw new SystemException("Missing required element 'controller' for 'system'-type page '{$identifier}'");
-                               }
-                               $controller = $data['elements']['controller'];
-                               
-                               if (!empty($data['elements']['handler'])) {
-                                       $handler = $data['elements']['handler'];
-                               }
-                               
-                               // @deprecated
-                               if (!empty($data['elements']['controllerCustomURL'])) {
-                                       $controllerCustomURL = $data['elements']['controllerCustomURL'];
-                                       if ($controllerCustomURL && !RouteHandler::isValidCustomUrl($controllerCustomURL)) {
-                                               throw new SystemException("Invalid custom url for page identifier '" . $data['attributes']['identifier'] . "'");
-                                       }
-                               }
-                               
-                               break;
-                       
-                       case 'html':
-                       case 'text':
-                       case 'tpl':
-                               if (empty($data['elements']['content'])) {
-                                       throw new SystemException("Missing required 'content' element(s) for page '{$identifier}'");
-                               }
-                               
-                               if (count($data['elements']['content']) === 1) {
-                                       if (!isset($data['elements']['content'][''])) {
-                                               throw new SystemException("Expected one 'content' element without a 'language' attribute for page '{$identifier}'");
-                                       }
-                               }
-                               else {
-                                       $isMultilingual = 1;
-                                       if (isset($data['elements']['content'][''])) {
-                                               throw new SystemException("Cannot mix 'content' elements with and without 'language' attribute for page '{$identifier}'");
-                                       }
-                               }
-                               
-                               break;
-                       
-                       default:
-                               throw new SystemException("Unknown type '{$pageType}' for page '{$identifier}");
-                               break;
-               }
-               
-               // get application package id
-               $applicationPackageID = 1;
-               if ($this->installation->getPackage()->isApplication) {
-                       $applicationPackageID = $this->installation->getPackageID();
-               }
-               if (!empty($data['elements']['application'])) {
-                       $application = PackageCache::getInstance()->getPackageByIdentifier($data['elements']['application']);
-                       if ($application === null || !$application->isApplication) {
-                               throw new SystemException("Unknown application '".$data['elements']['application']."' for page '{$identifier}");
-                       }
-                       $applicationPackageID = $application->packageID;
-               }
-               
-               return [
-                       'pageType' => $pageType,
-                       'content' => (!empty($data['elements']['content'])) ? $data['elements']['content'] : [],
-                       'controller' => $controller,
-                       'handler' => $handler,
-                       'controllerCustomURL' => $controllerCustomURL,
-                       'identifier' => $identifier,
-                       'isMultilingual' => $isMultilingual,
-                       'lastUpdateTime' => TIME_NOW,
-                       'name' => $name,
-                       'originIsSystem' => 1,
-                       'parentPageID' => $parentPageID,
-                       'applicationPackageID' => $applicationPackageID,
-                       'requireObjectID' => (!empty($data['elements']['requireObjectID'])) ? 1 : 0,
-                       'options' => isset($data['elements']['options']) ? $data['elements']['options'] : '',
-                       'permissions' => isset($data['elements']['permissions']) ? $data['elements']['permissions'] : '',
-                       'hasFixedParent' => ($pageType == 'system' && !empty($data['elements']['hasFixedParent'])) ? 1 : 0,
-                       'cssClassName' => isset($data['elements']['cssClassName']) ? $data['elements']['cssClassName'] : '',
-                       'availableDuringOfflineMode' => (!empty($data['elements']['availableDuringOfflineMode'])) ? 1 : 0,
-                       'allowSpidersToIndex' => (!empty($data['elements']['allowSpidersToIndex'])) ? 1 : 0,
-                       'excludeFromLandingPage' => (!empty($data['elements']['excludeFromLandingPage'])) ? 1 : 0
-               ];
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       protected function findExistingItem(array $data) {
-               $sql = "SELECT  *
-                       FROM    wcf".WCF_N."_".$this->tableName."
-                       WHERE   identifier = ?
-                               AND packageID = ?";
-               $parameters = [
-                       $data['identifier'],
-                       $this->installation->getPackageID()
-               ];
-               
-               return [
-                       'sql' => $sql,
-                       'parameters' => $parameters
-               ];
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       protected function import(array $row, array $data) {
-               // extract content
-               $content = $data['content'];
-               unset($data['content']);
-               
-               /** @var Page $page */
-               if (!empty($row)) {
-                       // allow update of `controller`, `handler` and `excludeFromLandingPage`
-                       // only, prevents user modifications form being overwritten
-                       if (!empty($data['controller'])) {
-                               $allowSpidersToIndex = $row['allowSpidersToIndex'] ?? 0;
-                               if ($allowSpidersToIndex == 2) {
-                                       // The value `2` resolves to be true-ish, eventually resulting in the same behavior
-                                       // when setting it to `1`. This value is special to the 3.0 -> 3.1 upgrade, because
-                                       // it force-enables the visibility, while also being some sort of indicator for non-
-                                       // user-modified values. The page edit form will set it to either `1` or `0`, there-
-                                       // fore `2` means that we can safely update the value w/o breaking the user's choice. 
-                                       $allowSpidersToIndex = $data['allowSpidersToIndex'];
-                               }
-                               
-                               $page = parent::import($row, [
-                                       'controller' => $data['controller'],
-                                       'handler' => $data['handler'] ?? '',
-                                       'options' => $data['options'] ?? '',
-                                       'permissions' => $data['permissions'] ?? '',
-                                       'excludeFromLandingPage' => $data['excludeFromLandingPage'] ?? 0,
-                                       'allowSpidersToIndex' => $allowSpidersToIndex,
-                                       'requireObjectID' => $data['requireObjectID'],
-                               ]);
-                       }
-                       else {
-                               $baseClass = call_user_func([$this->className, 'getBaseClass']);
-                               $page = new $baseClass(null, $row);
-                       }
-               }
-               else {
-                       // import
-                       $page = parent::import($row, $data);
-               }
-               
-               // store content for later import
-               $this->pages[$page->pageID] = $page;
-               $this->content[$page->pageID] = $content;
-               
-               return $page;
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       protected function postImport() {
-               if (!empty($this->content)) {
-                       $sql = "SELECT  COUNT(*) AS count
-                               FROM    wcf".WCF_N."_page_content
-                               WHERE   pageID = ?
-                                       AND languageID IS NULL";
-                       $statement = WCF::getDB()->prepareStatement($sql);
-                       
-                       $sql = "INSERT IGNORE INTO      wcf".WCF_N."_page_content
-                                                       (pageID, languageID, title, content, metaDescription, metaKeywords, customURL)
-                               VALUES                  (?, ?, ?, ?, ?, ?, ?)";
-                       $insertStatement = WCF::getDB()->prepareStatement($sql);
-                       
-                       WCF::getDB()->beginTransaction();
-                       foreach ($this->content as $pageID => $contentData) {
-                               foreach ($contentData as $languageCode => $content) {
-                                       $languageID = null;
-                                       if ($languageCode != '') {
-                                               $language = LanguageFactory::getInstance()->getLanguageByCode($languageCode);
-                                               if ($language === null) continue;
-                                               
-                                               $languageID = $language->languageID;
-                                       }
-                                       
-                                       if ($languageID === null) {
-                                               $statement->execute([$pageID]);
-                                               if ($statement->fetchColumn()) continue;
-                                       }
-                                       
-                                       $insertStatement->execute([
-                                               $pageID,
-                                               $languageID,
-                                               $content['title'],
-                                               $content['content'],
-                                               $content['metaDescription'],
-                                               $content['metaKeywords'],
-                                               $content['customURL']
-                                       ]);
-                                       
-                                       // generate template if page's type is 'tpl'
-                                       $page = new Page($pageID);
-                                       if ($page->pageType == 'tpl') {
-                                               (new PageEditor($page))->updateTemplate($languageID, $content['content']);
-                                       }
-                               }
-                       }
-                       WCF::getDB()->commitTransaction();
-                       
-                       // create search index tables
-                       SearchIndexManager::getInstance()->createSearchIndices();
-                       
-                       // update search index
-                       foreach ($this->pages as $pageID => $page) {
-                               if ($page->pageType == 'text' || $page->pageType == 'html') {
-                                       foreach ($page->getPageContents() as $languageID => $pageContent) {
-                                               SearchIndexManager::getInstance()->set(
-                                                       'com.woltlab.wcf.page',
-                                                       $pageContent->pageContentID,
-                                                       $pageContent->content,
-                                                       $pageContent->title,
-                                                       0,
-                                                       null,
-                                                       '',
-                                                       $languageID ?: null
-                                               );
-                                       }
-                               }
-                       }
-               }
-       }
-       
-       /**
-        * @inheritDoc
-        * @since       3.1
-        */
-       public static function getSyncDependencies() {
-               return ['language'];
-       }
-       
-       /**
-        * @inheritDoc
-        * @since       5.2
-        */
-       protected function addFormFields(IFormDocument $form) {
-               $tabContainter = TabMenuFormContainer::create('tabMenu');
-               $form->appendChild($tabContainter);
-               
-               $dataTab = TabFormContainer::create('dataTab')
-                       ->label('wcf.global.form.data');
-               $tabContainter->appendChild($dataTab);
-               $dataContainer = FormContainer::create('dataTabData');
-               $dataTab->appendChild($dataContainer);
-               
-               $contentTab = TabFormContainer::create('contentTab')
-                       ->label('wcf.acp.pip.page.content');
-               $tabContainter->appendChild($contentTab);
-               $contentContainer = FormContainer::create('contentTabContent');
-               $contentTab->appendChild($contentContainer);
-               
-               $dataContainer->appendChildren([
-                       TextFormField::create('identifier')
-                               ->label('wcf.acp.pip.page.identifier')
-                               ->description('wcf.acp.pip.page.identifier.description')
-                               ->required()
-                               ->addValidator(FormFieldValidatorUtil::getDotSeparatedStringValidator(
-                                       'wcf.acp.pip.page.identifier',
-                                       4
-                               ))
-                               ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField $formField) {
-                                       if (
-                                               $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE ||
-                                               $this->editedEntry->getAttribute('identifier') !== $formField->getValue()
-                                       ) {
-                                               $pageList = new PageList();
-                                               $pageList->getConditionBuilder()->add('identifier = ?', [$formField->getValue()]);
-                                               
-                                               if ($pageList->countObjects() > 0) {
-                                                       $formField->addValidationError(
-                                                               new FormFieldValidationError(
-                                                                       'notUnique',
-                                                                       'wcf.acp.pip.page.identifier.error.notUnique'
-                                                               )
-                                                       );
-                                               }
-                                       }
-                               })),
-                       
-                       RadioButtonFormField::create('pageType')
-                               ->label('wcf.acp.pip.page.pageType')
-                               ->description('wcf.acp.pip.page.pageType.description')
-                               ->options(array_combine(Page::$availablePageTypes, Page::$availablePageTypes))
-                               ->addClass('floated'),
-                       
-                       TextFormField::create('name')
-                               ->label('wcf.acp.pip.page.name')
-                               ->description('wcf.acp.pip.page.name.description')
-                               ->required()
-                               ->i18n()
-                               ->i18nRequired()
-                               ->languageItemPattern('__NONE__'),
-                       
-                       ClassNameFormField::create('controller')
-                               ->label('wcf.acp.pip.page.controller')
-                               ->implementedInterface(IPage::class)
-                               ->required(),
-                       
-                       ClassNameFormField::create('handler')
-                               ->label('wcf.acp.pip.page.handler')
-                               ->implementedInterface(IMenuPageHandler::class),
-                       
-                       BooleanFormField::create('requireObjectID')
-                               ->label('wcf.acp.pip.page.requireObjectID')
-                               ->description('wcf.acp.pip.page.requireObjectID.description'),
-                       
-                       SingleSelectionFormField::create('parent')
-                               ->label('wcf.acp.pip.page.parent')
-                               ->required()
-                               ->filterable()
-                               ->options(function() {
-                                       $pageNodeList = (new PageNodeTree())->getNodeList();
-                                       
-                                       $nestedOptions = [[
-                                               'depth' => 0,
-                                               'label' => 'wcf.global.noSelection',
-                                               'value' => ''
-                                       ]];
-                                       
-                                       $packageIDs = array_merge(
-                                               [$this->installation->getPackage()->packageID],
-                                               array_keys($this->installation->getPackage()->getAllRequiredPackages())
-                                       );
-                                       
-                                       /** @var PageNode $pageNode */
-                                       foreach ($pageNodeList as $pageNode) {
-                                               if (in_array($pageNode->packageID, $packageIDs)) {
-                                                       $nestedOptions[] = [
-                                                               'depth' => $pageNode->getDepth() - 1,
-                                                               'label' => $pageNode->name,
-                                                               'value' => $pageNode->identifier
-                                                       ];
-                                               }
-                                       }
-                                       
-                                       return $nestedOptions;
-                               }, true)
-                               ->addValidator(new FormFieldValidator('selfParent', function(SingleSelectionFormField $formField) {
-                                       /** @var TextFormField $identifier */
-                                       $identifier = $formField->getDocument()->getNodeById('identifier');
-                                       
-                                       if ($identifier->getSaveValue() === $formField->getValue()) {
-                                               $formField->addValidationError(
-                                                       new FormFieldValidationError(
-                                                               'selfParent',
-                                                               'wcf.acp.pip.page.parent.error.selfParent'
-                                                       )
-                                               );
-                                       }
-                               })),
-                       
-                       BooleanFormField::create('hasFixedParent')
-                               ->label('wcf.acp.pip.page.hasFixedParent')
-                               ->description('wcf.acp.pip.page.hasFixedParent.description'),
-                       
-                       OptionFormField::create()
-                               ->description('wcf.acp.pip.page.options.description')
-                               ->packageIDs(array_merge(
-                                       [$this->installation->getPackage()->packageID],
-                                       array_keys($this->installation->getPackage()->getAllRequiredPackages())
-                               )),
-                       
-                       UserGroupOptionFormField::create()
-                               ->description('wcf.acp.pip.page.permissions.description')
-                               ->packageIDs(array_merge(
-                                       [$this->installation->getPackage()->packageID],
-                                       array_keys($this->installation->getPackage()->getAllRequiredPackages())
-                               )),
-                       
-                       ItemListFormField::create('cssClassName')
-                               ->label('wcf.acp.pip.page.cssClassName')
-                               ->description('wcf.acp.pip.page.cssClassName.description'),
-                       
-                       BooleanFormField::create('allowSpidersToIndex')
-                               ->label('wcf.acp.pip.page.allowSpidersToIndex'),
-                       
-                       BooleanFormField::create('excludeFromLandingPage')
-                               ->label('wcf.acp.pip.page.excludeFromLandingPage'),
-                       
-                       BooleanFormField::create('availableDuringOfflineMode')
-                               ->label('wcf.acp.pip.page.availableDuringOfflineMode')
-               ]);
-               
-               $contentContainer->appendChildren([
-                       TitleFormField::create('contentTitle')
-                               ->objectProperty('title')
-                               ->label('wcf.acp.pip.page.contentTitle')
-                               ->i18n()
-                               ->i18nRequired()
-                               ->languageItemPattern('__NONE__'),
-                       
-                       MultilineTextFormField::create('contentContent')
-                               ->objectProperty('content')
-                               ->label('wcf.acp.pip.page.contentContent')
-                               ->i18n()
-                               ->i18nRequired()
-                               ->languageItemPattern('__NONE__'),
-                       
-                       TextFormField::create('contentCustomURL')
-                               ->objectProperty('customURL')
-                               ->label('wcf.acp.pip.page.contentCustomURL')
-                               ->i18n()
-                               ->i18nRequired()
-                               ->languageItemPattern('__NONE__'),
-                       
-                       TextFormField::create('contentMetaDescription')
-                               ->objectProperty('metaDescription')
-                               ->label('wcf.acp.pip.page.contentMetaDescription')
-                               ->i18n()
-                               ->i18nRequired()
-                               ->languageItemPattern('__NONE__'),
-                       
-                       TextFormField::create('contentMetaKeywords')
-                               ->objectProperty('metaKeywords')
-                               ->label('wcf.acp.pip.page.contentMetaKeywords')
-                               ->i18n()
-                               ->i18nRequired()
-                               ->languageItemPattern('__NONE__'),
-               ]);
-               
-               // dependencies
-               
-               /** @var RadioButtonFormField $pageType */
-               $pageType = $form->getNodeById('pageType');
-               foreach (['controller', 'handler', 'requireObjectID'] as $systemElement) {
-                       $form->getNodeById($systemElement)->addDependency(
-                               ValueFormFieldDependency::create('pageType')
-                                       ->field($pageType)
-                                       ->values(['system'])
-                       );
-               }
-               
-               foreach (['contentContent', 'contentCustomURL', 'contentMetaDescription', 'contentMetaKeywords'] as $nonSystemElement) {
-                       $form->getNodeById($nonSystemElement)->addDependency(
-                               ValueFormFieldDependency::create('pageType')
-                                       ->field($pageType)
-                                       ->values(['system'])
-                                       ->negate()
-                       );
-               }
-       }
-       
-       /**
-        * @inheritDoc
-        * @since       5.2
-        */
-       protected function fetchElementData(\DOMElement $element, $saveData) {
-               $data = [
-                       'identifier' => $element->getAttribute('identifier'),
-                       'originIsSystem' => 1,
-                       'packageID' => $this->installation->getPackageID(),
-                       'pageType' => $element->getElementsByTagName('pageType')->item(0)->nodeValue,
-                       'name' => [],
-                       'title' => [],
-                       'content' => [],
-                       'customURL' => [],
-                       'metaDescription' => [],
-                       'metaKeywords' => []
-               ];
-               
-               /** @var \DOMElement $name */
-               foreach ($element->getElementsByTagName('name') as $name) {
-                       $data['name'][LanguageFactory::getInstance()->getLanguageByCode($name->getAttribute('language'))->languageID] = $name->nodeValue;
-               }
-               
-               $optionalElements = [
-                       'controller', 'handler', 'hasFixedParent',
-                       'parent', 'options', 'permissions', 'cssClassName', 'allowSpidersToIndex',
-                       'excludeFromLandingPage', 'availableDuringOfflineMode', 'requireObjectID'
-               ];
-               
-               $zeroDefaultOptions = [
-                       'hasFixedParent',
-                       'allowSpidersToIndex',
-                       'excludeFromLandingPage',
-                       'availableDuringOfflineMode',
-                       'requireObjectID'
-               ];
-               
-               foreach ($optionalElements as $optionalElementName) {
-                       $optionalElement = $element->getElementsByTagName($optionalElementName)->item(0);
-                       if ($optionalElement !== null) {
-                               $data[$optionalElementName] = $optionalElement->nodeValue;
-                       }
-                       else if ($saveData) {
-                               if (in_array($optionalElementName, $zeroDefaultOptions)) {
-                                       $data[$optionalElementName] = 0;
-                               }
-                               else {
-                                       $data[$optionalElementName] = '';
-                               }
-                       }
-               }
-               
-               $readData = function($languageID, \DOMElement $content) use (&$data, $saveData) {
-                       foreach (['title', 'content', 'customURL', 'metaDescription', 'metaKeywords'] as $contentElementName) {
-                               $contentElement = $content->getElementsByTagName($contentElementName)->item(0);
-                               if (!isset($data[$contentElementName])) {
-                                       $data[$contentElementName] = [];
-                               }
-                               
-                               if ($contentElement) {
-                                       $data[$contentElementName][$languageID] = $contentElement->nodeValue;
-                               }
-                               else if ($saveData) {
-                                       $data[$contentElementName][$languageID] = '';
-                               }
-                       }
-               };
-               
-               /** @var \DOMElement $content */
-               foreach ($element->getElementsByTagName('content') as $content) {
-                       $languageCode = $content->getAttribute('language');
-                       if ($languageCode === '') {
-                               foreach (LanguageFactory::getInstance()->getLanguages() as $language) {
-                                       $readData($language->languageID, $content);
-                               }
-                       }
-                       else {
-                               $readData(
-                                       LanguageFactory::getInstance()->getLanguageByCode($languageCode)->languageID,
-                                       $content
-                               );
-                       }
-               }
-               
-               if ($saveData) {
-                       if ($this->editedEntry !== null) {
-                               unset($data['name']);
-                       }
-                       else {
-                               $titles = [];
-                               foreach ($data['name'] as $languageID => $title) {
-                                       $titles[LanguageFactory::getInstance()->getLanguage($languageID)->languageCode] = $title;
-                               }
-                               
-                               if (isset($data['name'][LanguageFactory::getInstance()->getDefaultLanguage()->languageID])) {
-                                       // use the default language
-                                       $data['name'] = $data['name'][LanguageFactory::getInstance()->getDefaultLanguage()->languageID];
-                               }
-                               else {
-                                       $english = LanguageFactory::getInstance()->getLanguageByCode('en');
-                                       if ($english !== null && isset($data['name'][$english->languageID])) {
-                                               $data['name'] = $data['name'][$english->languageID];
-                                       }
-                                       else {
-                                               $data['name'] = reset($data['name']);
-                                       }
-                               }
-                       }
-                       
-                       $content = [];
-                       
-                       foreach (['title', 'content', 'customURL', 'metaDescription', 'metaKeywords'] as $contentProperty) {
-                               if (!empty($data[$contentProperty])) {
-                                       foreach ($data[$contentProperty] as $languageID => $value) {
-                                               $languageCode = LanguageFactory::getInstance()->getLanguage($languageID)->languageCode;
-                                               
-                                               if (!isset($content[$languageCode])) {
-                                                       $content[$languageCode] = [];
-                                               }
-                                               
-                                               $content[$languageCode][$contentProperty] = $value;
-                                       }
-                               }
-                               
-                               unset($data[$contentProperty]);
-                       }
-                       
-                       foreach ($content as $languageCode => $values) {
-                               foreach (['title', 'content', 'customURL', 'metaDescription', 'metaKeywords'] as $contentProperty) {
-                                       if (!isset($values[$contentProperty])) {
-                                               $content[$languageCode][$contentProperty] = '';
-                                       }
-                               }
-                       }
-                       
-                       $data['content'] = $content;
-                       
-                       if (isset($data['parent'])) {
-                               $parent = $data['parent'];
-                               unset($data['parent']);
-                               
-                               if (!empty($parent)) {
-                                       $data['parentPageID'] = Page::getPageByIdentifier($parent)->pageID;
-                               }
-                       }
-               }
-               
-               return $data;
-       }
-       
-       /**
-        * @inheritDoc
-        * @since       5.2
-        */
-       public function getElementIdentifier(\DOMElement $element) {
-               return $element->getAttribute('identifier');
-       }
-       
-       /**
-        * @inheritDoc
-        * @since       5.2
-        */
-       protected function setEntryListKeys(IDevtoolsPipEntryList $entryList) {
-               $entryList->setKeys([
-                       'identifier' => 'wcf.acp.pip.page.identifier',
-                       'pageType' => 'wcf.acp.pip.page.pageType'
-               ]);
-       }
-       
-       /**
-        * @inheritDoc
-        * @since       5.2
-        */
-       protected function prepareXmlElement(\DOMDocument $document, IFormDocument $form) {
-               $formData = $form->getData();
-               $data = $formData['data'];
-               
-               $page = $document->createElement($this->tagName);
-               $page->setAttribute('identifier', $data['identifier']);
-               
-               $page->appendChild($document->createElement('pageType', $data['pageType']));
-               
-               $this->appendElementChildren(
-                       $page,
-                       ['controller' => '',],
-                       $form
-               );
-               
-               foreach ($formData['name_i18n'] as $languageID => $name) {
-                       $name = $document->createElement('name', $this->getAutoCdataValue($name));
-                       $name->setAttribute('language', LanguageFactory::getInstance()->getLanguage($languageID)->languageCode);
-                       
-                       $page->appendChild($name);
-               }
-               
-               $this->appendElementChildren(
-                       $page,
-                       [
-                               'handler' => '',
-                               'hasFixedParent' => 0,
-                               'parent' => '',
-                               'options' => '',
-                               'permissions' => '',
-                               'cssClassName' => '',
-                               'allowSpidersToIndex' => 0,
-                               'excludeFromLandingPage' => 0,
-                               'availableDuringOfflineMode' => 0,
-                               'requireObjectID' => 0
-                       ],
-                       $form
-               );
-               
-               $languages = LanguageFactory::getInstance()->getLanguages();
-               
-               // sort languages by language code but keep English first
-               uasort($languages, function(Language $language1, Language $language2) {
-                       if ($language1->languageCode === 'en') {
-                               return -1;
-                       }
-                       else if ($language2->languageCode === 'en') {
-                               return 1;
-                       }
-                       
-                       return $language1->languageCode <=> $language2->languageCode;
-               });
-               
-               foreach ($languages as $language) {
-                       $content = null;
-                       
-                       foreach (['title', 'content', 'customURL', 'metaDescription', 'metaKeywords'] as $property) {
-                               if (!empty($formData[$property . '_i18n'][$language->languageID])) {
-                                       if ($content === null) {
-                                               $content = $document->createElement('content');
-                                               $content->setAttribute('language', $language->languageCode);
-                                               
-                                               $page->appendChild($content);
-                                       }
-                                       
-                                       if ($property === 'content') {
-                                               $contentContent = $document->createElement('content');
-                                               $contentContent->appendChild(
-                                                       $document->createCDATASection(
-                                                               StringUtil::escapeCDATA(StringUtil::unifyNewlines(
-                                                                       $formData[$property . '_i18n'][$language->languageID]
-                                                               ))
-                                                       )
-                                               );
-                                               
-                                               $content->appendChild($contentContent);
-                                       }
-                                       else {
-                                               $content->appendChild(
-                                                       $document->createElement(
-                                                               $property,
-                                                               $formData[$property . '_i18n'][$language->languageID]
-                                                       )
-                                               );
-                                       }
-                               }
-                       }
-               }
-               
-               return $page;
-       }
+class PagePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements
+    IGuiPackageInstallationPlugin
+{
+    use TXmlGuiPackageInstallationPlugin;
+
+    /**
+     * @inheritDoc
+     */
+    public $className = PageEditor::class;
+
+    /**
+     * page content
+     * @var mixed[]
+     */
+    protected $content = [];
+
+    /**
+     * pages objects
+     * @var Page[]
+     */
+    protected $pages = [];
+
+    /**
+     * @inheritDoc
+     */
+    public $tagName = 'page';
+
+    /**
+     * @inheritDoc
+     */
+    protected function handleDelete(array $items)
+    {
+        $pages = [];
+        foreach ($items as $item) {
+            $page = Page::getPageByIdentifier($item['attributes']['identifier']);
+            if ($page !== null && $page->pageID && $page->packageID == $this->installation->getPackageID()) {
+                $pages[] = $page;
+            }
+        }
+
+        if (!empty($pages)) {
+            $pageAction = new PageAction($pages, 'delete');
+            $pageAction->executeAction();
+        }
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function getElement(\DOMXPath $xpath, array &$elements, \DOMElement $element)
+    {
+        $nodeValue = $element->nodeValue;
+
+        // read content
+        if ($element->tagName === 'content') {
+            if (!isset($elements['content'])) {
+                $elements['content'] = [];
+            }
+
+            $children = [];
+            /** @var \DOMElement $child */
+            foreach ($xpath->query('child::*', $element) as $child) {
+                $children[$child->tagName] = $child->nodeValue;
+            }
+
+            $elements[$element->tagName][$element->getAttribute('language')] = $children;
+        } elseif ($element->tagName === 'name') {
+            // <name> can occur multiple times using the `language` attribute
+            if (!isset($elements['name'])) {
+                $elements['name'] = [];
+            }
+
+            $elements['name'][$element->getAttribute('language')] = $element->nodeValue;
+        } else {
+            $elements[$element->tagName] = $nodeValue;
+        }
+    }
+
+    /**
+     * @inheritDoc
+     * @throws  SystemException
+     */
+    protected function prepareImport(array $data)
+    {
+        $pageType = $data['elements']['pageType'];
+
+        if (!empty($data['elements']['content'])) {
+            $content = [];
+            foreach ($data['elements']['content'] as $language => $contentData) {
+                if ($pageType != 'system' && !RouteHandler::isValidCustomUrl($contentData['customURL'])) {
+                    throw new SystemException("Invalid custom url for page content '" . $language . "', page identifier '" . $data['attributes']['identifier'] . "'");
+                }
+
+                $content[$language] = [
+                    'content' => (!empty($contentData['content'])) ? StringUtil::trim($contentData['content']) : '',
+                    'customURL' => (!empty($contentData['customURL'])) ? StringUtil::trim($contentData['customURL']) : '',
+                    'metaDescription' => (!empty($contentData['metaDescription'])) ? StringUtil::trim($contentData['metaDescription']) : '',
+                    'title' => (!empty($contentData['title'])) ? StringUtil::trim($contentData['title']) : '',
+                ];
+            }
+
+            $data['elements']['content'] = $content;
+        }
+
+        // pick the display name by choosing the default language, or 'en' or '' (empty string)
+        $defaultLanguageCode = LanguageFactory::getInstance()->getDefaultLanguage()->getFixedLanguageCode();
+        if (isset($data['elements']['name'][$defaultLanguageCode])) {
+            // use the default language
+            $name = $data['elements']['name'][$defaultLanguageCode];
+        } elseif (isset($data['elements']['name']['en'])) {
+            // use the value for English
+            $name = $data['elements']['name']['en'];
+        } elseif (isset($data['elements']['name'][''])) {
+            // fallback to the display name without/empty language attribute
+            $name = $data['elements']['name'][''];
+        } else {
+            // use whichever value is present, regardless of the language
+            $name = \reset($data['elements']['name']);
+        }
+
+        $parentPageID = null;
+        if (!empty($data['elements']['parent'])) {
+            $sql = "SELECT  pageID
+                    FROM    wcf" . WCF_N . "_" . $this->tableName . "
+                    WHERE   identifier = ?";
+            $statement = WCF::getDB()->prepareStatement($sql, 1);
+            $statement->execute([$data['elements']['parent']]);
+            $row = $statement->fetchSingleRow();
+            if ($row === false) {
+                throw new SystemException("Unknown parent page '" . $data['elements']['parent'] . "' for page identifier '" . $data['attributes']['identifier'] . "'");
+            }
+
+            $parentPageID = $row['pageID'];
+        }
+
+        // validate page type
+        $controller = '';
+        $handler = '';
+        $controllerCustomURL = '';
+        $identifier = $data['attributes']['identifier'];
+        $isMultilingual = 0;
+        switch ($pageType) {
+            case 'system':
+                if (empty($data['elements']['controller'])) {
+                    throw new SystemException("Missing required element 'controller' for 'system'-type page '{$identifier}'");
+                }
+                $controller = $data['elements']['controller'];
+
+                if (!empty($data['elements']['handler'])) {
+                    $handler = $data['elements']['handler'];
+                }
+
+                // @deprecated
+                if (!empty($data['elements']['controllerCustomURL'])) {
+                    $controllerCustomURL = $data['elements']['controllerCustomURL'];
+                    if ($controllerCustomURL && !RouteHandler::isValidCustomUrl($controllerCustomURL)) {
+                        throw new SystemException("Invalid custom url for page identifier '" . $data['attributes']['identifier'] . "'");
+                    }
+                }
+
+                break;
+
+            case 'html':
+            case 'text':
+            case 'tpl':
+                if (empty($data['elements']['content'])) {
+                    throw new SystemException("Missing required 'content' element(s) for page '{$identifier}'");
+                }
+
+                if (\count($data['elements']['content']) === 1) {
+                    if (!isset($data['elements']['content'][''])) {
+                        throw new SystemException("Expected one 'content' element without a 'language' attribute for page '{$identifier}'");
+                    }
+                } else {
+                    $isMultilingual = 1;
+                    if (isset($data['elements']['content'][''])) {
+                        throw new SystemException("Cannot mix 'content' elements with and without 'language' attribute for page '{$identifier}'");
+                    }
+                }
+
+                break;
+
+            default:
+                throw new SystemException("Unknown type '{$pageType}' for page '{$identifier}");
+                break;
+        }
+
+        // get application package id
+        $applicationPackageID = 1;
+        if ($this->installation->getPackage()->isApplication) {
+            $applicationPackageID = $this->installation->getPackageID();
+        }
+        if (!empty($data['elements']['application'])) {
+            $application = PackageCache::getInstance()->getPackageByIdentifier($data['elements']['application']);
+            if ($application === null || !$application->isApplication) {
+                throw new SystemException("Unknown application '" . $data['elements']['application'] . "' for page '{$identifier}");
+            }
+            $applicationPackageID = $application->packageID;
+        }
+
+        return [
+            'pageType' => $pageType,
+            'content' => (!empty($data['elements']['content'])) ? $data['elements']['content'] : [],
+            'controller' => $controller,
+            'handler' => $handler,
+            'controllerCustomURL' => $controllerCustomURL,
+            'identifier' => $identifier,
+            'isMultilingual' => $isMultilingual,
+            'lastUpdateTime' => TIME_NOW,
+            'name' => $name,
+            'originIsSystem' => 1,
+            'parentPageID' => $parentPageID,
+            'applicationPackageID' => $applicationPackageID,
+            'requireObjectID' => (!empty($data['elements']['requireObjectID'])) ? 1 : 0,
+            'options' => $data['elements']['options'] ?? '',
+            'permissions' => $data['elements']['permissions'] ?? '',
+            'hasFixedParent' => ($pageType == 'system' && !empty($data['elements']['hasFixedParent'])) ? 1 : 0,
+            'cssClassName' => $data['elements']['cssClassName'] ?? '',
+            'availableDuringOfflineMode' => (!empty($data['elements']['availableDuringOfflineMode'])) ? 1 : 0,
+            'allowSpidersToIndex' => (!empty($data['elements']['allowSpidersToIndex'])) ? 1 : 0,
+            'excludeFromLandingPage' => (!empty($data['elements']['excludeFromLandingPage'])) ? 1 : 0,
+        ];
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function findExistingItem(array $data)
+    {
+        $sql = "SELECT  *
+                FROM    wcf" . WCF_N . "_" . $this->tableName . "
+                WHERE   identifier = ?
+                    AND packageID = ?";
+        $parameters = [
+            $data['identifier'],
+            $this->installation->getPackageID(),
+        ];
+
+        return [
+            'sql' => $sql,
+            'parameters' => $parameters,
+        ];
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function import(array $row, array $data)
+    {
+        // extract content
+        $content = $data['content'];
+        unset($data['content']);
+
+        /** @var Page $page */
+        if (!empty($row)) {
+            // allow update of `controller`, `handler` and `excludeFromLandingPage`
+            // only, prevents user modifications form being overwritten
+            if (!empty($data['controller'])) {
+                $allowSpidersToIndex = $row['allowSpidersToIndex'] ?? 0;
+                if ($allowSpidersToIndex == 2) {
+                    // The value `2` resolves to be true-ish, eventually resulting in the same behavior
+                    // when setting it to `1`. This value is special to the 3.0 -> 3.1 upgrade, because
+                    // it force-enables the visibility, while also being some sort of indicator for non-
+                    // user-modified values. The page edit form will set it to either `1` or `0`, there-
+                    // fore `2` means that we can safely update the value w/o breaking the user's choice.
+                    $allowSpidersToIndex = $data['allowSpidersToIndex'];
+                }
+
+                $page = parent::import($row, [
+                    'controller' => $data['controller'],
+                    'handler' => $data['handler'] ?? '',
+                    'options' => $data['options'] ?? '',
+                    'permissions' => $data['permissions'] ?? '',
+                    'excludeFromLandingPage' => $data['excludeFromLandingPage'] ?? 0,
+                    'allowSpidersToIndex' => $allowSpidersToIndex,
+                    'requireObjectID' => $data['requireObjectID'],
+                ]);
+            } else {
+                $baseClass = \call_user_func([$this->className, 'getBaseClass']);
+                $page = new $baseClass(null, $row);
+            }
+        } else {
+            // import
+            $page = parent::import($row, $data);
+        }
+
+        // store content for later import
+        $this->pages[$page->pageID] = $page;
+        $this->content[$page->pageID] = $content;
+
+        return $page;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function postImport()
+    {
+        if (!empty($this->content)) {
+            $sql = "SELECT  COUNT(*) AS count
+                    FROM    wcf" . WCF_N . "_page_content
+                    WHERE   pageID = ?
+                        AND languageID IS NULL";
+            $statement = WCF::getDB()->prepareStatement($sql);
+
+            $sql = "INSERT IGNORE INTO  wcf" . WCF_N . "_page_content
+                                        (pageID, languageID, title, content, metaDescription, customURL)
+                    VALUES              (?, ?, ?, ?, ?, ?)";
+            $insertStatement = WCF::getDB()->prepareStatement($sql);
+
+            WCF::getDB()->beginTransaction();
+            foreach ($this->content as $pageID => $contentData) {
+                foreach ($contentData as $languageCode => $content) {
+                    $languageID = null;
+                    if ($languageCode != '') {
+                        $language = LanguageFactory::getInstance()->getLanguageByCode($languageCode);
+                        if ($language === null) {
+                            continue;
+                        }
+
+                        $languageID = $language->languageID;
+                    }
+
+                    if ($languageID === null) {
+                        $statement->execute([$pageID]);
+                        if ($statement->fetchColumn()) {
+                            continue;
+                        }
+                    }
+
+                    $insertStatement->execute([
+                        $pageID,
+                        $languageID,
+                        $content['title'],
+                        $content['content'],
+                        $content['metaDescription'],
+                        $content['customURL'],
+                    ]);
+
+                    // generate template if page's type is 'tpl'
+                    $page = new Page($pageID);
+                    if ($page->pageType == 'tpl') {
+                        (new PageEditor($page))->updateTemplate($languageID, $content['content']);
+                    }
+                }
+            }
+            WCF::getDB()->commitTransaction();
+
+            // create search index tables
+            SearchIndexManager::getInstance()->createSearchIndices();
+
+            // update search index
+            foreach ($this->pages as $page) {
+                if ($page->pageType == 'text' || $page->pageType == 'html') {
+                    foreach ($page->getPageContents() as $languageID => $pageContent) {
+                        SearchIndexManager::getInstance()->set(
+                            'com.woltlab.wcf.page',
+                            $pageContent->pageContentID,
+                            $pageContent->content,
+                            $pageContent->title,
+                            0,
+                            null,
+                            '',
+                            $languageID ?: null
+                        );
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * @inheritDoc
+     * @since   3.1
+     */
+    public static function getSyncDependencies()
+    {
+        return ['language'];
+    }
+
+    /**
+     * @inheritDoc
+     * @since   5.2
+     */
+    protected function addFormFields(IFormDocument $form)
+    {
+        $tabContainter = TabMenuFormContainer::create('tabMenu');
+        $form->appendChild($tabContainter);
+
+        $dataTab = TabFormContainer::create('dataTab')
+            ->label('wcf.global.form.data');
+        $tabContainter->appendChild($dataTab);
+        $dataContainer = FormContainer::create('dataTabData');
+        $dataTab->appendChild($dataContainer);
+
+        $contentTab = TabFormContainer::create('contentTab')
+            ->label('wcf.acp.pip.page.content');
+        $tabContainter->appendChild($contentTab);
+        $contentContainer = FormContainer::create('contentTabContent');
+        $contentTab->appendChild($contentContainer);
+
+        $dataContainer->appendChildren([
+            TextFormField::create('identifier')
+                ->label('wcf.acp.pip.page.identifier')
+                ->description('wcf.acp.pip.page.identifier.description')
+                ->required()
+                ->addValidator(FormFieldValidatorUtil::getDotSeparatedStringValidator(
+                    'wcf.acp.pip.page.identifier',
+                    4
+                ))
+                ->addValidator(new FormFieldValidator('uniqueness', function (TextFormField $formField) {
+                    if (
+                        $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE
+                        || $this->editedEntry->getAttribute('identifier') !== $formField->getValue()
+                    ) {
+                        $pageList = new PageList();
+                        $pageList->getConditionBuilder()->add('identifier = ?', [$formField->getValue()]);
+
+                        if ($pageList->countObjects() > 0) {
+                            $formField->addValidationError(
+                                new FormFieldValidationError(
+                                    'notUnique',
+                                    'wcf.acp.pip.page.identifier.error.notUnique'
+                                )
+                            );
+                        }
+                    }
+                })),
+
+            RadioButtonFormField::create('pageType')
+                ->label('wcf.acp.pip.page.pageType')
+                ->description('wcf.acp.pip.page.pageType.description')
+                ->options(\array_combine(Page::$availablePageTypes, Page::$availablePageTypes))
+                ->addClass('floated'),
+
+            TextFormField::create('name')
+                ->label('wcf.acp.pip.page.name')
+                ->description('wcf.acp.pip.page.name.description')
+                ->required()
+                ->i18n()
+                ->i18nRequired()
+                ->languageItemPattern('__NONE__'),
+
+            ClassNameFormField::create('controller')
+                ->label('wcf.acp.pip.page.controller')
+                ->implementedInterface(IPage::class)
+                ->required(),
+
+            ClassNameFormField::create('handler')
+                ->label('wcf.acp.pip.page.handler')
+                ->implementedInterface(IMenuPageHandler::class),
+
+            BooleanFormField::create('requireObjectID')
+                ->label('wcf.acp.pip.page.requireObjectID')
+                ->description('wcf.acp.pip.page.requireObjectID.description'),
+
+            SingleSelectionFormField::create('parent')
+                ->label('wcf.acp.pip.page.parent')
+                ->required()
+                ->filterable()
+                ->options(function () {
+                    $pageNodeList = (new PageNodeTree())->getNodeList();
+
+                    $nestedOptions = [
+                        [
+                            'depth' => 0,
+                            'label' => 'wcf.global.noSelection',
+                            'value' => '',
+                        ],
+                    ];
+
+                    $packageIDs = \array_merge(
+                        [$this->installation->getPackage()->packageID],
+                        \array_keys($this->installation->getPackage()->getAllRequiredPackages())
+                    );
+
+                    /** @var PageNode $pageNode */
+                    foreach ($pageNodeList as $pageNode) {
+                        if (\in_array($pageNode->packageID, $packageIDs)) {
+                            $nestedOptions[] = [
+                                'depth' => $pageNode->getDepth() - 1,
+                                'label' => $pageNode->name,
+                                'value' => $pageNode->identifier,
+                            ];
+                        }
+                    }
+
+                    return $nestedOptions;
+                }, true)
+                ->addValidator(new FormFieldValidator(
+                    'selfParent',
+                    static function (SingleSelectionFormField $formField) {
+                        /** @var TextFormField $identifier */
+                        $identifier = $formField->getDocument()->getNodeById('identifier');
+
+                        if ($identifier->getSaveValue() === $formField->getValue()) {
+                            $formField->addValidationError(
+                                new FormFieldValidationError(
+                                    'selfParent',
+                                    'wcf.acp.pip.page.parent.error.selfParent'
+                                )
+                            );
+                        }
+                    }
+                )),
+
+            BooleanFormField::create('hasFixedParent')
+                ->label('wcf.acp.pip.page.hasFixedParent')
+                ->description('wcf.acp.pip.page.hasFixedParent.description'),
+
+            OptionFormField::create()
+                ->description('wcf.acp.pip.page.options.description')
+                ->packageIDs(\array_merge(
+                    [$this->installation->getPackage()->packageID],
+                    \array_keys($this->installation->getPackage()->getAllRequiredPackages())
+                )),
+
+            UserGroupOptionFormField::create()
+                ->description('wcf.acp.pip.page.permissions.description')
+                ->packageIDs(\array_merge(
+                    [$this->installation->getPackage()->packageID],
+                    \array_keys($this->installation->getPackage()->getAllRequiredPackages())
+                )),
+
+            ItemListFormField::create('cssClassName')
+                ->label('wcf.acp.pip.page.cssClassName')
+                ->description('wcf.acp.pip.page.cssClassName.description'),
+
+            BooleanFormField::create('allowSpidersToIndex')
+                ->label('wcf.acp.pip.page.allowSpidersToIndex'),
+
+            BooleanFormField::create('excludeFromLandingPage')
+                ->label('wcf.acp.pip.page.excludeFromLandingPage'),
+
+            BooleanFormField::create('availableDuringOfflineMode')
+                ->label('wcf.acp.pip.page.availableDuringOfflineMode'),
+        ]);
+
+        $contentContainer->appendChildren([
+            TitleFormField::create('contentTitle')
+                ->objectProperty('title')
+                ->label('wcf.acp.pip.page.contentTitle')
+                ->i18n()
+                ->i18nRequired()
+                ->languageItemPattern('__NONE__'),
+
+            MultilineTextFormField::create('contentContent')
+                ->objectProperty('content')
+                ->label('wcf.acp.pip.page.contentContent')
+                ->i18n()
+                ->i18nRequired()
+                ->languageItemPattern('__NONE__'),
+
+            TextFormField::create('contentCustomURL')
+                ->objectProperty('customURL')
+                ->label('wcf.acp.pip.page.contentCustomURL')
+                ->i18n()
+                ->i18nRequired()
+                ->languageItemPattern('__NONE__'),
+
+            TextFormField::create('contentMetaDescription')
+                ->objectProperty('metaDescription')
+                ->label('wcf.acp.pip.page.contentMetaDescription')
+                ->i18n()
+                ->i18nRequired()
+                ->languageItemPattern('__NONE__'),
+        ]);
+
+        // dependencies
+
+        /** @var RadioButtonFormField $pageType */
+        $pageType = $form->getNodeById('pageType');
+        foreach (['controller', 'handler', 'requireObjectID'] as $systemElement) {
+            $form->getNodeById($systemElement)->addDependency(
+                ValueFormFieldDependency::create('pageType')
+                    ->field($pageType)
+                    ->values(['system'])
+            );
+        }
+
+        foreach (['contentContent', 'contentCustomURL', 'contentMetaDescription'] as $nonSystemElement) {
+            $form->getNodeById($nonSystemElement)->addDependency(
+                ValueFormFieldDependency::create('pageType')
+                    ->field($pageType)
+                    ->values(['system'])
+                    ->negate()
+            );
+        }
+    }
+
+    /**
+     * @inheritDoc
+     * @since   5.2
+     */
+    protected function fetchElementData(\DOMElement $element, $saveData)
+    {
+        $data = [
+            'identifier' => $element->getAttribute('identifier'),
+            'originIsSystem' => 1,
+            'packageID' => $this->installation->getPackageID(),
+            'pageType' => $element->getElementsByTagName('pageType')->item(0)->nodeValue,
+            'name' => [],
+            'title' => [],
+            'content' => [],
+            'customURL' => [],
+            'metaDescription' => [],
+        ];
+
+        /** @var \DOMElement $name */
+        foreach ($element->getElementsByTagName('name') as $name) {
+            $data['name'][LanguageFactory::getInstance()->getLanguageByCode($name->getAttribute('language'))->languageID] = $name->nodeValue;
+        }
+
+        $optionalElements = [
+            'controller',
+            'handler',
+            'hasFixedParent',
+            'parent',
+            'options',
+            'permissions',
+            'cssClassName',
+            'allowSpidersToIndex',
+            'excludeFromLandingPage',
+            'availableDuringOfflineMode',
+            'requireObjectID',
+        ];
+
+        $zeroDefaultOptions = [
+            'hasFixedParent',
+            'allowSpidersToIndex',
+            'excludeFromLandingPage',
+            'availableDuringOfflineMode',
+            'requireObjectID',
+        ];
+
+        foreach ($optionalElements as $optionalElementName) {
+            $optionalElement = $element->getElementsByTagName($optionalElementName)->item(0);
+            if ($optionalElement !== null) {
+                $data[$optionalElementName] = $optionalElement->nodeValue;
+            } elseif ($saveData) {
+                if (\in_array($optionalElementName, $zeroDefaultOptions)) {
+                    $data[$optionalElementName] = 0;
+                } else {
+                    $data[$optionalElementName] = '';
+                }
+            }
+        }
+
+        $readData = static function ($languageID, \DOMElement $content) use (&$data, $saveData) {
+            foreach (['title', 'content', 'customURL', 'metaDescription'] as $contentElementName) {
+                $contentElement = $content->getElementsByTagName($contentElementName)->item(0);
+                if (!isset($data[$contentElementName])) {
+                    $data[$contentElementName] = [];
+                }
+
+                if ($contentElement) {
+                    $data[$contentElementName][$languageID] = $contentElement->nodeValue;
+                } elseif ($saveData) {
+                    $data[$contentElementName][$languageID] = '';
+                }
+            }
+        };
+
+        /** @var \DOMElement $content */
+        foreach ($element->getElementsByTagName('content') as $content) {
+            $languageCode = $content->getAttribute('language');
+            if ($languageCode === '') {
+                foreach (LanguageFactory::getInstance()->getLanguages() as $language) {
+                    $readData($language->languageID, $content);
+                }
+            } else {
+                $readData(
+                    LanguageFactory::getInstance()->getLanguageByCode($languageCode)->languageID,
+                    $content
+                );
+            }
+        }
+
+        if ($saveData) {
+            if ($this->editedEntry !== null) {
+                unset($data['name']);
+            } else {
+                $titles = [];
+                foreach ($data['name'] as $languageID => $title) {
+                    $titles[LanguageFactory::getInstance()->getLanguage($languageID)->languageCode] = $title;
+                }
+
+                if (isset($data['name'][LanguageFactory::getInstance()->getDefaultLanguage()->languageID])) {
+                    // use the default language
+                    $data['name'] = $data['name'][LanguageFactory::getInstance()->getDefaultLanguage()->languageID];
+                } else {
+                    $english = LanguageFactory::getInstance()->getLanguageByCode('en');
+                    if ($english !== null && isset($data['name'][$english->languageID])) {
+                        $data['name'] = $data['name'][$english->languageID];
+                    } else {
+                        $data['name'] = \reset($data['name']);
+                    }
+                }
+            }
+
+            $content = [];
+
+            foreach (['title', 'content', 'customURL', 'metaDescription'] as $contentProperty) {
+                if (!empty($data[$contentProperty])) {
+                    foreach ($data[$contentProperty] as $languageID => $value) {
+                        $languageCode = LanguageFactory::getInstance()->getLanguage($languageID)->languageCode;
+
+                        if (!isset($content[$languageCode])) {
+                            $content[$languageCode] = [];
+                        }
+
+                        $content[$languageCode][$contentProperty] = $value;
+                    }
+                }
+
+                unset($data[$contentProperty]);
+            }
+
+            foreach ($content as $languageCode => $values) {
+                foreach (['title', 'content', 'customURL', 'metaDescription'] as $contentProperty) {
+                    if (!isset($values[$contentProperty])) {
+                        $content[$languageCode][$contentProperty] = '';
+                    }
+                }
+            }
+
+            $data['content'] = $content;
+
+            if (isset($data['parent'])) {
+                $parent = $data['parent'];
+                unset($data['parent']);
+
+                if (!empty($parent)) {
+                    $data['parentPageID'] = Page::getPageByIdentifier($parent)->pageID;
+                }
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * @inheritDoc
+     * @since   5.2
+     */
+    public function getElementIdentifier(\DOMElement $element)
+    {
+        return $element->getAttribute('identifier');
+    }
+
+    /**
+     * @inheritDoc
+     * @since   5.2
+     */
+    protected function setEntryListKeys(IDevtoolsPipEntryList $entryList)
+    {
+        $entryList->setKeys([
+            'identifier' => 'wcf.acp.pip.page.identifier',
+            'pageType' => 'wcf.acp.pip.page.pageType',
+        ]);
+    }
+
+    /**
+     * @inheritDoc
+     * @since   5.2
+     */
+    protected function prepareXmlElement(\DOMDocument $document, IFormDocument $form)
+    {
+        $formData = $form->getData();
+        $data = $formData['data'];
+
+        $page = $document->createElement($this->tagName);
+        $page->setAttribute('identifier', $data['identifier']);
+
+        $page->appendChild($document->createElement('pageType', $data['pageType']));
+
+        $this->appendElementChildren(
+            $page,
+            ['controller' => ''],
+            $form
+        );
+
+        foreach ($formData['name_i18n'] as $languageID => $name) {
+            $name = $document->createElement('name', $this->getAutoCdataValue($name));
+            $name->setAttribute('language', LanguageFactory::getInstance()->getLanguage($languageID)->languageCode);
+
+            $page->appendChild($name);
+        }
+
+        $this->appendElementChildren(
+            $page,
+            [
+                'handler' => '',
+                'hasFixedParent' => 0,
+                'parent' => '',
+                'options' => '',
+                'permissions' => '',
+                'cssClassName' => '',
+                'allowSpidersToIndex' => 0,
+                'excludeFromLandingPage' => 0,
+                'availableDuringOfflineMode' => 0,
+                'requireObjectID' => 0,
+            ],
+            $form
+        );
+
+        $languages = LanguageFactory::getInstance()->getLanguages();
+
+        // sort languages by language code but keep English first
+        \uasort($languages, static function (Language $language1, Language $language2) {
+            if ($language1->languageCode === 'en') {
+                return -1;
+            } elseif ($language2->languageCode === 'en') {
+                return 1;
+            }
+
+            return $language1->languageCode <=> $language2->languageCode;
+        });
+
+        foreach ($languages as $language) {
+            $content = null;
+
+            foreach (['title', 'content', 'customURL', 'metaDescription'] as $property) {
+                if (!empty($formData[$property . '_i18n'][$language->languageID])) {
+                    if ($content === null) {
+                        $content = $document->createElement('content');
+                        $content->setAttribute('language', $language->languageCode);
+
+                        $page->appendChild($content);
+                    }
+
+                    if ($property === 'content') {
+                        $contentContent = $document->createElement('content');
+                        $contentContent->appendChild(
+                            $document->createCDATASection(
+                                StringUtil::escapeCDATA(StringUtil::unifyNewlines(
+                                    $formData[$property . '_i18n'][$language->languageID]
+                                ))
+                            )
+                        );
+
+                        $content->appendChild($contentContent);
+                    } else {
+                        $content->appendChild(
+                            $document->createElement(
+                                $property,
+                                $formData[$property . '_i18n'][$language->languageID]
+                            )
+                        );
+                    }
+                }
+            }
+        }
+
+        return $page;
+    }
 }