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