From 2c3107d064ef87f90b62aeaa07835d6c74cc0fbc Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Sat, 7 Jul 2018 17:27:50 +0200 Subject: [PATCH] Add GUI for menuItem package installation plugin See #2545 --- ...enuItemPackageInstallationPlugin.class.php | 439 +++++++++++++++++- wcfsetup/install/lang/en.xml | 18 + 2 files changed, 436 insertions(+), 21 deletions(-) diff --git a/wcfsetup/install/files/lib/system/package/plugin/MenuItemPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/MenuItemPackageInstallationPlugin.class.php index 03adc3abd4..8eb760da83 100644 --- a/wcfsetup/install/files/lib/system/package/plugin/MenuItemPackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/package/plugin/MenuItemPackageInstallationPlugin.class.php @@ -3,20 +3,42 @@ declare(strict_types=1); namespace wcf\system\package\plugin; use wcf\data\menu\item\MenuItem; use wcf\data\menu\item\MenuItemEditor; -use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin; +use wcf\data\menu\item\MenuItemList; +use wcf\data\menu\item\MenuItemNode; +use wcf\data\menu\Menu; +use wcf\data\menu\MenuList; +use wcf\data\page\PageNode; +use wcf\data\page\PageNodeTree; +use wcf\system\devtools\pip\IDevtoolsPipEntryList; +use wcf\system\devtools\pip\IGuiPackageInstallationPlugin; +use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin; use wcf\system\exception\SystemException; +use wcf\system\form\builder\container\FormContainer; +use wcf\system\form\builder\field\dependency\ValueFormFieldDependency; +use wcf\system\form\builder\field\IntegerFormField; +use wcf\system\form\builder\field\IsDisabledFormField; +use wcf\system\form\builder\field\RadioButtonFormField; +use wcf\system\form\builder\field\SingleSelectionFormField; +use wcf\system\form\builder\field\TextFormField; +use wcf\system\form\builder\field\TitleFormField; +use wcf\system\form\builder\field\validation\FormFieldValidationError; +use wcf\system\form\builder\field\validation\FormFieldValidator; +use wcf\system\form\builder\IFormDocument; +use wcf\system\language\LanguageFactory; use wcf\system\WCF; /** * Installs, updates and deletes menu items. * - * @author Alexander Ebert + * @author Alexander Ebert, Matthias Schmidt * @copyright 2001-2018 WoltLab GmbH * @license GNU Lesser General Public License * @package WoltLabSuite\Core\Acp\Package\Plugin * @since 3.0 */ -class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin { +class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin { + use TXmlGuiPackageInstallationPlugin; + /** * @inheritDoc */ @@ -75,17 +97,11 @@ class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPl protected function prepareImport(array $data) { $menuID = null; if (!empty($data['elements']['menu'])) { - $sql = "SELECT menuID - FROM wcf".WCF_N."_menu - WHERE identifier = ?"; - $statement = WCF::getDB()->prepareStatement($sql, 1); - $statement->execute([$data['elements']['menu']]); - $row = $statement->fetchSingleRow(); - if ($row === false) { + $menuID = $this->getMenuID($data['elements']['menu']); + + if ($menuID === null) { throw new SystemException("Unable to find menu '" . $data['elements']['menu'] . "' for menu item '" . $data['attributes']['identifier'] . "'"); } - - $menuID = $row['menuID']; } $parentItemID = null; @@ -116,17 +132,11 @@ class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPl $pageID = null; if (!empty($data['elements']['page'])) { - $sql = "SELECT pageID - FROM wcf".WCF_N."_page - WHERE identifier = ?"; - $statement = WCF::getDB()->prepareStatement($sql, 1); - $statement->execute([$data['elements']['page']]); - $row = $statement->fetchSingleRow(); - if ($row === false) { + $pageID = $this->getPageID($data['elements']['page']); + + if ($pageID === null) { throw new SystemException("Unable to find page '" . $data['elements']['page'] . "' for menu item '" . $data['attributes']['identifier'] . "'"); } - - $pageID = $row['pageID']; } $externalURL = (!empty($data['elements']['externalURL'])) ? $data['elements']['externalURL'] : ''; @@ -150,6 +160,40 @@ class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPl ]; } + /** + * Returns the id of the menu with the given identifier. If no such menu + * exists, `null` is returned. + * + * @param string $identifier + * @return null|int + */ + protected function getMenuID(string $identifier) { + $sql = "SELECT menuID + FROM wcf" . WCF_N . "_menu + WHERE identifier = ?"; + $statement = WCF::getDB()->prepareStatement($sql, 1); + $statement->execute([$identifier]); + + return $statement->fetchSingleColumn(); + } + + /** + * Returns the id of the page with the given identifier. If no such page + * exists, `null` is returned. + * + * @param string $identifier + * @return null|int + */ + protected function getPageID(string $identifier) { + $sql = "SELECT pageID + FROM wcf" . WCF_N . "_page + WHERE identifier = ?"; + $statement = WCF::getDB()->prepareStatement($sql, 1); + $statement->execute([$identifier]); + + return $statement->fetchSingleColumn(); + } + /** * @inheritDoc */ @@ -206,8 +250,361 @@ class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPl /** * @inheritDoc + * @since 3.1 */ public static function getSyncDependencies() { return ['language', 'menu', 'page']; } + + /** + * @inheritDoc + * @since 3.2 + */ + public function addFormFields(IFormDocument $form) { + $menuList = new MenuList(); + $menuList->readObjects(); + + /** @var FormContainer $dataContainer */ + $dataContainer = $form->getNodeById('data'); + + $dataContainer->appendChildren([ + TextFormField::create('identifier') + ->label('wcf.acp.pip.menuItem.identifier') + ->description('wcf.acp.pip.menuItem.identifier.description') + ->required() + ->addValidator(ObjectTypePackageInstallationPlugin::getObjectTypeAlikeValueValidator( + 'wcf.acp.pip.menuItem.identifier', + 4 + )) + ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField $formField) { + if ( + $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE || + $this->editedEntry->getAttribute('identifier') !== $formField->getValue() + ) { + $menuItemList = new MenuItemList(); + $menuItemList->getConditionBuilder()->add('identifier = ?', [$formField->getValue()]); + + if ($menuItemList->countObjects() > 0) { + $formField->addValidationError( + new FormFieldValidationError( + 'notUnique', + 'wcf.acp.pip.menuItem.identifier.error.notUnique' + ) + ); + } + } + })), + + TitleFormField::create() + ->required() + ->i18n() + ->i18nRequired() + ->languageItemPattern('__NONE__'), + + SingleSelectionFormField::create('menu') + ->label('wcf.acp.pip.menuItem.menu') + ->description('wcf.acp.pip.menuItem.menu.description') + ->required() + ->options(function() use ($menuList): array { + $options = []; + foreach ($menuList as $menu) { + $options[$menu->identifier] = $menu->identifier; + } + + asort($options); + + return $options; + }), + + RadioButtonFormField::create('linkType') + ->label('wcf.acp.pip.menuItem.linkType') + ->required() + ->options([ + 'internal' => 'wcf.acp.pip.menuItem.linkType.internal', + 'external' => 'wcf.acp.pip.menuItem.linkType.external' + ]) + ->value('internal'), + + SingleSelectionFormField::create('menuItemPage') + ->objectProperty('page') + ->label('wcf.acp.pip.menuItem.page') + ->description('wcf.acp.pip.menuItem.page.description') + ->required() + ->filterable() + ->options(function(): array { + $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), + + TextFormField::create('externalURL') + ->label('wcf.acp.pip.menuItem.externalURL') + ->description('wcf.acp.pip.menuItem.externalURL.description') + ->required() + ->i18n(), + + IntegerFormField::create('showOrder') + ->objectProperty('showorder') + ->label('wcf.acp.pip.menuItem.showOrder') + ->description('wcf.acp.pip.menuItem.showOrder.description') + ->objectProperty('showorder') + ->minimum(1) + ]); + + /** @var SingleSelectionFormField $menuField */ + $menuField = $form->getNodeById('menu'); + + foreach ($menuList as $menu) { + $dataContainer->insertBefore( + SingleSelectionFormField::create('parentMenuItem' . $menu->menuID) + ->objectProperty('parent') + ->label('wcf.acp.pip.menuItem.parentMenuItem') + ->options(function() use($menu): array { + $options = [[ + 'depth' => 0, + 'label' => 'wcf.global.noSelection', + 'value' => '' + ]]; + + $packageIDs = array_merge( + [$this->installation->getPackage()->packageID], + array_keys($this->installation->getPackage()->getAllRequiredPackages()) + ); + + /** @var MenuItemNode $menuItem */ + foreach ($menu->getMenuItemNodeList() as $menuItem) { + if (in_array($menuItem->packageID, $packageIDs)) { + $options[] = [ + 'depth' => $menuItem->getDepth() - 1, + 'label' => $menuItem->identifier, + 'value' => $menuItem->identifier + ]; + } + } + + if (count($options) === 1) { + return []; + } + + return $options; + }, true) + ->addDependency( + ValueFormFieldDependency::create('menu') + ->field($menuField) + ->values([$menu->identifier]) + ), + 'linkType' + ); + } + + // dependencies + + /** @var RadioButtonFormField $linkType */ + $linkType = $form->getNodeById('linkType'); + $form->getNodeById('menuItemPage')->addDependency( + ValueFormFieldDependency::create('linkType') + ->field($linkType) + ->values(['internal']) + ); + + $form->getNodeById('externalURL')->addDependency( + ValueFormFieldDependency::create('linkType') + ->field($linkType) + ->values(['external']) + ); + } + + /** + * @inheritDoc + * @since 3.2 + */ + protected function getElementData(\DOMElement $element, bool $saveData = false): array { + $data = [ + 'identifier' => $element->getAttribute('identifier'), + 'packageID' => $this->installation->getPackageID(), + 'originIsSystem' => 1, + 'title' => [] + ]; + + /** @var \DOMElement $title */ + foreach ($element->getElementsByTagName('title') as $title) { + $data['title'][LanguageFactory::getInstance()->getLanguageByCode($title->getAttribute('language'))->languageID] = $title->nodeValue; + } + + foreach (['externalURL', 'menu', 'page', 'parent', 'showOrder'] as $optionalElementName) { + $optionalElement = $element->getElementsByTagName($optionalElementName)->item(0); + if ($optionalElement !== null) { + $data[$optionalElementName] = $optionalElement->nodeValue; + } + } + + if (isset($data['parent'])) { + $menuItemList = new MenuItemList(); + $menuItemList->getConditionBuilder()->add('identifier = ?', [$data['parent']]); + $menuItemList->getConditionBuilder()->add('packageID IN (?)', [ + array_merge( + [$this->installation->getPackage()->packageID], + array_keys($this->installation->getPackage()->getAllRequiredPackages()) + ) + ]); + $menuItemList->readObjects(); + + if (count($menuItemList) === 1) { + if ($saveData) { + $data['menuID'] = $menuItemList->current()->menuID; + } + else { + $data['menu'] = (new Menu($menuItemList->current()->menuID))->identifier; + } + } + } + else if (isset($data['menu']) && $saveData) { + $data['menuID'] = $this->getMenuID($data['menu']); + unset($data['menu']); + } + + if ($saveData) { + // updating menu items is not supported thus handling the title + // array causes issues + if ($this->editedEntry !== null) { + unset($data['title']); + } + else { + $titles = []; + foreach ($data['title'] as $languageID => $title) { + $titles[LanguageFactory::getInstance()->getLanguage($languageID)->languageCode] = $title; + } + + $data['title'] = $titles; + } + + if (isset($data['page'])) { + $data['pageID'] = $this->getPageID($data['page']); + unset($data['page']); + } + } + + return $data; + } + + /** + * @inheritDoc + * @since 3.2 + */ + public function getElementIdentifier(\DOMElement $element): string { + return $element->getAttribute('identifier'); + } + + /** + * @inheritDoc + * @since 3.2 + */ + protected function setEntryListKeys(IDevtoolsPipEntryList $entryList) { + $entryList->setKeys([ + 'identifier' => 'wcf.acp.pip.menuItem.identifier', + 'menu' => 'wcf.acp.pip.menuItem.menu' + ]); + } + + /** + * @inheritDoc + * @since 3.2 + */ + protected function sortDocument(\DOMDocument $document) { + $this->sortImportDelete($document); + + $this->sortChildNodes($document->getElementsByTagName('import'), function(\DOMElement $element1, \DOMElement $element2) { + // first, compare by `menu`, then by `parent` ... + foreach (['menu', 'parent'] as $element) { + $compareElement1 = $element1->getElementsByTagName($element)->item(0); + $compareElement2 = $element2->getElementsByTagName($element)->item(0); + + if ($compareElement1 !== null) { + if ($compareElement2 !== null) { + $compare = $compareElement1->nodeValue <=> $compareElement2->nodeValue; + + if ($compare !== 0) { + return $compare; + } + } + + return -1; + } + else if ($compareElement2 !== null) { + return 1; + } + } + + // ... and lastly by `identifier` + return strcmp( + $element1->getAttribute('identifier'), + $element2->getAttribute('identifier') + ); + }); + + $this->sortChildNodes($document->getElementsByTagName('delete'), function(\DOMElement $element1, \DOMElement $element2) { + return strcmp( + $element1->getAttribute('identifier'), + $element2->getAttribute('identifier') + ); + }); + } + + /** + * @inheritDoc + * @since 3.2 + */ + protected function writeEntry(\DOMDocument $document, IFormDocument $form): \DOMElement { + $formData = $form->getData(); + $data = $formData['data']; + + $menuItem = $document->createElement('item'); + $menuItem->setAttribute('identifier', $data['identifier']); + + if (!empty($data['menu'])) { + $menuItem->appendChild($document->createElement('menu', $data['menu'])); + } + else if (!empty($data['parent'])) { + $menuItem->appendChild($document->createElement('parent', $data['parent'])); + } + + foreach ($formData['title_i18n'] as $languageID => $title) { + $title = $document->createElement('title', $this->getAutoCdataValue($title)); + $title->setAttribute('language', LanguageFactory::getInstance()->getLanguage($languageID)->languageCode); + + $menuItem->appendChild($title); + } + + foreach (['page', 'externalURL', 'showOrder'] as $property) { + if (!empty($data[$property])) { + $menuItem->appendChild($document->createElement($property, (string)$data[$property])); + } + } + + $document->getElementsByTagName('import')->item(0)->appendChild($menuItem); + + return $menuItem; + } } diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index f464ffbb0e..976ec618cf 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -1964,6 +1964,24 @@ If you have already bought the licenses for the listed apps, th Hidden]]> + + com.foo.bar.menu]]> + {$segment}{else}(empty){/if} (segment {#$segmentNumber + 1}){/implode}.]]> + + 1}s{/if}.]]> + + + + + + + + + + + + + -- 2.20.1