2 namespace wcf\system\package\plugin
;
4 use wcf\data\box\BoxEditor
;
5 use wcf\data\menu\Menu
;
6 use wcf\data\menu\MenuEditor
;
7 use wcf\data\menu\MenuList
;
8 use wcf\data\page\PageNode
;
9 use wcf\data\page\PageNodeTree
;
10 use wcf\system\database\util\PreparedStatementConditionBuilder
;
11 use wcf\system\devtools\pip\IDevtoolsPipEntryList
;
12 use wcf\system\devtools\pip\IGuiPackageInstallationPlugin
;
13 use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin
;
14 use wcf\system\exception\SystemException
;
15 use wcf\system\form\builder\container\FormContainer
;
16 use wcf\system\form\builder\field\BooleanFormField
;
17 use wcf\system\form\builder\field\dependency\NonEmptyFormFieldDependency
;
18 use wcf\system\form\builder\field\dependency\ValueFormFieldDependency
;
19 use wcf\system\form\builder\field\ItemListFormField
;
20 use wcf\system\form\builder\field\MultipleSelectionFormField
;
21 use wcf\system\form\builder\field\SingleSelectionFormField
;
22 use wcf\system\form\builder\field\TextFormField
;
23 use wcf\system\form\builder\field\TitleFormField
;
24 use wcf\system\form\builder\field\validation\FormFieldValidationError
;
25 use wcf\system\form\builder\field\validation\FormFieldValidator
;
26 use wcf\system\form\builder\field\validation\FormFieldValidatorUtil
;
27 use wcf\system\form\builder\IFormDocument
;
28 use wcf\system\language\LanguageFactory
;
32 * Installs, updates and deletes menus.
34 * @author Alexander Ebert
35 * @copyright 2001-2018 WoltLab GmbH
36 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
37 * @package WoltLabSuite\Core\Acp\Package\Plugin
40 class MenuPackageInstallationPlugin
extends AbstractXMLPackageInstallationPlugin
implements IGuiPackageInstallationPlugin
{
41 use TXmlGuiPackageInstallationPlugin
;
44 * box meta data per menu
50 * visibility exceptions per box
53 public $visibilityExceptions = [];
58 public $className = MenuEditor
::class;
63 public $tagName = 'menu';
68 protected function handleDelete(array $items) {
69 $sql = "DELETE FROM wcf".WCF_N
."_menu
72 $statement = WCF
::getDB()->prepareStatement($sql);
74 WCF
::getDB()->beginTransaction();
75 foreach ($items as $item) {
77 $item['attributes']['identifier'],
78 $this->installation
->getPackageID()
81 WCF
::getDB()->commitTransaction();
86 * @throws SystemException
88 protected function getElement(\DOMXPath
$xpath, array &$elements, \DOMElement
$element) {
89 $nodeValue = $element->nodeValue
;
91 if ($element->tagName
=== 'title') {
92 if (empty($element->getAttribute('language'))) {
93 throw new SystemException("Missing required attribute 'language' for menu '" . $element->parentNode
->getAttribute('identifier') . "'");
96 // <title> can occur multiple times using the `language` attribute
97 if (!isset($elements['title'])) $elements['title'] = [];
99 $elements['title'][$element->getAttribute('language')] = $element->nodeValue
;
101 else if ($element->tagName
=== 'box') {
102 $elements['box'] = [];
104 /** @var \DOMElement $child */
105 foreach ($xpath->query('child::*', $element) as $child) {
106 if ($child->tagName
=== 'name') {
107 if (empty($child->getAttribute('language'))) {
108 throw new SystemException("Missing required attribute 'language' for box name (menu '" . $element->parentNode
->getAttribute('identifier') . "')");
111 // <title> can occur multiple times using the `language` attribute
112 if (!isset($elements['box']['name'])) $elements['box']['name'] = [];
114 $elements['box']['name'][$element->getAttribute('language')] = $element->nodeValue
;
116 else if ($child->tagName
=== 'visibilityExceptions') {
117 $elements['box']['visibilityExceptions'] = [];
118 /** @var \DOMElement $child */
119 foreach ($xpath->query('child::*', $child) as $child2) {
120 $elements['box']['visibilityExceptions'][] = $child2->nodeValue
;
124 $elements['box'][$child->tagName
] = $child->nodeValue
;
129 $elements[$element->tagName
] = $nodeValue;
136 protected function prepareImport(array $data) {
137 $identifier = $data['attributes']['identifier'];
139 if (!empty($data['elements']['box'])) {
140 $position = $data['elements']['box']['position'];
142 if ($identifier === 'com.woltlab.wcf.MainMenu') {
143 $position = 'mainMenu';
145 else if (!in_array($position, Box
::$availableMenuPositions)) {
146 throw new SystemException("Unknown box position '{$position}' for menu box '{$identifier}'");
149 $this->boxData
[$identifier] = [
150 'identifier' => $identifier,
151 'name' => $this->getI18nValues($data['elements']['title'], true),
153 'position' => $position,
154 'showHeader' => !empty($data['elements']['box']['showHeader']) ?
1 : 0,
155 'visibleEverywhere' => !empty($data['elements']['box']['visibleEverywhere']) ?
1 : 0,
156 'cssClassName' => (!empty($data['elements']['box']['cssClassName'])) ?
$data['elements']['box']['cssClassName'] : '',
157 'originIsSystem' => 1,
158 'packageID' => $this->installation
->getPackageID()
161 if (!empty($data['elements']['box']['visibilityExceptions'])) {
162 $this->visibilityExceptions
[$identifier] = $data['elements']['box']['visibilityExceptions'];
165 unset($data['elements']['box']);
169 'identifier' => $identifier,
170 'title' => $this->getI18nValues($data['elements']['title']),
171 'originIsSystem' => 1
178 protected function findExistingItem(array $data) {
180 FROM wcf".WCF_N
."_menu
185 $this->installation
->getPackageID()
190 'parameters' => $parameters
197 protected function import(array $row, array $data) {
198 // updating menus is not supported because the only modifiable data is the
199 // title and overwriting it could conflict with user changes
201 return new Menu(null, $row);
204 return parent
::import($row, $data);
210 protected function postImport() {
211 if (empty($this->boxData
)) return;
213 // all boxes belonging to the identifiers
214 $conditions = new PreparedStatementConditionBuilder();
215 $conditions->add("identifier IN (?)", [array_keys($this->boxData
)]);
216 $conditions->add("packageID = ?", [$this->installation
->getPackageID()]);
219 FROM wcf".WCF_N
."_box
221 $statement = WCF
::getDB()->prepareStatement($sql);
222 $statement->execute($conditions->getParameters());
224 /** @var Box[] $boxes */
225 $boxes = $statement->fetchObjects(Box
::class, 'identifier');
227 // fetch all menus relevant
228 $menuList = new MenuList();
229 $menuList->getConditionBuilder()->add('identifier IN (?)', [array_keys($this->boxData
)]);
230 $menuList->readObjects();
233 foreach ($menuList as $menu) {
234 $menus[$menu->identifier
] = $menu;
237 // handle visibility exceptions
238 $sql = "DELETE FROM wcf".WCF_N
."_box_to_page
240 $deleteStatement = WCF
::getDB()->prepareStatement($sql);
241 $sql = "INSERT IGNORE wcf".WCF_N
."_box_to_page
242 (boxID, pageID, visible)
244 $insertStatement = WCF
::getDB()->prepareStatement($sql);
245 foreach ($this->boxData
as $identifier => $data) {
246 // connect box with menu
247 if (isset($menus[$identifier])) {
248 $data['menuID'] = $menus[$identifier]->menuID
;
252 if (isset($boxes[$identifier])) {
253 $box = $boxes[$identifier];
255 // delete old visibility exceptions
256 $deleteStatement->execute([$box->boxID
]);
258 // skip both 'identifier' and 'packageID' as these properties are immutable
259 unset($data['identifier']);
260 unset($data['packageID']);
262 $boxEditor = new BoxEditor($box);
263 $boxEditor->update($data);
266 $box = BoxEditor
::create($data);
269 // save visibility exceptions
270 if (!empty($this->visibilityExceptions
[$identifier])) {
272 $conditionBuilder = new PreparedStatementConditionBuilder();
273 $conditionBuilder->add('identifier IN (?)', [$this->visibilityExceptions
[$identifier]]);
274 $sql = "SELECT pageID
275 FROM wcf" . WCF_N
. "_page
276 " . $conditionBuilder;
277 $statement = WCF
::getDB()->prepareStatement($sql);
278 $statement->execute($conditionBuilder->getParameters());
279 $pageIDs = $statement->fetchAll(\PDO
::FETCH_COLUMN
);
282 foreach ($pageIDs as $pageID) {
283 $insertStatement->execute([$box->boxID
, $pageID, $box->visibleEverywhere ?
0 : 1]);
293 public static function getSyncDependencies() {
301 public function getAdditionalTemplateCode() {
302 return WCF
::getTPL()->fetch('__menuPipGui');
309 protected function addFormFields(IFormDocument
$form) {
310 /** @var FormContainer $dataContainer */
311 $dataContainer = $form->getNodeById('data');
313 $dataContainer->appendChildren([
314 TextFormField
::create('identifier')
315 ->label('wcf.acp.pip.menu.identifier')
316 ->description('wcf.acp.pip.menu.identifier.description')
318 ->addValidator(FormFieldValidatorUtil
::getDotSeparatedStringValidator(
319 'wcf.acp.pip.menu.identifier',
322 ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField
$formField) {
324 $formField->getDocument()->getFormMode() === IFormDocument
::FORM_MODE_CREATE ||
325 $this->editedEntry
->getAttribute('identifier') !== $formField->getValue()
327 $menuList = new MenuList();
328 $menuList->getConditionBuilder()->add('identifier = ?', [$formField->getValue()]);
330 if ($menuList->countObjects() > 0) {
331 $formField->addValidationError(
332 new FormFieldValidationError(
334 'wcf.acp.pip.menu.identifier.error.notUnique'
341 TitleFormField
::create()
345 ->languageItemPattern('__NONE__'),
347 BooleanFormField
::create('createBox')
348 ->label('wcf.acp.pip.menu.createBox')
349 ->description('wcf.acp.pip.menu.createBox.description'),
351 SingleSelectionFormField
::create('boxPosition')
352 ->label('wcf.acp.pip.menu.boxPosition')
353 ->description('wcf.acp.pip.menu.boxPosition.description')
354 ->options(array_combine(Box
::$availablePositions, Box
::$availablePositions)),
356 BooleanFormField
::create('boxShowHeader')
357 ->label('wcf.acp.pip.menu.boxShowHeader'),
359 BooleanFormField
::create('boxVisibleEverywhere')
360 ->label('wcf.acp.pip.menu.boxVisibleEverywhere'),
362 MultipleSelectionFormField
::create('boxVisibilityExceptions')
363 ->label('wcf.acp.pip.menu.boxVisibilityExceptions.hiddenEverywhere')
365 ->options(function() {
366 $pageNodeList = (new PageNodeTree())->getNodeList();
369 /** @var PageNode $pageNode */
370 foreach ($pageNodeList as $pageNode) {
372 'depth' => $pageNode->getDepth() - 1,
373 'label' => $pageNode->name
,
374 'value' => $pageNode->identifier
378 return $nestedOptions;
381 ItemListFormField
::create('boxCssClassName')
382 ->label('wcf.acp.pip.menu.boxCssClassName')
383 ->description('wcf.acp.pip.menu.boxCssClassName.description')
384 ->saveValueType(ItemListFormField
::SAVE_VALUE_TYPE_SSV
)
387 /** @var BooleanFormField $createBox */
388 $createBox = $form->getNodeById('createBox');
389 foreach (['boxPosition', 'boxShowHeader', 'boxVisibleEverywhere', 'boxVisibilityExceptions', 'boxCssClassName'] as $boxField) {
390 $form->getNodeById($boxField)->addDependency(
391 NonEmptyFormFieldDependency
::create('createBox')
396 /** @var TextFormField $identifier */
397 $identifier = $form->getNodeById('identifier');
398 $form->getNodeById('boxPosition')->addDependency(
399 ValueFormFieldDependency
::create('identifier')
401 ->values(['com.woltlab.wcf.MainMenu'])
410 protected function fetchElementData(\DOMElement
$element, $saveData) {
412 'identifier' => $element->getAttribute('identifier'),
413 'packageID' => $this->installation
->getPackageID(),
417 /** @var \DOMElement $title */
418 foreach ($element->getElementsByTagName('title') as $title) {
419 $data['title'][LanguageFactory
::getInstance()->getLanguageByCode($title->getAttribute('language'))->languageID
] = $title->nodeValue
;
422 $box = $element->getElementsByTagName('box')->item(0);
425 $boxData['position'] = $box->getElementsByTagName('position')->item(0)->nodeValue
;
427 // work-around for unofficial position `mainMenu`
428 if ($data['identifier'] === 'com.woltlab.wcf.MainMenu' && !$saveData) {
429 unset($boxData['position']);
432 $showHeader = $element->getElementsByTagName('showHeader')->item(0);
433 if ($showHeader !== null) {
434 $boxData['showHeader'] = $showHeader->nodeValue
;
437 $visibleEverywhere = $element->getElementsByTagName('visibleEverywhere')->item(0);
438 if ($visibleEverywhere !== null) {
439 $boxData['visibleEverywhere'] = $visibleEverywhere->nodeValue
;
442 $cssClassName = $element->getElementsByTagName('cssClassName')->item(0);
443 if ($cssClassName !== null) {
444 $boxData['cssClassName'] = $cssClassName->nodeValue
;
447 $visibilityExceptions = $element->getElementsByTagName('visibilityExceptions');
448 if ($visibilityExceptions->length
> 0) {
449 $boxData['visibilityExceptions'] = [];
451 /** @var \DOMElement $visibilityException */
452 foreach ($visibilityExceptions as $visibilityException) {
453 $boxData['visibilityExceptions'] = $visibilityException->nodeValue
;
459 if (!empty($boxData)) {
460 $this->boxData
[$data['identifier']] = [
461 'identifier' => $data['identifier'],
462 'name' => $this->getI18nValues($data['title'], true),
464 'position' => $boxData['position'],
465 'showHeader' => $boxData['showHeader'] ??
0,
466 'visibleEverywhere' => $boxData['visibleEverywhere'] ??
0,
467 'cssClassName' => $boxData['cssClassName'] ??
'',
468 'originIsSystem' => 1,
469 'packageID' => $this->installation
->getPackageID()
472 if (!empty($boxData['visibilityExceptions'])) {
473 $this->visibilityExceptions
[$data['identifier']] = $boxData['visibilityExceptions'];
477 // update menus is not supported thus handling the title
478 // array causes issues
479 if ($this->editedEntry
!== null) {
480 unset($data['title']);
484 foreach ($data['title'] as $languageID => $title) {
485 $titles[LanguageFactory
::getInstance()->getLanguage($languageID)->languageCode
] = $title;
488 $data['title'] = $titles;
492 $data['createBox'] = $box !== null;
494 foreach ($boxData as $key => $value) {
495 $data['box' . ucfirst($key)] = $value;
506 public function getElementIdentifier(\DOMElement
$element) {
507 return $element->getAttribute('identifier');
514 protected function setEntryListKeys(IDevtoolsPipEntryList
$entryList) {
515 $entryList->setKeys([
516 'identifier' => 'wcf.acp.pip.menu.identifier'
524 protected function prepareXmlElement(\DOMDocument
$document, IFormDocument
$form) {
525 $formData = $form->getData();
527 if ($formData['data']['identifier'] === 'com.woltlab.wcf.MainMenu') {
528 $formData['data']['boxPosition'] = 'mainMenu';
531 $menu = $document->createElement($this->tagName
);
532 $menu->setAttribute('identifier', $formData['data']['identifier']);
534 foreach ($formData['title_i18n'] as $languageID => $title) {
535 $title = $document->createElement('title', $this->getAutoCdataValue($title));
536 $title->setAttribute('language', LanguageFactory
::getInstance()->getLanguage($languageID)->languageCode
);
538 $menu->appendChild($title);
541 if ($formData['data']['createBox']) {
542 $box = $document->createElement('box');
544 $box->appendChild($document->createElement('position', $formData['data']['boxPosition']));
546 foreach (['showHeader' => 0, 'visibleEverywhere' => 0, 'cssClassName' => ''] as $boxProperty => $defaultValue) {
547 $index = 'box' . ucfirst($boxProperty);
548 if (isset($formData['data'][$index]) && $formData['data'][$index] !== $defaultValue) {
549 $box->appendChild($document->createElement($boxProperty, (string)$formData['data'][$index]));
553 if (!empty($formData['data']['boxVisibilityExceptions'])) {
554 $visibilityExceptions = $box->appendChild($document->createElement('visibilityExceptions'));
556 foreach ($formData['data']['boxVisibilityExceptions'] as $pageIdentifier) {
557 $visibilityExceptions->appendChild($document->createElement('page', $pageIdentifier));
561 $menu->appendChild($box);