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
;
24 * This PIP installs, updates or deletes acl options.
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
31 class ACLOptionPackageInstallationPlugin
extends AbstractOptionPackageInstallationPlugin
implements IGuiPackageInstallationPlugin
{
32 use TXmlGuiPackageInstallationPlugin
;
37 public $className = ACLOptionEditor
::class;
40 * list of loaded acl object type ids sorted by their option type name
43 protected $optionTypeIDs = [];
48 public $tableName = 'acl_option';
53 public $tagName = 'option';
58 protected function deleteItems(\DOMXPath
$xpath) {
60 $elements = $xpath->query('/ns:data/ns:delete/ns:option');
63 /** @var \DOMElement $element */
64 foreach ($elements as $element) {
66 'name' => $element->getAttribute('name'),
67 'objectType' => $element->getElementsByTagName('objecttype')->item(0)->nodeValue
71 if (!empty($options)) {
72 $sql = "DELETE FROM " . $this->application
. WCF_N
. "_" . $this->tableName
. "
76 $statement = WCF
::getDB()->prepareStatement($sql);
78 foreach ($options as $option) {
81 $this->getObjectTypeID($option['objectType']),
82 $this->installation
->getPackageID()
88 $elements = $xpath->query('/ns:data/ns:delete/ns:optioncategory');
91 /** @var \DOMElement $element */
92 foreach ($elements as $element) {
94 'name' => $element->getAttribute('name'),
95 'objectType' => $element->getElementsByTagName('objecttype')->item(0)->nodeValue
99 if (!empty($categories)) {
100 // delete options for given categories
101 $sql = "DELETE FROM " . $this->application
. WCF_N
. "_" . $this->tableName
. "
102 WHERE categoryName = ?
105 $statement = WCF
::getDB()->prepareStatement($sql);
106 foreach ($categories as $category) {
107 $statement->execute([
109 $this->getObjectTypeID($category['objectType']),
110 $this->installation
->getPackageID()
115 $sql = "DELETE FROM " . $this->application
. WCF_N
. "_" . $this->tableName
. "_category
116 WHERE categoryName = ?
119 $statement = WCF
::getDB()->prepareStatement($sql);
121 foreach ($categories as $category) {
122 $statement->execute([
124 $this->getObjectTypeID($category['objectType']),
125 $this->installation
->getPackageID()
134 protected function importCategories(\DOMXPath
$xpath) {
135 $elements = $xpath->query('/ns:data/ns:import/ns:categories/ns:category');
137 /** @var \DOMElement $element */
138 foreach ($elements as $element) {
139 $data = ['categoryName' => $element->getAttribute('name')];
141 // get child elements
142 $children = $xpath->query('child::*', $element);
143 foreach ($children as $child) {
144 $data[$child->tagName
] = $child->nodeValue
;
147 $this->saveCategory($data);
154 protected function saveCategory($category) {
155 $objectTypeID = $this->getObjectTypeID($category['objecttype']);
157 // search existing category
158 $sql = "SELECT categoryID
159 FROM wcf".WCF_N
."_".$this->tableName
."_category
160 WHERE categoryName = ?
163 $statement = WCF
::getDB()->prepareStatement($sql);
164 $statement->execute([
165 $category['categoryName'],
167 $this->installation
->getPackageID()
169 $row = $statement->fetchArray();
171 // insert new category
172 $sql = "INSERT INTO wcf".WCF_N
."_".$this->tableName
."_category
173 (packageID, objectTypeID, categoryName)
175 $statement = WCF
::getDB()->prepareStatement($sql);
176 $statement->execute([
177 $this->installation
->getPackageID(),
179 $category['categoryName']
187 * @param \DOMXPath $xpath
188 * @throws SystemException
190 protected function importOptions(\DOMXPath
$xpath) {
191 $elements = $xpath->query('/ns:data/ns:import/ns:options/ns:option');
193 /** @var \DOMElement $element */
194 foreach ($elements as $element) {
196 $children = $xpath->query('child::*', $element);
197 foreach ($children as $child) {
198 $data[$child->tagName
] = $child->nodeValue
;
201 $objectTypeID = $this->getObjectTypeID($data['objecttype']);
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'],
215 if (!$statement->fetchSingleColumn()) {
216 throw new SystemException("unknown category '".$data['categoryname']."' for acl object type '".$data['objecttype']."' given");
221 'categoryName' => isset($data['categoryname']) ?
$data['categoryname'] : '',
222 'optionName' => $element->getAttribute('name'),
223 'objectTypeID' => $objectTypeID
226 // check for option existence
227 $sql = "SELECT optionID
228 FROM wcf".WCF_N
."_".$this->tableName
."
232 $statement = WCF
::getDB()->prepareStatement($sql);
233 $statement->execute([
235 $data['objectTypeID'],
236 $this->installation
->getPackageID()
238 $row = $statement->fetchArray();
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'],
248 $data['categoryName']
252 $sql = "UPDATE wcf".WCF_N
."_".$this->tableName
."
255 $statement = WCF
::getDB()->prepareStatement($sql);
256 $statement->execute([
257 $data['categoryName'],
267 protected function saveOption($option, $categoryName, $existingOptionID = 0) {
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.
275 * @param string $optionType
277 * @throws SystemException
279 protected function getObjectTypeID($optionType) {
280 if (!isset($this->optionTypeIDs
[$optionType])) {
281 $sql = "SELECT objectTypeID
282 FROM wcf".WCF_N
."_object_type
284 AND definitionID IN (
286 FROM wcf".WCF_N
."_object_type_definition
287 WHERE definitionName = 'com.woltlab.wcf.acl'
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");
296 $this->optionTypeIDs
[$optionType] = $objectTypeID;
299 return $this->optionTypeIDs
[$optionType];
306 public static function getDefaultFilename() {
307 return 'aclOption.xml';
314 public static function getSyncDependencies() {
315 return ['objectType'];
322 public function addFormFields(IFormDocument
$form) {
325 $requiredPackageIDs = array_merge(
326 [$this->installation
->getPackageID()],
327 array_keys($this->installation
->getPackage()->getAllRequiredPackages())
330 foreach (ObjectTypeCache
::getInstance()->getObjectTypes('com.woltlab.wcf.acl') as $objectType) {
331 if (in_array($objectType->packageID
, $requiredPackageIDs)) {
332 $objectTypes[$objectType->objectType
] = $objectType->objectType
;
338 switch ($this->entryType
) {
340 $nameFormField = TextFormField
::create('name')
341 ->label('wcf.acp.pip.aclOption.categories.name')
342 ->description('wcf.acp.pip.aclOption.categories.name.description')
344 ->addValidator(ObjectTypePackageInstallationPlugin
::getObjectTypeAlikeValueValidator('wcf.acp.pip.aclOption.categories.name', 2));
348 $nameFormField = TextFormField
::create('name')
349 ->label('wcf.acp.pip.aclOption.options.name')
350 ->description('wcf.acp.pip.aclOption.options.name.description')
352 ->addValidator(new FormFieldValidator('format', function(TextFormField
$formField) {
353 if (!preg_match('~[a-z][A-z]+~', $formField->getValue())) {
354 $formField->addValidationError(
355 new FormFieldValidationError(
357 'wcf.acp.pip.aclOption.options.name.error.format'
365 throw new \
LogicException('Unreachable');
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)
375 ->addValidator(new FormFieldValidator('nameUniqueness', function(SingleSelectionFormField
$formField) use($entryType) {
376 /** @var TextFormField $nameField */
377 $nameField = $formField->getDocument()->getNodeById('name');
380 $formField->getDocument()->getFormMode() === IFormDocument
::FORM_MODE_CREATE ||
381 $this->editedEntry
->getAttribute('name') !== $nameField->getValue()
383 switch ($entryType) {
385 $categoryList = new ACLOptionCategoryList();
386 $categoryList->getConditionBuilder()->add('categoryName = ?', [
387 $nameField->getValue()
389 $categoryList->getConditionBuilder()->add('objectTypeID = ?', [
390 ObjectTypeCache
::getInstance()->getObjectTypeByName('com.woltlab.wcf.acl', $formField->getValue())->objectTypeID
393 if ($categoryList->countObjects() > 0) {
394 $nameField->addValidationError(
395 new FormFieldValidationError(
397 'wcf.acp.pip.aclOption.objectType.' . $entryType . '.error.notUnique'
404 $optionList = new ACLOptionList();
405 $optionList->getConditionBuilder()->add('optionName = ?', [
406 $nameField->getValue()
408 $optionList->getConditionBuilder()->add('objectTypeID = ?', [
409 ObjectTypeCache
::getInstance()->getObjectTypeByName('com.woltlab.wcf.acl', $formField->getValue())->objectTypeID
412 if ($optionList->countObjects() > 0) {
413 $nameField->addValidationError(
414 new FormFieldValidationError(
416 'wcf.acp.pip.aclOption.objectType.' . $entryType . '.error.notUnique'
425 $form->getNodeById('data')->appendChildren([$nameFormField, $objectTypeFormField]);
427 if ($this->entryType
=== 'options') {
428 $categoryList = new ACLOptionCategoryList();
429 $categoryList->getConditionBuilder()->add('packageID IN (?)', [$requiredPackageIDs]);
430 $categoryList->sqlOrderBy
= 'categoryName ASC';
431 $categoryList->readObjects();
434 foreach ($categoryList as $category) {
435 if (!isset($categories[$category->objectTypeID
])) {
436 $categories[$category->objectTypeID
] = [];
439 $categories[$category->objectTypeID
][$category->categoryName
] = $category->categoryName
;
442 foreach ($objectTypes as $objectType) {
443 $objectTypeID = ObjectTypeCache
::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.acl', $objectType);
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]);
452 $categoryNameField->addDependency(
453 ValueFormFieldDependency
::create('objectType')
454 ->field($objectTypeFormField)
455 ->values([$objectType])
458 $form->getNodeById('data')->appendChild($categoryNameField);
468 public function getEntryTypes(): array {
469 return ['options', 'categories'];
476 protected function getElementData(\DOMElement
$element): array {
478 'name' => $element->getAttribute('name'),
479 'packageID' => $this->installation
->getPackage()->packageID
,
480 'objectType' => $element->getElementsByTagName('objecttype')->item(0)->nodeValue
483 if ($this->entryType
=== 'options') {
484 $categoryName = $element->getElementsByTagName('categoryname')->item(0);
485 if ($categoryName !== null) {
486 $data['categoryName'] = $categoryName->nodeValue
;
497 public function getElementIdentifier(\DOMElement
$element): string {
498 $elementData = $this->getElementData($element);
500 return sha1($elementData['objectType'] . '/' . $elementData['name']);
507 protected function getXsdFilename(): string {
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'
526 protected function saveObject(\DOMElement
$newElement, \DOMElement
$oldElement = null) {
527 if ($oldElement === null) {
528 $xpath = $this->getProjectXml()->xpath();
530 switch ($this->entryType
) {
532 $this->importCategories($xpath);
536 $this->importOptions($xpath);
540 throw new \
LogicException('Unreachable');
544 $oldData = $this->getElementData($oldElement);
545 $newData = $this->getElementData($newElement);
547 switch ($this->entryType
) {
550 FROM wcf" . WCF_N
. "_acl_option_category
551 WHERE categoryName = ?
554 $statement = WCF
::getDB()->prepareStatement($sql);
555 $statement->execute([
557 ObjectTypeCache
::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.acl', $oldData['objectType']),
558 $oldData['packageID']
560 (new ACLOptionCategoryEditor($statement->fetchObject(ACLOptionCategory
::class)))->update([
561 'categoryNameName' => $newData['name'],
562 'objectTypeID' => ObjectTypeCache
::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.acl', $newData['objectType'])
569 FROM wcf" . WCF_N
. "_acl_option
573 $statement = WCF
::getDB()->prepareStatement($sql);
574 $statement->execute([
576 ObjectTypeCache
::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.acl', $oldData['objectType']),
577 $oldData['packageID']
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'] ??
''
588 throw new \
LogicException('Unreachable');
597 protected function sortDocument(\DOMDocument
$document) {
598 $this->sortImportDelete($document);
600 // `<categories>` before `<options>`
601 $compareFunction = function(\DOMElement
$element1, \DOMElement
$element2) {
602 if ($element1->nodeName
=== 'categories') {
605 else if ($element2->nodeName
=== 'categories') {
612 $this->sortChildNodes($document->getElementsByTagName('import'), $compareFunction);
613 $this->sortChildNodes($document->getElementsByTagName('delete'), $compareFunction);
615 $compareFunction = function(\DOMElement
$element1, \DOMElement
$element2) {
616 $objectType1 = $element1->getElementsByTagName('objecttype')->item(0)->nodeValue
;
617 $objectType2 = $element2->getElementsByTagName('objecttype')->item(0)->nodeValue
;
619 if ($objectType1 !== $objectType2) {
620 return strcmp($objectType1, $objectType2);
623 if ($element1->nodeName
=== 'option') {
624 $categoryName1 = $element1->getElementsByTagName('categoryname')->item(0);
625 $categoryName2 = $element2->getElementsByTagName('categoryname')->item(0);
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
);
634 // only first category specified
639 // only second category specified
640 else if ($categoryName2 !== null) {
646 $element1->getAttribute('name'),
647 $element2->getAttribute('name')
651 $this->sortChildNodes($document->getElementsByTagName('categories'), $compareFunction);
652 $this->sortChildNodes($document->getElementsByTagName('options'), $compareFunction);
659 protected function writeEntry(\DOMDocument
$document, IFormDocument
$form): \DOMElement
{
660 $formData = $form->getData()['data'];
662 switch ($this->entryType
) {
664 $category = $document->createElement('category');
665 $category->setAttribute('name', $formData['name']);
667 $category->appendChild($document->createElement('objecttype', $formData['objecttype']));
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);
676 $categories->appendChild($category);
681 $option = $document->createElement('option');
682 $option->setAttribute('name', $formData['name']);
684 $option->appendChild($document->createElement('objecttype', $formData['objecttype']));
686 if (isset($formData['categoryname'])) {
687 $option->appendChild($document->createElement('categoryname', $formData['categoryname']));
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);
697 $options->appendChild($option);
702 throw new \
LogicException('Unreachable');