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')
358 ->description('wcf.acp.pip.menu.boxShowHeader.description'),
360 BooleanFormField
::create('boxVisibleEverywhere')
361 ->label('wcf.acp.pip.menu.boxVisibleEverywhere'),
363 MultipleSelectionFormField
::create('boxVisibilityExceptions')
364 ->label('wcf.acp.pip.menu.boxVisibilityExceptions.hiddenEverywhere')
366 ->options(function() {
367 $pageNodeList = (new PageNodeTree())->getNodeList();
370 /** @var PageNode $pageNode */
371 foreach ($pageNodeList as $pageNode) {
373 'depth' => $pageNode->getDepth() - 1,
374 'label' => $pageNode->name
,
375 'value' => $pageNode->identifier
379 return $nestedOptions;
382 ItemListFormField
::create('boxCssClassName')
383 ->label('wcf.acp.pip.menu.boxCssClassName')
384 ->description('wcf.acp.pip.menu.boxCssClassName.description')
385 ->saveValueType(ItemListFormField
::SAVE_VALUE_TYPE_SSV
)
388 /** @var BooleanFormField $createBox */
389 $createBox = $form->getNodeById('createBox');
390 foreach (['boxPosition', 'boxShowHeader', 'boxVisibleEverywhere', 'boxVisibilityExceptions', 'boxCssClassName'] as $boxField) {
391 $form->getNodeById($boxField)->addDependency(
392 NonEmptyFormFieldDependency
::create('createBox')
397 $form->getNodeById('boxPosition')->addDependency(
398 ValueFormFieldDependency
::create('identifier')
399 ->field($form->getNodeById('identifier'))
400 ->values(['com.woltlab.wcf.MainMenu'])
409 protected function doGetElementData(\DOMElement
$element, $saveData) {
411 'identifier' => $element->getAttribute('identifier'),
412 'packageID' => $this->installation
->getPackageID(),
416 /** @var \DOMElement $title */
417 foreach ($element->getElementsByTagName('title') as $title) {
418 $data['title'][LanguageFactory
::getInstance()->getLanguageByCode($title->getAttribute('language'))->languageID
] = $title->nodeValue
;
421 $box = $element->getElementsByTagName('box')->item(0);
424 $boxData['position'] = $box->getElementsByTagName('position')->item(0)->nodeValue
;
426 // work-around for unofficial position `mainMenu`
427 if ($data['identifier'] === 'com.woltlab.wcf.MainMenu' && !$saveData) {
428 unset($boxData['position']);
431 $showHeader = $element->getElementsByTagName('showHeader')->item(0);
432 if ($showHeader !== null) {
433 $boxData['showHeader'] = $showHeader->nodeValue
;
436 $visibleEverywhere = $element->getElementsByTagName('visibleEverywhere')->item(0);
437 if ($visibleEverywhere !== null) {
438 $boxData['visibleEverywhere'] = $visibleEverywhere->nodeValue
;
441 $cssClassName = $element->getElementsByTagName('cssClassName')->item(0);
442 if ($cssClassName !== null) {
443 $boxData['cssClassName'] = $cssClassName->nodeValue
;
446 $visibilityExceptions = $element->getElementsByTagName('visibilityExceptions');
447 if ($visibilityExceptions->length
> 0) {
448 $boxData['visibilityExceptions'] = [];
450 /** @var \DOMElement $visibilityException */
451 foreach ($visibilityExceptions as $visibilityException) {
452 $boxData['visibilityExceptions'] = $visibilityException->nodeValue
;
458 if (!empty($boxData)) {
459 $this->boxData
[$data['identifier']] = [
460 'identifier' => $data['identifier'],
461 'name' => $this->getI18nValues($data['title'], true),
463 'position' => $boxData['position'],
464 'showHeader' => $boxData['showHeader'] ??
0,
465 'visibleEverywhere' => $boxData['visibleEverywhere'] ??
0,
466 'cssClassName' => $boxData['cssClassName'] ??
'',
467 'originIsSystem' => 1,
468 'packageID' => $this->installation
->getPackageID()
471 if (!empty($boxData['visibilityExceptions'])) {
472 $this->visibilityExceptions
[$data['identifier']] = $boxData['visibilityExceptions'];
476 // update menus is not supported thus handling the title
477 // array causes issues
478 if ($this->editedEntry
!== null) {
479 unset($data['title']);
483 foreach ($data['title'] as $languageID => $title) {
484 $titles[LanguageFactory
::getInstance()->getLanguage($languageID)->languageCode
] = $title;
487 $data['title'] = $titles;
491 $data['createBox'] = $box !== null;
493 foreach ($boxData as $key => $value) {
494 $data['box' . ucfirst($key)] = $value;
505 public function getElementIdentifier(\DOMElement
$element) {
506 return $element->getAttribute('identifier');
513 protected function setEntryListKeys(IDevtoolsPipEntryList
$entryList) {
514 $entryList->setKeys([
515 'identifier' => 'wcf.acp.pip.menu.identifier'
523 protected function doCreateXmlElement(\DOMDocument
$document, IFormDocument
$form) {
524 $formData = $form->getData();
526 if ($formData['data']['identifier'] === 'com.woltlab.wcf.MainMenu') {
527 $formData['data']['boxPosition'] = 'mainMenu';
530 $menu = $document->createElement($this->tagName
);
531 $menu->setAttribute('identifier', $formData['data']['identifier']);
533 foreach ($formData['title_i18n'] as $languageID => $title) {
534 $title = $document->createElement('title', $this->getAutoCdataValue($title));
535 $title->setAttribute('language', LanguageFactory
::getInstance()->getLanguage($languageID)->languageCode
);
537 $menu->appendChild($title);
540 if ($formData['data']['createBox']) {
541 $box = $document->createElement('box');
543 $box->appendChild($document->createElement('position', $formData['data']['boxPosition']));
545 foreach (['showHeader' => 0, 'visibleEverywhere' => 0, 'cssClassName' => ''] as $boxProperty => $defaultValue) {
546 $index = 'box' . ucfirst($boxProperty);
547 if (isset($formData['data'][$index]) && $formData['data'][$index] !== $defaultValue) {
548 $box->appendChild($document->createElement($boxProperty, (string)$formData['data'][$index]));
552 if (!empty($formData['data']['boxVisibilityExceptions'])) {
553 $visibilityExceptions = $box->appendChild($document->createElement('visibilityExceptions'));
555 foreach ($formData['data']['boxVisibilityExceptions'] as $pageIdentifier) {
556 $visibilityExceptions->appendChild($document->createElement('page', $pageIdentifier));
560 $menu->appendChild($box);