Add missing PIP GUI-related German language items
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / plugin / MenuPackageInstallationPlugin.class.php
1 <?php
2 namespace wcf\system\package\plugin;
3 use wcf\data\box\Box;
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;
29 use wcf\system\WCF;
30
31 /**
32 * Installs, updates and deletes menus.
33 *
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
38 * @since 3.0
39 */
40 class MenuPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin {
41 use TXmlGuiPackageInstallationPlugin;
42
43 /**
44 * box meta data per menu
45 * @var array
46 */
47 public $boxData = [];
48
49 /**
50 * visibility exceptions per box
51 * @var string[]
52 */
53 public $visibilityExceptions = [];
54
55 /**
56 * @inheritDoc
57 */
58 public $className = MenuEditor::class;
59
60 /**
61 * @inheritDoc
62 */
63 public $tagName = 'menu';
64
65 /**
66 * @inheritDoc
67 */
68 protected function handleDelete(array $items) {
69 $sql = "DELETE FROM wcf".WCF_N."_menu
70 WHERE identifier = ?
71 AND packageID = ?";
72 $statement = WCF::getDB()->prepareStatement($sql);
73
74 WCF::getDB()->beginTransaction();
75 foreach ($items as $item) {
76 $statement->execute([
77 $item['attributes']['identifier'],
78 $this->installation->getPackageID()
79 ]);
80 }
81 WCF::getDB()->commitTransaction();
82 }
83
84 /**
85 * @inheritDoc
86 * @throws SystemException
87 */
88 protected function getElement(\DOMXPath $xpath, array &$elements, \DOMElement $element) {
89 $nodeValue = $element->nodeValue;
90
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') . "'");
94 }
95
96 // <title> can occur multiple times using the `language` attribute
97 if (!isset($elements['title'])) $elements['title'] = [];
98
99 $elements['title'][$element->getAttribute('language')] = $element->nodeValue;
100 }
101 else if ($element->tagName === 'box') {
102 $elements['box'] = [];
103
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') . "')");
109 }
110
111 // <title> can occur multiple times using the `language` attribute
112 if (!isset($elements['box']['name'])) $elements['box']['name'] = [];
113
114 $elements['box']['name'][$element->getAttribute('language')] = $element->nodeValue;
115 }
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;
121 }
122 }
123 else {
124 $elements['box'][$child->tagName] = $child->nodeValue;
125 }
126 }
127 }
128 else {
129 $elements[$element->tagName] = $nodeValue;
130 }
131 }
132
133 /**
134 * @inheritDoc
135 */
136 protected function prepareImport(array $data) {
137 $identifier = $data['attributes']['identifier'];
138
139 if (!empty($data['elements']['box'])) {
140 $position = $data['elements']['box']['position'];
141
142 if ($identifier === 'com.woltlab.wcf.MainMenu') {
143 $position = 'mainMenu';
144 }
145 else if (!in_array($position, Box::$availableMenuPositions)) {
146 throw new SystemException("Unknown box position '{$position}' for menu box '{$identifier}'");
147 }
148
149 $this->boxData[$identifier] = [
150 'identifier' => $identifier,
151 'name' => $this->getI18nValues($data['elements']['title'], true),
152 'boxType' => 'menu',
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()
159 ];
160
161 if (!empty($data['elements']['box']['visibilityExceptions'])) {
162 $this->visibilityExceptions[$identifier] = $data['elements']['box']['visibilityExceptions'];
163 }
164
165 unset($data['elements']['box']);
166 }
167
168 return [
169 'identifier' => $identifier,
170 'title' => $this->getI18nValues($data['elements']['title']),
171 'originIsSystem' => 1
172 ];
173 }
174
175 /**
176 * @inheritDoc
177 */
178 protected function findExistingItem(array $data) {
179 $sql = "SELECT *
180 FROM wcf".WCF_N."_menu
181 WHERE identifier = ?
182 AND packageID = ?";
183 $parameters = [
184 $data['identifier'],
185 $this->installation->getPackageID()
186 ];
187
188 return [
189 'sql' => $sql,
190 'parameters' => $parameters
191 ];
192 }
193
194 /**
195 * @inheritDoc
196 */
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
200 if (!empty($row)) {
201 return new Menu(null, $row);
202 }
203
204 return parent::import($row, $data);
205 }
206
207 /**
208 * @inheritDoc
209 */
210 protected function postImport() {
211 if (empty($this->boxData)) return;
212
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()]);
217
218 $sql = "SELECT *
219 FROM wcf".WCF_N."_box
220 ".$conditions;
221 $statement = WCF::getDB()->prepareStatement($sql);
222 $statement->execute($conditions->getParameters());
223
224 /** @var Box[] $boxes */
225 $boxes = $statement->fetchObjects(Box::class, 'identifier');
226
227 // fetch all menus relevant
228 $menuList = new MenuList();
229 $menuList->getConditionBuilder()->add('identifier IN (?)', [array_keys($this->boxData)]);
230 $menuList->readObjects();
231
232 $menus = [];
233 foreach ($menuList as $menu) {
234 $menus[$menu->identifier] = $menu;
235 }
236
237 // handle visibility exceptions
238 $sql = "DELETE FROM wcf".WCF_N."_box_to_page
239 WHERE boxID = ?";
240 $deleteStatement = WCF::getDB()->prepareStatement($sql);
241 $sql = "INSERT IGNORE wcf".WCF_N."_box_to_page
242 (boxID, pageID, visible)
243 VALUES (?, ?, ?)";
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;
249 }
250
251 $box = null;
252 if (isset($boxes[$identifier])) {
253 $box = $boxes[$identifier];
254
255 // delete old visibility exceptions
256 $deleteStatement->execute([$box->boxID]);
257
258 // skip both 'identifier' and 'packageID' as these properties are immutable
259 unset($data['identifier']);
260 unset($data['packageID']);
261
262 $boxEditor = new BoxEditor($box);
263 $boxEditor->update($data);
264 }
265 else {
266 $box = BoxEditor::create($data);
267 }
268
269 // save visibility exceptions
270 if (!empty($this->visibilityExceptions[$identifier])) {
271 // get page ids
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);
280
281 // save page ids
282 foreach ($pageIDs as $pageID) {
283 $insertStatement->execute([$box->boxID, $pageID, $box->visibleEverywhere ? 0 : 1]);
284 }
285 }
286 }
287 }
288
289 /**
290 * @inheritDoc
291 * @since 3.1
292 */
293 public static function getSyncDependencies() {
294 return ['language'];
295 }
296
297 /**
298 * @inheritDoc
299 * @since 3.2
300 */
301 public function getAdditionalTemplateCode() {
302 return WCF::getTPL()->fetch('__menuPipGui');
303 }
304
305 /**
306 * @inheritDoc
307 * @since 3.2
308 */
309 protected function addFormFields(IFormDocument $form) {
310 /** @var FormContainer $dataContainer */
311 $dataContainer = $form->getNodeById('data');
312
313 $dataContainer->appendChildren([
314 TextFormField::create('identifier')
315 ->label('wcf.acp.pip.menu.identifier')
316 ->description('wcf.acp.pip.menu.identifier.description')
317 ->required()
318 ->addValidator(FormFieldValidatorUtil::getDotSeparatedStringValidator(
319 'wcf.acp.pip.menu.identifier',
320 4
321 ))
322 ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField $formField) {
323 if (
324 $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE ||
325 $this->editedEntry->getAttribute('identifier') !== $formField->getValue()
326 ) {
327 $menuList = new MenuList();
328 $menuList->getConditionBuilder()->add('identifier = ?', [$formField->getValue()]);
329
330 if ($menuList->countObjects() > 0) {
331 $formField->addValidationError(
332 new FormFieldValidationError(
333 'notUnique',
334 'wcf.acp.pip.menu.identifier.error.notUnique'
335 )
336 );
337 }
338 }
339 })),
340
341 TitleFormField::create()
342 ->required()
343 ->i18n()
344 ->i18nRequired()
345 ->languageItemPattern('__NONE__'),
346
347 BooleanFormField::create('createBox')
348 ->label('wcf.acp.pip.menu.createBox')
349 ->description('wcf.acp.pip.menu.createBox.description'),
350
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)),
355
356 BooleanFormField::create('boxShowHeader')
357 ->label('wcf.acp.pip.menu.boxShowHeader'),
358
359 BooleanFormField::create('boxVisibleEverywhere')
360 ->label('wcf.acp.pip.menu.boxVisibleEverywhere'),
361
362 MultipleSelectionFormField::create('boxVisibilityExceptions')
363 ->label('wcf.acp.pip.menu.boxVisibilityExceptions.hiddenEverywhere')
364 ->filterable()
365 ->options(function() {
366 $pageNodeList = (new PageNodeTree())->getNodeList();
367
368 $nestedOptions = [];
369 /** @var PageNode $pageNode */
370 foreach ($pageNodeList as $pageNode) {
371 $nestedOptions[] = [
372 'depth' => $pageNode->getDepth() - 1,
373 'label' => $pageNode->name,
374 'value' => $pageNode->identifier
375 ];
376 }
377
378 return $nestedOptions;
379 }, true),
380
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)
385 ]);
386
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')
392 ->field($createBox)
393 );
394 }
395
396 /** @var TextFormField $identifier */
397 $identifier = $form->getNodeById('identifier');
398 $form->getNodeById('boxPosition')->addDependency(
399 ValueFormFieldDependency::create('identifier')
400 ->field($identifier)
401 ->values(['com.woltlab.wcf.MainMenu'])
402 ->negate()
403 );
404 }
405
406 /**
407 * @inheritDoc
408 * @since 3.2
409 */
410 protected function fetchElementData(\DOMElement $element, $saveData) {
411 $data = [
412 'identifier' => $element->getAttribute('identifier'),
413 'packageID' => $this->installation->getPackageID(),
414 'title' => []
415 ];
416
417 /** @var \DOMElement $title */
418 foreach ($element->getElementsByTagName('title') as $title) {
419 $data['title'][LanguageFactory::getInstance()->getLanguageByCode($title->getAttribute('language'))->languageID] = $title->nodeValue;
420 }
421
422 $box = $element->getElementsByTagName('box')->item(0);
423 $boxData = [];
424 if ($box !== null) {
425 $boxData['position'] = $box->getElementsByTagName('position')->item(0)->nodeValue;
426
427 // work-around for unofficial position `mainMenu`
428 if ($data['identifier'] === 'com.woltlab.wcf.MainMenu' && !$saveData) {
429 unset($boxData['position']);
430 }
431
432 $showHeader = $element->getElementsByTagName('showHeader')->item(0);
433 if ($showHeader !== null) {
434 $boxData['showHeader'] = $showHeader->nodeValue;
435 }
436
437 $visibleEverywhere = $element->getElementsByTagName('visibleEverywhere')->item(0);
438 if ($visibleEverywhere !== null) {
439 $boxData['visibleEverywhere'] = $visibleEverywhere->nodeValue;
440 }
441
442 $cssClassName = $element->getElementsByTagName('cssClassName')->item(0);
443 if ($cssClassName !== null) {
444 $boxData['cssClassName'] = $cssClassName->nodeValue;
445 }
446
447 $visibilityExceptions = $element->getElementsByTagName('visibilityExceptions');
448 if ($visibilityExceptions->length > 0) {
449 $boxData['visibilityExceptions'] = [];
450
451 /** @var \DOMElement $visibilityException */
452 foreach ($visibilityExceptions as $visibilityException) {
453 $boxData['visibilityExceptions'] = $visibilityException->nodeValue;
454 }
455 }
456 }
457
458 if ($saveData) {
459 if (!empty($boxData)) {
460 $this->boxData[$data['identifier']] = [
461 'identifier' => $data['identifier'],
462 'name' => $this->getI18nValues($data['title'], true),
463 'boxType' => 'menu',
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()
470 ];
471
472 if (!empty($boxData['visibilityExceptions'])) {
473 $this->visibilityExceptions[$data['identifier']] = $boxData['visibilityExceptions'];
474 }
475 }
476
477 // update menus is not supported thus handling the title
478 // array causes issues
479 if ($this->editedEntry !== null) {
480 unset($data['title']);
481 }
482 else {
483 $titles = [];
484 foreach ($data['title'] as $languageID => $title) {
485 $titles[LanguageFactory::getInstance()->getLanguage($languageID)->languageCode] = $title;
486 }
487
488 $data['title'] = $titles;
489 }
490 }
491 else {
492 $data['createBox'] = $box !== null;
493
494 foreach ($boxData as $key => $value) {
495 $data['box' . ucfirst($key)] = $value;
496 }
497 }
498
499 return $data;
500 }
501
502 /**
503 * @inheritDoc
504 * @since 3.2
505 */
506 public function getElementIdentifier(\DOMElement $element) {
507 return $element->getAttribute('identifier');
508 }
509
510 /**
511 * @inheritDoc
512 * @since 3.2
513 */
514 protected function setEntryListKeys(IDevtoolsPipEntryList $entryList) {
515 $entryList->setKeys([
516 'identifier' => 'wcf.acp.pip.menu.identifier'
517 ]);
518 }
519
520 /**
521 * @inheritDoc
522 * @since 3.2
523 */
524 protected function prepareXmlElement(\DOMDocument $document, IFormDocument $form) {
525 $formData = $form->getData();
526
527 if ($formData['data']['identifier'] === 'com.woltlab.wcf.MainMenu') {
528 $formData['data']['boxPosition'] = 'mainMenu';
529 }
530
531 $menu = $document->createElement($this->tagName);
532 $menu->setAttribute('identifier', $formData['data']['identifier']);
533
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);
537
538 $menu->appendChild($title);
539 }
540
541 if ($formData['data']['createBox']) {
542 $box = $document->createElement('box');
543
544 $box->appendChild($document->createElement('position', $formData['data']['boxPosition']));
545
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]));
550 }
551 }
552
553 if (!empty($formData['data']['boxVisibilityExceptions'])) {
554 $visibilityExceptions = $box->appendChild($document->createElement('visibilityExceptions'));
555
556 foreach ($formData['data']['boxVisibilityExceptions'] as $pageIdentifier) {
557 $visibilityExceptions->appendChild($document->createElement('page', $pageIdentifier));
558 }
559 }
560
561 $menu->appendChild($box);
562 }
563
564 return $menu;
565 }
566 }