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;
}
$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 */
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;
/**
* @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
*/
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;
+ }
}
<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>
<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>
<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>
<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>
<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">