Add GUI for pip package installation plugin
authorMatthias Schmidt <gravatronics@live.com>
Sun, 15 Apr 2018 15:39:33 +0000 (17:39 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Sun, 15 Apr 2018 15:39:33 +0000 (17:39 +0200)
See #2545

wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/PIPPackageInstallationPlugin.class.php
wcfsetup/install/lang/en.xml

index 18752d8ce0c2923f978a96f095a91e2119ee8b04..a63e1bcb3adb0bfb68783f348a090878001b8be0 100644 (file)
@@ -5,6 +5,7 @@ use wcf\data\devtools\project\DevtoolsProject;
 use wcf\data\IEditableCachedObject;
 use wcf\system\form\builder\field\IFormField;
 use wcf\system\form\builder\IFormDocument;
+use wcf\system\form\builder\IFormNode;
 use wcf\system\WCF;
 use wcf\util\DOMUtil;
 use wcf\util\StringUtil;
@@ -255,8 +256,17 @@ XML;
                }
                
                $data = [];
+               /** @var \DOMNode $attribute */
+               foreach ($element->attributes as $attribute) {
+                       $data[$attribute->nodeName] = $attribute->nodeValue;
+               }
                foreach ($element->childNodes as $childNode) {
-                       $data[$childNode->nodeName] = $childNode->nodeValue;
+                       if ($childNode instanceof \DOMText) {
+                               $data['__value'] = $childNode->nodeValue;
+                       }
+                       else {
+                               $data[$childNode->nodeName] = $childNode->nodeValue;
+                       }
                }
                
                /** @var IFormNode $node */
index 121ff5d1e0381067e2b1aa628de463165128da9c..7342e051137acb0e3fe842b6e905bda85ab7bba8 100644 (file)
@@ -2,7 +2,16 @@
 declare(strict_types=1);
 namespace wcf\system\package\plugin;
 use wcf\data\package\installation\plugin\PackageInstallationPluginEditor;
-use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
+use wcf\data\package\installation\plugin\PackageInstallationPluginList;
+use wcf\system\devtools\pip\DevtoolsPipEntryList;
+use wcf\system\devtools\pip\IDevtoolsPipEntryList;
+use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
+use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
+use wcf\system\form\builder\container\FormContainer;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\form\builder\field\validation\FormFieldValidator;
+use wcf\system\form\builder\field\TextFormField;
+use wcf\system\form\builder\IFormDocument;
 use wcf\system\WCF;
 
 /**
@@ -13,7 +22,9 @@ use wcf\system\WCF;
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\System\Package\Plugin
  */
-class PIPPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
+class PIPPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin {
+       use TXmlGuiPackageInstallationPlugin;
+       
        /**
         * @inheritDoc
         */
@@ -84,4 +95,169 @@ class PIPPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin
        public static function getSyncDependencies() {
                return [];
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function addFormFields(IFormDocument $form) {
+               /** @var FormContainer $dataContainer */
+               $dataContainer = $form->getNodeById('data');
+               
+               $dataContainer->appendChildren([
+                       TextFormField::create('pluginName')
+                               ->attribute('data-tag', 'name')
+                               ->label('wcf.acp.pip.pip.pluginName')
+                               ->description('wcf.acp.pip.pip.pluginName.description')
+                               ->required()
+                               ->addValidator(new FormFieldValidator('format', function(TextFormField $formField) {
+                                       if (preg_match('~^[a-z][A-z]+$~', $formField->getValue()) !== 1) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'format',
+                                                               'wcf.acp.pip.pip.pluginName.error.format'
+                                                       )
+                                               );
+                                       }
+                               }))
+                               ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField $formField) {
+                                       $pipList = new PackageInstallationPluginList();
+                                       $pipList->getConditionBuilder()->add('pluginName = ?', [$formField->getValue()]);
+                                       
+                                       if ($pipList->countObjects()) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'format',
+                                                               'wcf.acp.pip.pip.pluginName.error.notUnique'
+                                                       )
+                                               );
+                                       }
+                               })),
+                       
+                       TextFormField::create('className')
+                               ->attribute('data-tag', '__value')
+                               ->label('wcf.acp.pip.pip.className')
+                               ->description('wcf.acp.pip.pip.className.description')
+                               ->required()
+                               ->addValidator(new FormFieldValidator('noLeadingBackslash', function(TextFormField $formField) {
+                                       if (substr($formField->getValue(), 0, 1) === '\\') {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'leadingBackslash',
+                                                               'wcf.acp.pip.pip.className.error.leadingBackslash'
+                                                       )
+                                               );
+                                       }
+                               }))
+                               ->addValidator(new FormFieldValidator('classExists', function(TextFormField $formField) {
+                                       if (!class_exists($formField->getValue())) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'nonExistent',
+                                                               'wcf.acp.pip.pip.className.error.nonExistent'
+                                                       )
+                                               );
+                                       }
+                               }))
+                               ->addValidator(new FormFieldValidator('implementsInterface', function(TextFormField $formField) {
+                                       if (!is_subclass_of($formField->getValue(), IPackageInstallationPlugin::class)) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'interface',
+                                                               'wcf.acp.pip.pip.className.error.interface'
+                                                       )
+                                               );
+                                       }
+                               }))
+                               ->addValidator(new FormFieldValidator('isInstantiable', function(TextFormField $formField) {
+                                       $reflection = new \ReflectionClass($formField->getValue());
+                                       if (!$reflection->isInstantiable()) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'interface',
+                                                               'wcf.acp.pip.pip.className.error.isInstantiable'
+                                                       )
+                                               );
+                                       }
+                               }))
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function getElementData(\DOMElement $element): array {
+               return [
+                       'className' => $element->nodeValue,
+                       'pluginName' => $element->getAttribute('name'),
+                       'priority' => $this->installation->getPackage()->package == 'com.woltlab.wcf' ? 1 : 0
+               ];
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getElementIdentifier(\DOMElement $element): string {
+               return $element->getAttribute('name');
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getEntryList(): IDevtoolsPipEntryList {
+               $xml = $this->getProjectXml();
+               $xpath = $xml->xpath();
+               
+               $entryList = new DevtoolsPipEntryList();
+               $entryList->setKeys([
+                       'pluginName' => 'wcf.acp.pip.pip.pluginName',
+                       'className' => 'wcf.acp.pip.pip.className'
+               ]);
+               
+               /** @var \DOMElement $languageItem */
+               foreach ($this->getImportElements($xpath) as $element) {
+                       $entryList->addEntry($this->getElementIdentifier($element), [
+                               'className' => $element->nodeValue,
+                               'pluginName' => $element->getAttribute('name')
+                       ]);
+               }
+               
+               return $entryList;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function sortDocument(\DOMDocument $document) {
+               $this->sortImportDelete($document);
+               
+               $compareFunction = function(\DOMElement $element1, \DOMElement $element2) {
+                       return strcmp($element1->getAttribute('name'), $element2->getAttribute('name'));
+               };
+               
+               $this->sortChildNodes($document->getElementsByTagName('import'), $compareFunction);
+               $this->sortChildNodes($document->getElementsByTagName('delete'), $compareFunction);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function writeEntry(\DOMDocument $document, IFormDocument $form): \DOMElement {
+               /** @var TextFormField $className */
+               $className = $form->getNodeById('className');
+               /** @var TextFormField $pluginName */
+               $pluginName = $form->getNodeById('pluginName');
+               
+               $pip = $document->createElement('pip', $className->getSaveValue());
+               $pip->setAttribute('name', $pluginName->getSaveValue());
+               
+               $document->getElementsByTagName('import')->item(0)->appendChild($pip);
+               
+               return $pip;
+       }
 }
index fe5984393505f0ecf1929bebf8448fa0f041dae7..aca176f8297277f84f1e75f3a6e684323cba2b0c 100644 (file)
@@ -1698,7 +1698,7 @@ When prompted for the notification URL for the instant payment notifications, pl
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.categoryName"><![CDATA[Category]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.categoryName.description"><![CDATA[Ad locations are grouped by their category in the ad location selection when creating and editing ads. The category consists of at least four segments that are separated by dots. Each segment may only contain the following characters: <code>[A-z0-9-_]</code>.]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.categoryName.error.invalidSegments"><![CDATA[The following segments are invalid: {implode from=$invalidSegments key=segmentNumber item=segment}{if $segment !== ''}<code>{$segment}</code>{else}(empty){/if} (segment {#$segmentNumber + 1}){/implode}.]]></item>
-               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.categoryName.error.tooFewSegments"><![CDATA[The given category only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.categoryName.error.tooFewSegments"><![CDATA[The entered category only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.cssClassName"><![CDATA[CSS Classes]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.cssClassName.description"><![CDATA[The entered comma-separated CSS classes are assigned to the element that wraps all ads at the specific location. ]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.cssClassName.error.invalid"><![CDATA[The following CSS classes are invalid: {implode from=$invalidClasses item=invalidClass}<code>{$invalidClass}</code>{/implode}.]]></item>
@@ -1710,7 +1710,7 @@ When prompted for the notification URL for the instant payment notifications, pl
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.action.description"><![CDATA[Unique textual identifier of the user bulk processing action that may only contain letters and must start with a lowercase letter.]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.action.error.format"><![CDATA[The entered action is invalid.]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.options.description"><![CDATA[At least one of the entered options has to be enabled for the user bulk processing action to be available.]]></item>
-               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.permissions.description"><![CDATA[The active user must be granted at least one of the given permissions in order to execute the action.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.action.permissions.description"><![CDATA[The active user must be granted at least one of the entered permissions in order to execute the action.]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.bulkProcessing.user.condition.data.title"><![CDATA[User Bulk Processing Condition Data]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.category.data.title"><![CDATA[Category Type Data]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.category.defaultPermission"><![CDATA[Category Default Permission]]></item>
@@ -1747,12 +1747,12 @@ When prompted for the notification URL for the instant payment notifications, pl
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.categoryName"><![CDATA[Category]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.categoryName.description"><![CDATA[Daily statistics handler are grouped by their category on the stats page. The category consists of at least four segments that are separated by dots. Each segment may only contain the following characters: <code>[A-z0-9-_]</code>.]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.categoryName.error.invalidSegments"><![CDATA[The following segments are invalid: {implode from=$invalidSegments key=segmentNumber item=segment}{if $segment !== ''}<code>{$segment}</code>{else}(empty){/if} (segment {#$segmentNumber + 1}){/implode}.]]></item>
-               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.categoryName.error.tooFewSegments"><![CDATA[The given category only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.categoryName.error.tooFewSegments"><![CDATA[The entered category only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.isDefault"><![CDATA[Default Daily Statistics Handler]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.isDefault.description"><![CDATA[Default daily statistics handler are pre-selected when loading the stats page.]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.tagging.taggableObject.data.title"><![CDATA[Taggable Object Data]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.tagging.taggableObject.options.description"><![CDATA[At least one of the entered options has to be enabled the list of all objects of this type with a specific tag to be available.]]></item>
-               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.tagging.taggableObject.permissions.description"><![CDATA[The active user must be granted at least one of the given permissions in order to see the list of all objects of this type with a specific tag.]]></item>
+               <item name="wcf.acp.pip.objectType.com.woltlab.wcf.tagging.taggableObject.permissions.description"><![CDATA[The active user must be granted at least one of the entered permissions in order to see the list of all objects of this type with a specific tag.]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.user.activityPointEvent.data.title"><![CDATA[User Activity Event Points Data]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.user.activityPointEvent.points"><![CDATA[Points]]></item>
                <item name="wcf.acp.pip.objectType.com.woltlab.wcf.user.activityPointEvent.points.description"><![CDATA[Number of points the user is awarded for the event.]]></item>
@@ -1767,9 +1767,9 @@ When prompted for the notification URL for the instant payment notifications, pl
                <item name="wcf.acp.pip.objectType.condition.conditionObject"><![CDATA[Conditioned Object Identifier]]></item>
                <item name="wcf.acp.pip.objectType.condition.conditionObject.description"><![CDATA[The object type-alike identifier of the object this condition is related with is used to group large lists of conditions into logical groups.]]></item>
                <item name="wcf.acp.pip.objectType.condition.conditionObject.error.invalidSegments"><![CDATA[The following segments are invalid: {implode from=$invalidSegments key=segmentNumber item=segment}{if $segment !== ''}<code>{$segment}</code>{else}(empty){/if} (segment {#$segmentNumber + 1}){/implode}.]]></item>
-               <item name="wcf.acp.pip.objectType.condition.conditionObject.error.tooFewSegments"><![CDATA[The given identifier only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.conditionObject.error.tooFewSegments"><![CDATA[The entered identifier only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
                <item name="wcf.acp.pip.objectType.condition.conditionGroup"><![CDATA[Condition Group]]></item>
-               <item name="wcf.acp.pip.objectType.condition.conditionGroup.description"><![CDATA[The condition group is used to group conditions with the same group identifier together into, generally, one tab. The condition group may only consist of letters and must beging with a lowercase letter.]]></item>
+               <item name="wcf.acp.pip.objectType.condition.conditionGroup.description"><![CDATA[The condition group is used to group conditions with the same group identifier together into, generally, one tab. The condition group may only consist of letters and must begin with a lowercase letter.]]></item>
                <item name="wcf.acp.pip.objectType.condition.conditionGroup.error.format"><![CDATA[The entered condition group is invalid.]]></item>
                <item name="wcf.acp.pip.objectType.condition.integer.maxValue"><![CDATA[Maximum Value]]></item>
                <item name="wcf.acp.pip.objectType.condition.integer.maxValue.description"><![CDATA[When setting up the condition, the value for this condition may not be greater than the entered value.]]></item>
@@ -1838,16 +1838,26 @@ When prompted for the notification URL for the instant payment notifications, pl
                <item name="wcf.acp.pip.objectType.objectType"><![CDATA[Object Type Identifier]]></item>
                <item name="wcf.acp.pip.objectType.objectType.description"><![CDATA[Textual identifier of the object type that is primarily used in PHP code. The identifier consists of at least four segments that are separated by dots. Each segment may only contain the following characters: <code>[A-z0-9-_]</code>.]]></item>
                <item name="wcf.acp.pip.objectType.objectType.error.invalidSegments"><![CDATA[The following segments are invalid: {implode from=$invalidSegments key=segmentNumber item=segment}{if $segment !== ''}<code>{$segment}</code>{else}(empty){/if} (segment {#$segmentNumber + 1}){/implode}.]]></item>
-               <item name="wcf.acp.pip.objectType.objectType.error.notUnique"><![CDATA[The given name is already used by another object type of the same object type definition.]]></item>
-               <item name="wcf.acp.pip.objectType.objectType.error.tooFewSegments"><![CDATA[The given identifier only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
+               <item name="wcf.acp.pip.objectType.objectType.error.notUnique"><![CDATA[The entered name is already used by another object type of the same object type definition.]]></item>
+               <item name="wcf.acp.pip.objectType.objectType.error.tooFewSegments"><![CDATA[The entered identifier only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
                <item name="wcf.acp.pip.objectTypeDefinition.definitionName"><![CDATA[Definition Name]]></item>
                <item name="wcf.acp.pip.objectTypeDefinition.definitionName.description"><![CDATA[The name of an object type definitions consists of least four segments separated by dots. Each segment must not be empty and may only contain letters, numbers, underscores, and dashes. In general, the first part of the definition name matches the package identifier. Example: <code>{$project->getPackage()->package}.type</code>]]></item>
                <item name="wcf.acp.pip.objectTypeDefinition.definitionName.error.invalidSegments"><![CDATA[The following segments are invalid: {implode from=$invalidSegments key=segmentNumber item=segment}{if $segment !== ''}<code>{$segment}</code>{else}(empty){/if} (segment {#$segmentNumber + 1}){/implode}.]]></item>
-               <item name="wcf.acp.pip.objectTypeDefinition.definitionName.error.notUnique"><![CDATA[The given name is already used by another definition.]]></item>
-               <item name="wcf.acp.pip.objectTypeDefinition.definitionName.error.tooFewSegments"><![CDATA[The given name only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.definitionName.error.notUnique"><![CDATA[The entered name is already used by another definition.]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.definitionName.error.tooFewSegments"><![CDATA[The entered name only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
                <item name="wcf.acp.pip.objectTypeDefinition.interfaceName"><![CDATA[PHP Interface]]></item>
-               <item name="wcf.acp.pip.objectTypeDefinition.interfaceName.description"><![CDATA[If a PHP interface is given, every object type of this definition must provide the name of a PHP class that implements the interface.]]></item>
+               <item name="wcf.acp.pip.objectTypeDefinition.interfaceName.description"><![CDATA[If a PHP interface is entered, every object type of this definition must provide the name of a PHP class that implements the interface.]]></item>
                <item name="wcf.acp.pip.objectTypeDefinition.interfaceName.error.nonExistent"><![CDATA[The entered interface does not exist.]]></item>
+               <item name="wcf.acp.pip.pip.pluginName"><![CDATA[Package Installation Plugin Name]]></item>
+               <item name="wcf.acp.pip.pip.pluginName.description"><![CDATA[The name of the package installation plugin is used as the value of the <code>type</code> attribute of an <code>instruction</code> element in a <code>package.xml</code> file. The name may only consist of letters and must begin with a lowercase letter.]]></item>
+               <item name="wcf.acp.pip.pip.pluginName.error.format"><![CDATA[The entered name is invalid.]]></item>
+               <item name="wcf.acp.pip.pip.pluginName.error.notUnique"><![CDATA[The entered name is already used by another package installation plugin.]]></item>
+               <item name="wcf.acp.pip.pip.className"><![CDATA[Class Name]]></item>
+               <item name="wcf.acp.pip.pip.className.description"><![CDATA[The entered class (without leading backslash) must implement the interface <code>wcf\system\package\plugin\IPackageInstallationPlugin</code>.]]></item>
+               <item name="wcf.acp.pip.pip.className.error.leadingBackslash"><![CDATA[The entered class name has a leading backslash.]]></item>
+               <item name="wcf.acp.pip.pip.className.error.nonExistent"><![CDATA[The entered class does not exist.]]></item>
+               <item name="wcf.acp.pip.pip.className.error.interface"><![CDATA[The entered class does not implement the interface <code>wcf\system\package\plugin\IPackageInstallationPlugin</code>.]]></item>
+               <item name="wcf.acp.pip.pip.className.error.isInstantiable"><![CDATA[The entered class is not instantiable.]]></item>
        </category>
        
        <category name="wcf.acp.rebuildData">