Fix XSD filename in newly created ACL option PIP files
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / plugin / ACLOptionPackageInstallationPlugin.class.php
1 <?php
2 declare(strict_types=1);
3 namespace wcf\system\package\plugin;
4 use wcf\data\acl\option\ACLOption;
5 use wcf\data\acl\option\ACLOptionEditor;
6 use wcf\data\acl\option\ACLOptionList;
7 use wcf\data\acl\option\category\ACLOptionCategory;
8 use wcf\data\acl\option\category\ACLOptionCategoryEditor;
9 use wcf\data\acl\option\category\ACLOptionCategoryList;
10 use wcf\data\object\type\ObjectTypeCache;
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\field\dependency\ValueFormFieldDependency;
16 use wcf\system\form\builder\field\SingleSelectionFormField;
17 use wcf\system\form\builder\field\TextFormField;
18 use wcf\system\form\builder\field\validation\FormFieldValidationError;
19 use wcf\system\form\builder\field\validation\FormFieldValidator;
20 use wcf\system\form\builder\IFormDocument;
21 use wcf\system\WCF;
22
23 /**
24 * This PIP installs, updates or deletes acl options.
25 *
26 * @author Marcel Werk
27 * @copyright 2001-2018 WoltLab GmbH
28 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
29 * @package WoltLabSuite\Core\System\Package\Plugin
30 */
31 class ACLOptionPackageInstallationPlugin extends AbstractOptionPackageInstallationPlugin implements IGuiPackageInstallationPlugin {
32 use TXmlGuiPackageInstallationPlugin;
33
34 /**
35 * @inheritDoc
36 */
37 public $className = ACLOptionEditor::class;
38
39 /**
40 * list of loaded acl object type ids sorted by their option type name
41 * @var integer[]
42 */
43 protected $optionTypeIDs = [];
44
45 /**
46 * @inheritDoc
47 */
48 public $tableName = 'acl_option';
49
50 /**
51 * @inheritDoc
52 */
53 public $tagName = 'option';
54
55 /**
56 * @inheritDoc
57 */
58 protected function deleteItems(\DOMXPath $xpath) {
59 // delete options
60 $elements = $xpath->query('/ns:data/ns:delete/ns:option');
61 $options = [];
62
63 /** @var \DOMElement $element */
64 foreach ($elements as $element) {
65 $options[] = [
66 'name' => $element->getAttribute('name'),
67 'objectType' => $element->getElementsByTagName('objecttype')->item(0)->nodeValue
68 ];
69 }
70
71 if (!empty($options)) {
72 $sql = "DELETE FROM " . $this->application . WCF_N . "_" . $this->tableName. "
73 WHERE optionName = ?
74 AND objectTypeID = ?
75 AND packageID = ?";
76 $statement = WCF::getDB()->prepareStatement($sql);
77
78 foreach ($options as $option) {
79 $statement->execute([
80 $option['name'],
81 $this->getObjectTypeID($option['objectType']),
82 $this->installation->getPackageID()
83 ]);
84 }
85 }
86
87 // delete categories
88 $elements = $xpath->query('/ns:data/ns:delete/ns:optioncategory');
89 $categories = [];
90
91 /** @var \DOMElement $element */
92 foreach ($elements as $element) {
93 $categories[] = [
94 'name' => $element->getAttribute('name'),
95 'objectType' => $element->getElementsByTagName('objecttype')->item(0)->nodeValue
96 ];
97 }
98
99 if (!empty($categories)) {
100 // delete options for given categories
101 $sql = "DELETE FROM " . $this->application . WCF_N . "_" . $this->tableName. "
102 WHERE categoryName = ?
103 AND objectTypeID = ?
104 AND packageID = ?";
105 $statement = WCF::getDB()->prepareStatement($sql);
106 foreach ($categories as $category) {
107 $statement->execute([
108 $category['name'],
109 $this->getObjectTypeID($category['objectType']),
110 $this->installation->getPackageID()
111 ]);
112 }
113
114 // delete categories
115 $sql = "DELETE FROM " . $this->application . WCF_N . "_" . $this->tableName. "_category
116 WHERE categoryName = ?
117 AND objectTypeID = ?
118 AND packageID = ?";
119 $statement = WCF::getDB()->prepareStatement($sql);
120
121 foreach ($categories as $category) {
122 $statement->execute([
123 $category['name'],
124 $this->getObjectTypeID($category['objectType']),
125 $this->installation->getPackageID()
126 ]);
127 }
128 }
129 }
130
131 /**
132 * @inheritDoc
133 */
134 protected function importCategories(\DOMXPath $xpath) {
135 $elements = $xpath->query('/ns:data/ns:import/ns:categories/ns:category');
136
137 /** @var \DOMElement $element */
138 foreach ($elements as $element) {
139 $data = ['categoryName' => $element->getAttribute('name')];
140
141 // get child elements
142 $children = $xpath->query('child::*', $element);
143 foreach ($children as $child) {
144 $data[$child->tagName] = $child->nodeValue;
145 }
146
147 $this->saveCategory($data);
148 }
149 }
150
151 /**
152 * @inheritDoc
153 */
154 protected function saveCategory($category) {
155 $objectTypeID = $this->getObjectTypeID($category['objecttype']);
156
157 // search existing category
158 $sql = "SELECT categoryID
159 FROM wcf".WCF_N."_".$this->tableName."_category
160 WHERE categoryName = ?
161 AND objectTypeID = ?
162 AND packageID = ?";
163 $statement = WCF::getDB()->prepareStatement($sql);
164 $statement->execute([
165 $category['categoryName'],
166 $objectTypeID,
167 $this->installation->getPackageID()
168 ]);
169 $row = $statement->fetchArray();
170 if (!$row) {
171 // insert new category
172 $sql = "INSERT INTO wcf".WCF_N."_".$this->tableName."_category
173 (packageID, objectTypeID, categoryName)
174 VALUES (?, ?, ?)";
175 $statement = WCF::getDB()->prepareStatement($sql);
176 $statement->execute([
177 $this->installation->getPackageID(),
178 $objectTypeID,
179 $category['categoryName']
180 ]);
181 }
182 }
183
184 /**
185 * Imports options.
186 *
187 * @param \DOMXPath $xpath
188 * @throws SystemException
189 */
190 protected function importOptions(\DOMXPath $xpath) {
191 $elements = $xpath->query('/ns:data/ns:import/ns:options/ns:option');
192
193 /** @var \DOMElement $element */
194 foreach ($elements as $element) {
195 $data = [];
196 $children = $xpath->query('child::*', $element);
197 foreach ($children as $child) {
198 $data[$child->tagName] = $child->nodeValue;
199 }
200
201 $objectTypeID = $this->getObjectTypeID($data['objecttype']);
202
203 // validate category name
204 if (isset($data['categoryname'])) {
205 $sql = "SELECT COUNT(categoryID)
206 FROM wcf".WCF_N."_".$this->tableName."_category
207 WHERE categoryName = ?
208 AND objectTypeID = ?";
209 $statement = WCF::getDB()->prepareStatement($sql);
210 $statement->execute([
211 $data['categoryname'],
212 $objectTypeID
213 ]);
214
215 if (!$statement->fetchSingleColumn()) {
216 throw new SystemException("unknown category '".$data['categoryname']."' for acl object type '".$data['objecttype']."' given");
217 }
218 }
219
220 $data = [
221 'categoryName' => isset($data['categoryname']) ? $data['categoryname'] : '',
222 'optionName' => $element->getAttribute('name'),
223 'objectTypeID' => $objectTypeID
224 ];
225
226 // check for option existence
227 $sql = "SELECT optionID
228 FROM wcf".WCF_N."_".$this->tableName."
229 WHERE optionName = ?
230 AND objectTypeID = ?
231 AND packageID = ?";
232 $statement = WCF::getDB()->prepareStatement($sql);
233 $statement->execute([
234 $data['optionName'],
235 $data['objectTypeID'],
236 $this->installation->getPackageID()
237 ]);
238 $row = $statement->fetchArray();
239 if (!$row) {
240 $sql = "INSERT INTO wcf".WCF_N."_".$this->tableName."
241 (packageID, objectTypeID, optionName, categoryName)
242 VALUES (?, ?, ?, ?)";
243 $statement = WCF::getDB()->prepareStatement($sql);
244 $statement->execute([
245 $this->installation->getPackageID(),
246 $data['objectTypeID'],
247 $data['optionName'],
248 $data['categoryName']
249 ]);
250 }
251 else {
252 $sql = "UPDATE wcf".WCF_N."_".$this->tableName."
253 SET categoryName = ?
254 WHERE optionID = ?";
255 $statement = WCF::getDB()->prepareStatement($sql);
256 $statement->execute([
257 $data['categoryName'],
258 $row['optionID']
259 ]);
260 }
261 }
262 }
263
264 /**
265 * @inheritDoc
266 */
267 protected function saveOption($option, $categoryName, $existingOptionID = 0) {
268 // does nothing
269 }
270
271 /**
272 * Returns the object type id of the acl option type with the given name
273 * or throws a SystemException if no such option type exists.
274 *
275 * @param string $optionType
276 * @return integer
277 * @throws SystemException
278 */
279 protected function getObjectTypeID($optionType) {
280 if (!isset($this->optionTypeIDs[$optionType])) {
281 $sql = "SELECT objectTypeID
282 FROM wcf".WCF_N."_object_type
283 WHERE objectType = ?
284 AND definitionID IN (
285 SELECT definitionID
286 FROM wcf".WCF_N."_object_type_definition
287 WHERE definitionName = 'com.woltlab.wcf.acl'
288 )";
289 $statement = WCF::getDB()->prepareStatement($sql, 1);
290 $statement->execute([$optionType]);
291 $objectTypeID = $statement->fetchSingleColumn();
292 if ($objectTypeID === false) {
293 throw new SystemException("unknown object type '".$optionType."' given");
294 }
295
296 $this->optionTypeIDs[$optionType] = $objectTypeID;
297 }
298
299 return $this->optionTypeIDs[$optionType];
300 }
301
302 /**
303 * @inheritDoc
304 * @since 3.0
305 */
306 public static function getDefaultFilename() {
307 return 'aclOption.xml';
308 }
309
310 /**
311 * @inheritDoc
312 * @since 3.1
313 */
314 public static function getSyncDependencies() {
315 return ['objectType'];
316 }
317
318 /**
319 * @inheritDoc
320 * @since 3.2
321 */
322 public function addFormFields(IFormDocument $form) {
323 $objectTypes = [];
324
325 $requiredPackageIDs = array_merge(
326 [$this->installation->getPackageID()],
327 array_keys($this->installation->getPackage()->getAllRequiredPackages())
328 );
329
330 foreach (ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.acl') as $objectType) {
331 if (in_array($objectType->packageID, $requiredPackageIDs)) {
332 $objectTypes[$objectType->objectType] = $objectType->objectType;
333 }
334 }
335
336 asort($objectTypes);
337
338 switch ($this->entryType) {
339 case 'categories';
340 $nameFormField = TextFormField::create('name')
341 ->label('wcf.acp.pip.aclOption.categories.name')
342 ->description('wcf.acp.pip.aclOption.categories.name.description')
343 ->required()
344 ->addValidator(ObjectTypePackageInstallationPlugin::getObjectTypeAlikeValueValidator('wcf.acp.pip.aclOption.categories.name', 2));
345 break;
346
347 case 'options':
348 $nameFormField = TextFormField::create('name')
349 ->label('wcf.acp.pip.aclOption.options.name')
350 ->description('wcf.acp.pip.aclOption.options.name.description')
351 ->required()
352 ->addValidator(new FormFieldValidator('format', function(TextFormField $formField) {
353 if (!preg_match('~[a-z][A-z]+~', $formField->getValue())) {
354 $formField->addValidationError(
355 new FormFieldValidationError(
356 'format',
357 'wcf.acp.pip.aclOption.options.name.error.format'
358 )
359 );
360 }
361 }));
362 break;
363
364 default:
365 throw new \LogicException('Unreachable');
366 }
367
368 $entryType = $this->entryType;
369 $objectTypeFormField = SingleSelectionFormField::create('objectType')
370 ->objectProperty('objecttype')
371 ->label('wcf.acp.pip.aclOption.objectType')
372 ->description('wcf.acp.pip.aclOption.objectType.' . $this->entryType . '.description')
373 ->options($objectTypes)
374 ->required()
375 ->addValidator(new FormFieldValidator('nameUniqueness', function(SingleSelectionFormField $formField) use($entryType) {
376 /** @var TextFormField $nameField */
377 $nameField = $formField->getDocument()->getNodeById('name');
378
379 if (
380 $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE ||
381 $this->editedEntry->getAttribute('name') !== $nameField->getValue()
382 ) {
383 switch ($entryType) {
384 case 'categories':
385 $categoryList = new ACLOptionCategoryList();
386 $categoryList->getConditionBuilder()->add('categoryName = ?', [
387 $nameField->getValue()
388 ]);
389 $categoryList->getConditionBuilder()->add('objectTypeID = ?', [
390 ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.acl', $formField->getValue())->objectTypeID
391 ]);
392
393 if ($categoryList->countObjects() > 0) {
394 $nameField->addValidationError(
395 new FormFieldValidationError(
396 'notUnique',
397 'wcf.acp.pip.aclOption.objectType.' . $entryType . '.error.notUnique'
398 )
399 );
400 }
401 break;
402
403 case 'options':
404 $optionList = new ACLOptionList();
405 $optionList->getConditionBuilder()->add('optionName = ?', [
406 $nameField->getValue()
407 ]);
408 $optionList->getConditionBuilder()->add('objectTypeID = ?', [
409 ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.acl', $formField->getValue())->objectTypeID
410 ]);
411
412 if ($optionList->countObjects() > 0) {
413 $nameField->addValidationError(
414 new FormFieldValidationError(
415 'notUnique',
416 'wcf.acp.pip.aclOption.objectType.' . $entryType . '.error.notUnique'
417 )
418 );
419 }
420 break;
421 }
422 }
423 }));
424
425 $form->getNodeById('data')->appendChildren([$nameFormField, $objectTypeFormField]);
426
427 if ($this->entryType === 'options') {
428 $categoryList = new ACLOptionCategoryList();
429 $categoryList->getConditionBuilder()->add('packageID IN (?)', [$requiredPackageIDs]);
430 $categoryList->sqlOrderBy = 'categoryName ASC';
431 $categoryList->readObjects();
432
433 $categories = [];
434 foreach ($categoryList as $category) {
435 if (!isset($categories[$category->objectTypeID])) {
436 $categories[$category->objectTypeID] = [];
437 }
438
439 $categories[$category->objectTypeID][$category->categoryName] = $category->categoryName;
440 }
441
442 foreach ($objectTypes as $objectType) {
443 $objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.acl', $objectType);
444
445 if (isset($categories[$objectTypeID])) {
446 $categoryNameField = SingleSelectionFormField::create('categoryName_' . $objectTypeID)
447 ->objectProperty('categoryname')
448 ->label('wcf.acp.pip.aclOption.options.categoryName')
449 ->description('wcf.acp.pip.aclOption.options.categoryName.description')
450 ->options(['' => 'wcf.global.noSelection'] + $categories[$objectTypeID]);
451
452 $categoryNameField->addDependency(
453 ValueFormFieldDependency::create('objectType')
454 ->field($objectTypeFormField)
455 ->values([$objectType])
456 );
457
458 $form->getNodeById('data')->appendChild($categoryNameField);
459 }
460 }
461 }
462 }
463
464 /**
465 * @inheritDoc
466 * @since 3.2
467 */
468 public function getEntryTypes(): array {
469 return ['options', 'categories'];
470 }
471
472 /**
473 * @inheritDoc
474 * @since 3.2
475 */
476 protected function getElementData(\DOMElement $element): array {
477 $data = [
478 'name' => $element->getAttribute('name'),
479 'packageID' => $this->installation->getPackage()->packageID,
480 'objectType' => $element->getElementsByTagName('objecttype')->item(0)->nodeValue
481 ];
482
483 if ($this->entryType === 'options') {
484 $categoryName = $element->getElementsByTagName('categoryname')->item(0);
485 if ($categoryName !== null) {
486 $data['categoryName'] = $categoryName->nodeValue;
487 }
488 }
489
490 return $data;
491 }
492
493 /**
494 * @inheritDoc
495 * @since 3.2
496 */
497 public function getElementIdentifier(\DOMElement $element): string {
498 $elementData = $this->getElementData($element);
499
500 return sha1($elementData['objectType'] . '/' . $elementData['name']);
501 }
502
503 /**
504 * @inheritDoc
505 * @since 3.2
506 */
507 protected function getXsdFilename(): string {
508 return 'aclOption';
509 }
510
511 /**
512 * @inheritDoc
513 * @since 3.2
514 */
515 protected function setEntryListKeys(IDevtoolsPipEntryList $entryList) {
516 $entryList->setKeys([
517 'name' => 'wcf.acp.pip.aclOption.' . $this->entryType . '.name',
518 'objectType' => 'wcf.acp.pip.aclOption.objectType'
519 ]);
520 }
521
522 /**
523 * @inheritDoc
524 * @since 3.2
525 */
526 protected function saveObject(\DOMElement $newElement, \DOMElement $oldElement = null) {
527 if ($oldElement === null) {
528 $xpath = $this->getProjectXml()->xpath();
529
530 switch ($this->entryType) {
531 case 'categories':
532 $this->importCategories($xpath);
533 break;
534
535 case 'options':
536 $this->importOptions($xpath);
537 break;
538
539 default:
540 throw new \LogicException('Unreachable');
541 }
542 }
543 else {
544 $oldData = $this->getElementData($oldElement);
545 $newData = $this->getElementData($newElement);
546
547 switch ($this->entryType) {
548 case 'categories':
549 $sql = "SELECT *
550 FROM wcf" . WCF_N . "_acl_option_category
551 WHERE categoryName = ?
552 AND objectTypeID = ?
553 AND packageID = ?";
554 $statement = WCF::getDB()->prepareStatement($sql);
555 $statement->execute([
556 $oldData['name'],
557 ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.acl', $oldData['objectType']),
558 $oldData['packageID']
559 ]);
560 (new ACLOptionCategoryEditor($statement->fetchObject(ACLOptionCategory::class)))->update([
561 'categoryNameName' => $newData['name'],
562 'objectTypeID' => ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.acl', $newData['objectType'])
563 ]);
564
565 break;
566
567 case 'options':
568 $sql = "SELECT *
569 FROM wcf" . WCF_N . "_acl_option
570 WHERE optionName = ?
571 AND objectTypeID = ?
572 AND packageID = ?";
573 $statement = WCF::getDB()->prepareStatement($sql);
574 $statement->execute([
575 $oldData['name'],
576 ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.acl', $oldData['objectType']),
577 $oldData['packageID']
578 ]);
579 (new ACLOptionEditor($statement->fetchObject(ACLOption::class)))->update([
580 'objectTypeID' => ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.acl', $newData['objectType']),
581 'optionName' => $newData['name'],
582 'categoryName' => $newData['categoryname'] ?? ''
583 ]);
584
585 break;
586
587 default:
588 throw new \LogicException('Unreachable');
589 }
590 }
591 }
592
593 /**
594 * @inheritDoc
595 * @since 3.2
596 */
597 protected function sortDocument(\DOMDocument $document) {
598 $this->sortImportDelete($document);
599
600 // `<categories>` before `<options>`
601 $compareFunction = function(\DOMElement $element1, \DOMElement $element2) {
602 if ($element1->nodeName === 'categories') {
603 return -1;
604 }
605 else if ($element2->nodeName === 'categories') {
606 return 1;
607 }
608
609 return 0;
610 };
611
612 $this->sortChildNodes($document->getElementsByTagName('import'), $compareFunction);
613 $this->sortChildNodes($document->getElementsByTagName('delete'), $compareFunction);
614
615 $compareFunction = function(\DOMElement $element1, \DOMElement $element2) {
616 $objectType1 = $element1->getElementsByTagName('objecttype')->item(0)->nodeValue;
617 $objectType2 = $element2->getElementsByTagName('objecttype')->item(0)->nodeValue;
618
619 if ($objectType1 !== $objectType2) {
620 return strcmp($objectType1, $objectType2);
621 }
622
623 if ($element1->nodeName === 'option') {
624 $categoryName1 = $element1->getElementsByTagName('categoryname')->item(0);
625 $categoryName2 = $element2->getElementsByTagName('categoryname')->item(0);
626
627 if ($categoryName1 !== null) {
628 // both categories specified
629 if ($categoryName2 !== null) {
630 if ($categoryName1->nodeValue !== $categoryName2->nodeValue) {
631 return strcmp($categoryName1->nodeValue, $categoryName2->nodeValue);
632 }
633 }
634 // only first category specified
635 else {
636 return 1;
637 }
638 }
639 // only second category specified
640 else if ($categoryName2 !== null) {
641 return -1;
642 }
643 }
644
645 return strcmp(
646 $element1->getAttribute('name'),
647 $element2->getAttribute('name')
648 );
649 };
650
651 $this->sortChildNodes($document->getElementsByTagName('categories'), $compareFunction);
652 $this->sortChildNodes($document->getElementsByTagName('options'), $compareFunction);
653 }
654
655 /**
656 * @inheritDoc
657 * @since 3.2
658 */
659 protected function writeEntry(\DOMDocument $document, IFormDocument $form): \DOMElement {
660 $formData = $form->getData()['data'];
661
662 switch ($this->entryType) {
663 case 'categories':
664 $category = $document->createElement('category');
665 $category->setAttribute('name', $formData['name']);
666
667 $category->appendChild($document->createElement('objecttype', $formData['objecttype']));
668
669 $import = $document->getElementsByTagName('import')->item(0);
670 $categories = $import->getElementsByTagName('categories')->item(0);
671 if ($categories === null) {
672 $categories = $document->createElement('categories');
673 $import->appendChild($categories);
674 }
675
676 $categories->appendChild($category);
677
678 return $category;
679
680 case 'options':
681 $option = $document->createElement('option');
682 $option->setAttribute('name', $formData['name']);
683
684 $option->appendChild($document->createElement('objecttype', $formData['objecttype']));
685
686 if (isset($formData['categoryname'])) {
687 $option->appendChild($document->createElement('categoryname', $formData['categoryname']));
688 }
689
690 $import = $document->getElementsByTagName('import')->item(0);
691 $options = $import->getElementsByTagName('options')->item(0);
692 if ($options === null) {
693 $options = $document->createElement('options');
694 $import->appendChild($options);
695 }
696
697 $options->appendChild($option);
698
699 return $option;
700 }
701
702 throw new \LogicException('Unreachable');
703 }
704 }