Apply PSR-12 code style (#3886)
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / plugin / MenuItemPackageInstallationPlugin.class.php
CommitLineData
effc9389 1<?php
a9229942 2
effc9389 3namespace wcf\system\package\plugin;
a9229942 4
effc9389
AE
5use wcf\data\menu\item\MenuItem;
6use wcf\data\menu\item\MenuItemEditor;
2c3107d0
MS
7use wcf\data\menu\item\MenuItemList;
8use wcf\data\menu\item\MenuItemNode;
9use wcf\data\menu\Menu;
10use wcf\data\menu\MenuList;
11use wcf\data\page\PageNode;
12use wcf\data\page\PageNodeTree;
13use wcf\system\devtools\pip\IDevtoolsPipEntryList;
14use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
15use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
effc9389 16use wcf\system\exception\SystemException;
2c3107d0
MS
17use wcf\system\form\builder\container\FormContainer;
18use wcf\system\form\builder\field\dependency\ValueFormFieldDependency;
2c3107d0
MS
19use wcf\system\form\builder\field\RadioButtonFormField;
20use wcf\system\form\builder\field\SingleSelectionFormField;
21use wcf\system\form\builder\field\TextFormField;
22use wcf\system\form\builder\field\TitleFormField;
23use wcf\system\form\builder\field\validation\FormFieldValidationError;
24use wcf\system\form\builder\field\validation\FormFieldValidator;
ab116957 25use wcf\system\form\builder\field\validation\FormFieldValidatorUtil;
2c3107d0
MS
26use wcf\system\form\builder\IFormDocument;
27use wcf\system\language\LanguageFactory;
effc9389
AE
28use wcf\system\WCF;
29
30/**
31 * Installs, updates and deletes menu items.
a9229942
TD
32 *
33 * @author Alexander Ebert, Matthias Schmidt
34 * @copyright 2001-2019 WoltLab GmbH
35 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
36 * @package WoltLabSuite\Core\Acp\Package\Plugin
37 * @since 3.0
effc9389 38 */
a9229942
TD
39class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements
40 IGuiPackageInstallationPlugin
41{
42 use TXmlGuiPackageInstallationPlugin;
43
44 /**
45 * @inheritDoc
46 */
47 public $className = MenuItemEditor::class;
48
49 /**
50 * @inheritDoc
51 */
52 public $tagName = 'item';
53
54 /**
55 * @inheritDoc
56 */
57 protected function handleDelete(array $items)
58 {
59 $sql = "DELETE FROM wcf" . WCF_N . "_menu_item
60 WHERE identifier = ?
61 AND packageID = ?";
62 $statement = WCF::getDB()->prepareStatement($sql);
63
64 $sql = "DELETE FROM wcf" . WCF_N . "_language_item
65 WHERE languageItem = ?";
66 $languageItemStatement = WCF::getDB()->prepareStatement($sql);
67
68 WCF::getDB()->beginTransaction();
69 foreach ($items as $item) {
70 $statement->execute([
71 $item['attributes']['identifier'],
72 $this->installation->getPackageID(),
73 ]);
74
75 $languageItemStatement->execute([
76 'wcf.menu.item.' . $item['attributes']['identifier'],
77 ]);
78 }
79 WCF::getDB()->commitTransaction();
80 }
81
82 /**
83 * @inheritDoc
84 * @throws SystemException
85 */
86 protected function getElement(\DOMXPath $xpath, array &$elements, \DOMElement $element)
87 {
88 $nodeValue = $element->nodeValue;
89
90 if ($element->tagName === 'title') {
91 if (empty($element->getAttribute('language'))) {
92 throw new SystemException("Missing required attribute 'language' for menu item '" . $element->parentNode->getAttribute('identifier') . "'");
93 }
94
95 // <title> can occur multiple times using the `language` attribute
96 if (!isset($elements['title'])) {
97 $elements['title'] = [];
98 }
99
100 $elements['title'][$element->getAttribute('language')] = $element->nodeValue;
101 } else {
102 $elements[$element->tagName] = $nodeValue;
103 }
104 }
105
106 /**
107 * @inheritDoc
108 * @throws SystemException
109 */
110 protected function prepareImport(array $data)
111 {
112 $menuID = null;
113 if (!empty($data['elements']['menu'])) {
114 $menuID = $this->getMenuID($data['elements']['menu']);
115
116 if ($menuID === null) {
117 throw new SystemException("Unable to find menu '" . $data['elements']['menu'] . "' for menu item '" . $data['attributes']['identifier'] . "'");
118 }
119 }
120
121 $parentItemID = null;
122 if (!empty($data['elements']['parent'])) {
123 if ($menuID !== null) {
124 throw new SystemException("The menu item '" . $data['attributes']['identifier'] . "' can either have an associated menu or a parent menu item, but not both.");
125 }
126
127 $sql = "SELECT *
128 FROM wcf" . WCF_N . "_menu_item
129 WHERE identifier = ?";
130 $statement = WCF::getDB()->prepareStatement($sql, 1);
131 $statement->execute([$data['elements']['parent']]);
132
133 /** @var MenuItem|null $parent */
134 $parent = $statement->fetchObject(MenuItem::class);
135 if ($parent === null) {
136 throw new SystemException("Unable to find parent menu item '" . $data['elements']['parent'] . "' for menu item '" . $data['attributes']['identifier'] . "'");
137 }
138
139 $parentItemID = $parent->itemID;
140 $menuID = $parent->menuID;
141 }
142
143 if ($menuID === null && $parentItemID === null) {
144 throw new SystemException("The menu item '" . $data['attributes']['identifier'] . "' must either have an associated menu or a parent menu item.");
145 }
146
147 $pageID = null;
148 if (!empty($data['elements']['page'])) {
149 $pageID = $this->getPageID($data['elements']['page']);
150
151 if ($pageID === null) {
152 throw new SystemException("Unable to find page '" . $data['elements']['page'] . "' for menu item '" . $data['attributes']['identifier'] . "'");
153 }
154 }
155
156 $externalURL = (!empty($data['elements']['externalURL'])) ? $data['elements']['externalURL'] : '';
157
158 if ($pageID === null && empty($externalURL)) {
159 throw new SystemException("The menu item '" . $data['attributes']['identifier'] . "' must either have an associated page or an external url set.");
160 } elseif ($pageID !== null && !empty($externalURL)) {
161 throw new SystemException("The menu item '" . $data['attributes']['identifier'] . "' can either have an associated page or an external url, but not both.");
162 }
163
164 return [
165 'externalURL' => $externalURL,
166 'identifier' => $data['attributes']['identifier'],
167 'menuID' => $menuID,
168 'originIsSystem' => 1,
169 'pageID' => $pageID,
170 'parentItemID' => $parentItemID,
171 'showOrder' => $this->getItemOrder($menuID, $parentItemID),
172 'title' => $this->getI18nValues($data['elements']['title']),
173 ];
174 }
175
176 /**
177 * Returns the id of the menu with the given identifier. If no such menu
178 * exists, `null` is returned.
179 *
180 * @param string $identifier
181 * @return null|int
182 */
183 protected function getMenuID($identifier)
184 {
185 $sql = "SELECT menuID
186 FROM wcf" . WCF_N . "_menu
187 WHERE identifier = ?";
188 $statement = WCF::getDB()->prepareStatement($sql, 1);
189 $statement->execute([$identifier]);
190
191 return $statement->fetchSingleColumn();
192 }
193
194 /**
195 * Returns the id of the page with the given identifier. If no such page
196 * exists, `null` is returned.
197 *
198 * @param string $identifier
199 * @return null|int
200 */
201 protected function getPageID($identifier)
202 {
203 $sql = "SELECT pageID
204 FROM wcf" . WCF_N . "_page
205 WHERE identifier = ?";
206 $statement = WCF::getDB()->prepareStatement($sql, 1);
207 $statement->execute([$identifier]);
208
209 return $statement->fetchSingleColumn();
210 }
211
212 /**
213 * @inheritDoc
214 */
215 protected function findExistingItem(array $data)
216 {
217 $sql = "SELECT *
218 FROM wcf" . WCF_N . "_menu_item
219 WHERE identifier = ?
220 AND packageID = ?";
221 $parameters = [
222 $data['identifier'],
223 $this->installation->getPackageID(),
224 ];
225
226 return [
227 'sql' => $sql,
228 'parameters' => $parameters,
229 ];
230 }
231
232 /**
233 * @inheritDoc
234 */
235 protected function import(array $row, array $data)
236 {
237 // updating menu items is not supported because all fields that could be modified
238 // would potentially overwrite changes made by the user
239 if (!empty($row)) {
240 return new MenuItem(null, $row);
241 }
242
243 return parent::import($row, $data);
244 }
245
246 /**
247 * Returns the show order for a new item that will append it to the current
248 * menu or parent item.
249 *
250 * @param int $menuID
251 * @param int $parentItemID
252 * @return int
253 */
254 protected function getItemOrder($menuID, $parentItemID = null)
255 {
256 $sql = "SELECT MAX(showOrder) AS showOrder
257 FROM wcf" . WCF_N . "_menu_item
258 WHERE " . ($parentItemID === null ? 'menuID' : 'parentItemID') . " = ?";
259 $statement = WCF::getDB()->prepareStatement($sql, 1);
260 $statement->execute([
261 $parentItemID === null ? $menuID : $parentItemID,
262 ]);
263
264 $row = $statement->fetchSingleRow();
265
266 return (!$row['showOrder']) ? 1 : $row['showOrder'] + 1;
267 }
268
269 /**
270 * @inheritDoc
271 * @since 3.1
272 */
273 public static function getSyncDependencies()
274 {
275 return ['language', 'menu', 'page'];
276 }
277
278 /**
279 * @inheritDoc
280 * @since 5.2
281 */
282 protected function addFormFields(IFormDocument $form)
283 {
284 $menuList = new MenuList();
285 $menuList->readObjects();
286
287 /** @var FormContainer $dataContainer */
288 $dataContainer = $form->getNodeById('data');
289
290 $dataContainer->appendChildren([
291 TextFormField::create('identifier')
292 ->label('wcf.acp.pip.menuItem.identifier')
293 ->description('wcf.acp.pip.menuItem.identifier.description')
294 ->required()
295 ->addValidator(FormFieldValidatorUtil::getDotSeparatedStringValidator(
296 'wcf.acp.pip.menuItem.identifier',
297 4
298 ))
299 ->addValidator(new FormFieldValidator('uniqueness', function (TextFormField $formField) {
300 if (
301 $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE
302 || $this->editedEntry->getAttribute('identifier') !== $formField->getValue()
303 ) {
304 $menuItemList = new MenuItemList();
305 $menuItemList->getConditionBuilder()->add('identifier = ?', [$formField->getValue()]);
306
307 if ($menuItemList->countObjects() > 0) {
308 $formField->addValidationError(
309 new FormFieldValidationError(
310 'notUnique',
311 'wcf.acp.pip.menuItem.identifier.error.notUnique'
312 )
313 );
314 }
315 }
316 })),
317
318 TitleFormField::create()
319 ->required()
320 ->i18n()
321 ->i18nRequired()
322 ->languageItemPattern('__NONE__'),
323
324 SingleSelectionFormField::create('menu')
325 ->label('wcf.acp.pip.menuItem.menu')
326 ->description('wcf.acp.pip.menuItem.menu.description')
327 ->required()
328 ->options(static function () use ($menuList) {
329 $options = [];
330 foreach ($menuList as $menu) {
331 $options[$menu->identifier] = $menu->identifier;
332 }
333
334 \asort($options);
335
336 return $options;
337 }),
338
339 RadioButtonFormField::create('linkType')
340 ->label('wcf.acp.pip.menuItem.linkType')
341 ->required()
342 ->options([
343 'internal' => 'wcf.acp.pip.menuItem.linkType.internal',
344 'external' => 'wcf.acp.pip.menuItem.linkType.external',
345 ])
346 ->value('internal'),
347
348 SingleSelectionFormField::create('menuItemPage')
349 ->objectProperty('page')
350 ->label('wcf.acp.pip.menuItem.page')
351 ->description('wcf.acp.pip.menuItem.page.description')
352 ->required()
353 ->filterable()
354 ->options(function () {
355 $pageNodeList = (new PageNodeTree())->getNodeList();
356
357 $packageIDs = \array_merge(
358 [$this->installation->getPackage()->packageID],
359 \array_keys($this->installation->getPackage()->getAllRequiredPackages())
360 );
361
362 $nestedOptions = [];
363
364 /** @var PageNode $pageNode */
365 foreach ($pageNodeList as $pageNode) {
366 if (\in_array($pageNode->packageID, $packageIDs)) {
367 $nestedOptions[] = [
368 'depth' => $pageNode->getDepth() - 1,
369 'label' => $pageNode->name,
370 'value' => $pageNode->identifier,
371 ];
372 }
373 }
374
375 return $nestedOptions;
376 }, true)
377 ->addDependency(
378 ValueFormFieldDependency::create('linkType')
379 ->fieldId('linkType')
380 ->values(['internal'])
381 ),
382
383 TextFormField::create('externalURL')
384 ->label('wcf.acp.pip.menuItem.externalURL')
385 ->description('wcf.acp.pip.menuItem.externalURL.description')
386 ->required()
387 ->i18n()
388 ->addDependency(
389 ValueFormFieldDependency::create('linkType')
390 ->fieldId('linkType')
391 ->values(['external'])
392 ),
393 ]);
394
395 /** @var SingleSelectionFormField $menuField */
396 $menuField = $form->getNodeById('menu');
397
398 foreach ($menuList as $menu) {
399 $dataContainer->insertBefore(
400 SingleSelectionFormField::create('parentMenuItem' . $menu->menuID)
401 ->objectProperty('parent')
402 ->label('wcf.acp.pip.menuItem.parentMenuItem')
403 ->options(function () use ($menu) {
404 $options = [
405 [
406 'depth' => 0,
407 'label' => 'wcf.global.noSelection',
408 'value' => '',
409 ],
410 ];
411
412 $packageIDs = \array_merge(
413 [$this->installation->getPackage()->packageID],
414 \array_keys($this->installation->getPackage()->getAllRequiredPackages())
415 );
416
417 /** @var MenuItemNode $menuItem */
418 foreach ($menu->getMenuItemNodeList() as $menuItem) {
419 if (\in_array($menuItem->packageID, $packageIDs)) {
420 $options[] = [
421 'depth' => $menuItem->getDepth() - 1,
422 'label' => $menuItem->identifier,
423 'value' => $menuItem->identifier,
424 ];
425 }
426 }
427
428 if (\count($options) === 1) {
429 return [];
430 }
431
432 return $options;
433 }, true)
434 ->addDependency(
435 ValueFormFieldDependency::create('menu')
436 ->field($menuField)
437 ->values([$menu->identifier])
438 ),
439 'linkType'
440 );
441 }
442 }
443
444 /**
445 * @inheritDoc
446 * @since 5.2
447 */
448 protected function fetchElementData(\DOMElement $element, $saveData)
449 {
450 $data = [
451 'identifier' => $element->getAttribute('identifier'),
452 'packageID' => $this->installation->getPackageID(),
453 'originIsSystem' => 1,
454 'title' => [],
455 ];
456
457 /** @var \DOMElement $title */
458 foreach ($element->getElementsByTagName('title') as $title) {
459 $data['title'][LanguageFactory::getInstance()->getLanguageByCode($title->getAttribute('language'))->languageID] = $title->nodeValue;
460 }
461
462 foreach (['externalURL', 'menu', 'page', 'parent'] as $optionalElementName) {
463 $optionalElement = $element->getElementsByTagName($optionalElementName)->item(0);
464 if ($optionalElement !== null) {
465 $data[$optionalElementName] = $optionalElement->nodeValue;
466 } elseif ($saveData) {
467 $data[$optionalElementName] = '';
468 }
469 }
470
471 if (!empty($data['parent'])) {
472 $menuItemList = new MenuItemList();
473 $menuItemList->getConditionBuilder()->add('identifier = ?', [$data['parent']]);
474 $menuItemList->getConditionBuilder()->add('packageID IN (?)', [
475 \array_merge(
476 [$this->installation->getPackage()->packageID],
477 \array_keys($this->installation->getPackage()->getAllRequiredPackages())
478 ),
479 ]);
480 $menuItemList->readObjects();
481
482 if (\count($menuItemList) === 1) {
483 if ($saveData) {
484 $data['menuID'] = $menuItemList->current()->menuID;
485 $data['parentItemID'] = $menuItemList->current()->itemID;
486
487 unset($data['menu'], $data['parent']);
488 } else {
489 $data['menu'] = (new Menu($menuItemList->current()->menuID))->identifier;
490 }
491 }
492 } elseif ($saveData) {
493 if (isset($data['menu'])) {
494 $data['menuID'] = $this->getMenuID($data['menu']);
495 unset($data['menu']);
496 }
497
498 $data['parentItemID'] = null;
499 }
500
501 if ($saveData && $this->editedEntry === null) {
502 // only set explicit showOrder when adding new menu item
503 $data['showOrder'] = $this->getItemOrder($data['menuID'], $data['parentItemID']);
504 }
505
506 if ($saveData) {
507 // updating menu items is not supported thus handling the title
508 // array causes issues
509 if ($this->editedEntry !== null) {
510 unset($data['title']);
511 } else {
512 $titles = [];
513 foreach ($data['title'] as $languageID => $title) {
514 $titles[LanguageFactory::getInstance()->getLanguage($languageID)->languageCode] = $title;
515 }
516
517 $data['title'] = $titles;
518 }
519
520 if (isset($data['page'])) {
521 $data['pageID'] = $this->getPageID($data['page']);
522 unset($data['page']);
523 }
524 }
525
526 return $data;
527 }
528
529 /**
530 * @inheritDoc
531 * @since 5.2
532 */
533 public function getElementIdentifier(\DOMElement $element)
534 {
535 return $element->getAttribute('identifier');
536 }
537
538 /**
539 * @inheritDoc
540 * @since 5.2
541 */
542 protected function setEntryListKeys(IDevtoolsPipEntryList $entryList)
543 {
544 $entryList->setKeys([
545 'identifier' => 'wcf.acp.pip.menuItem.identifier',
546 'menu' => 'wcf.acp.pip.menuItem.menu',
547 ]);
548 }
549
550 /**
551 * @inheritDoc
552 * @since 5.2
553 */
554 protected function prepareXmlElement(\DOMDocument $document, IFormDocument $form)
555 {
556 $formData = $form->getData();
557 $data = $formData['data'];
558
559 $menuItem = $document->createElement($this->tagName);
560
561 $menuItem->setAttribute('identifier', $data['identifier']);
562
563 if (!empty($data['parent'])) {
564 $menuItem->appendChild($document->createElement('parent', $data['parent']));
565 } elseif (!empty($data['menu'])) {
566 $menuItem->appendChild($document->createElement('menu', $data['menu']));
567 }
568
569 foreach ($formData['title_i18n'] as $languageID => $title) {
570 $title = $document->createElement('title', $this->getAutoCdataValue($title));
571 $title->setAttribute('language', LanguageFactory::getInstance()->getLanguage($languageID)->languageCode);
572
573 $menuItem->appendChild($title);
574 }
575
576 $this->appendElementChildren(
577 $menuItem,
578 [
579 'page' => '',
580 'externalURL' => '',
581 'showOrder' => null,
582 ],
583 $form
584 );
585
586 return $menuItem;
587 }
effc9389 588}