Resolve language item-related PIP GUI todos
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / plugin / ObjectTypePackageInstallationPlugin.class.php
1 <?php
2 namespace wcf\system\package\plugin;
3 use wcf\data\object\type\definition\ObjectTypeDefinitionList;
4 use wcf\data\object\type\ObjectTypeCache;
5 use wcf\data\object\type\ObjectTypeEditor;
6 use wcf\data\DatabaseObjectList;
7 use wcf\data\page\PageNode;
8 use wcf\data\page\PageNodeTree;
9 use wcf\system\application\ApplicationHandler;
10 use wcf\system\condition\AbstractIntegerCondition;
11 use wcf\system\condition\UserGroupCondition;
12 use wcf\system\condition\UserIntegerPropertyCondition;
13 use wcf\system\condition\UserTimestampPropertyCondition;
14 use wcf\system\devtools\pip\DevtoolsPipEntryList;
15 use wcf\system\devtools\pip\IDevtoolsPipEntryList;
16 use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
17 use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
18 use wcf\system\event\EventHandler;
19 use wcf\system\exception\SystemException;
20 use wcf\system\form\builder\container\FormContainer;
21 use wcf\system\form\builder\container\IFormContainer;
22 use wcf\system\form\builder\field\dependency\ValueFormFieldDependency;
23 use wcf\system\form\builder\field\OptionFormField;
24 use wcf\system\form\builder\field\UserGroupOptionFormField;
25 use wcf\system\form\builder\field\validation\FormFieldValidationError;
26 use wcf\system\form\builder\field\validation\FormFieldValidator;
27 use wcf\system\form\builder\field\BooleanFormField;
28 use wcf\system\form\builder\field\ClassNameFormField;
29 use wcf\system\form\builder\field\FloatFormField;
30 use wcf\system\form\builder\field\IntegerFormField;
31 use wcf\system\form\builder\field\ItemListFormField;
32 use wcf\system\form\builder\field\SingleSelectionFormField;
33 use wcf\system\form\builder\field\TextFormField;
34 use wcf\system\form\builder\field\validation\FormFieldValidatorUtil;
35 use wcf\system\form\builder\IFormDocument;
36 use wcf\system\Regex;
37 use wcf\system\WCF;
38 use wcf\util\DirectoryUtil;
39
40 /**
41 * Installs, updates and deletes object types.
42 *
43 * @author Alexander Ebert, Matthias Schmidt
44 * @copyright 2001-2018 WoltLab GmbH
45 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
46 * @package WoltLabSuite\Core\Acp\Package\Plugin
47 */
48 class ObjectTypePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin {
49 use TXmlGuiPackageInstallationPlugin;
50
51 /**
52 * @inheritDoc
53 */
54 public $className = ObjectTypeEditor::class;
55
56 /**
57 * @inheritDoc
58 */
59 public $tagName = 'type';
60
61 /**
62 * list of names of tags which aren't considered as additional data
63 * @var string[]
64 */
65 public static $reservedTags = ['classname', 'definitionname', 'name'];
66
67 /**
68 * @var string[]
69 */
70 public $definitionNames = [];
71
72 /**
73 * @var string[]
74 */
75 public $definitionInterfaces = [];
76
77 /**
78 * data for object type definition-specific xml element children
79 * @var array
80 */
81 public $definitionElementChildren = [];
82
83 /**
84 * Returns the id of the object type definition with the given name.
85 *
86 * @param string $definitionName
87 * @return integer
88 * @throws SystemException
89 */
90 protected function getDefinitionID($definitionName) {
91 // get object type id
92 $sql = "SELECT definitionID
93 FROM wcf".WCF_N."_object_type_definition
94 WHERE definitionName = ?";
95 $statement = WCF::getDB()->prepareStatement($sql, 1);
96 $statement->execute([$definitionName]);
97 $row = $statement->fetchArray();
98 if (empty($row['definitionID'])) throw new SystemException("unknown object type definition '".$definitionName."' given");
99 return $row['definitionID'];
100 }
101
102 /**
103 * @inheritDoc
104 */
105 protected function handleDelete(array $items) {
106 $sql = "DELETE FROM wcf".WCF_N."_".$this->tableName."
107 WHERE objectType = ?
108 AND definitionID = ?
109 AND packageID = ?";
110 $statement = WCF::getDB()->prepareStatement($sql);
111 foreach ($items as $item) {
112 $statement->execute([
113 $item['attributes']['name'],
114 $this->getDefinitionID($item['elements']['definitionname']),
115 $this->installation->getPackageID()
116 ]);
117 }
118 }
119
120 /**
121 * @inheritDoc
122 */
123 protected function prepareImport(array $data) {
124 $additionalData = [];
125 foreach ($data['elements'] as $tagName => $nodeValue) {
126 if (!in_array($tagName, self::$reservedTags)) $additionalData[$tagName] = $nodeValue;
127 }
128
129 return [
130 'definitionID' => $this->getDefinitionID($data['elements']['definitionname']),
131 'objectType' => $data['elements']['name'],
132 'className' => isset($data['elements']['classname']) ? $data['elements']['classname'] : '',
133 'additionalData' => serialize($additionalData)
134 ];
135 }
136
137 /**
138 * @inheritDoc
139 */
140 protected function findExistingItem(array $data) {
141 $sql = "SELECT *
142 FROM wcf".WCF_N."_".$this->tableName."
143 WHERE objectType = ?
144 AND definitionID = ?
145 AND packageID = ?";
146 $parameters = [
147 $data['objectType'],
148 $data['definitionID'],
149 $this->installation->getPackageID()
150 ];
151
152 return [
153 'sql' => $sql,
154 'parameters' => $parameters
155 ];
156 }
157
158 /**
159 * @inheritDoc
160 */
161 public static function getSyncDependencies() {
162 return ['objectTypeDefinition'];
163 }
164
165 /**
166 * @inheritDoc
167 * @since 3.2
168 */
169 public function getAdditionalTemplateCode() {
170 return WCF::getTPL()->fetch('__objectTypePipGui', 'wcf', [
171 'definitionNames' => $this->definitionNames,
172 'definitionInterfaces' => $this->definitionInterfaces
173 ], true);
174 }
175
176 /**
177 * @inheritDoc
178 * @since 3.2
179 */
180 protected function doGetElementData(\DOMElement $element, $saveData) {
181 $data = [
182 'definitionID' => $this->getDefinitionID($element->getElementsByTagName('definitionname')->item(0)->nodeValue),
183 'objectType' => $element->getElementsByTagName('name')->item(0)->nodeValue,
184 'packageID' => $this->installation->getPackage()->packageID
185 ];
186
187 $className = $element->getElementsByTagName('classname')->item(0);
188 if ($className) {
189 $data['className'] = $className->nodeValue;
190 }
191
192 $additionalData = [];
193
194 /** @var \DOMElement $child */
195 foreach ($element->childNodes as $child) {
196 if (!in_array($child->nodeName, self::$reservedTags)) {
197 $additionalData[$child->nodeName] = $child->nodeValue;
198 }
199 }
200
201 if ($saveData) {
202 $data['additionalData'] = serialize($additionalData);
203 }
204 else {
205 $data = array_merge($additionalData, $data);
206 }
207
208 return $data;
209 }
210
211 /**
212 * @inheritDoc
213 * @since 3.2
214 */
215 protected function addFormFields(IFormDocument $form) {
216 // read available object type definitions
217 $list = new ObjectTypeDefinitionList();
218 $list->sqlOrderBy = 'definitionName';
219 $list->readObjects();
220
221 foreach ($list as $definition) {
222 $this->definitionNames[$definition->definitionID] = $definition->definitionName;
223
224 if ($definition->interfaceName) {
225 $this->definitionInterfaces[$definition->definitionID] = $definition->interfaceName;
226 }
227 }
228
229 // add default form fields
230 /** @var FormContainer $dataContainer */
231 $dataContainer = $form->getNodeById('data');
232
233 $dataContainer->appendChildren([
234 SingleSelectionFormField::create('definitionID')
235 ->label('wcf.acp.pip.objectType.definitionName')
236 ->description('<!-- will be replaced by JavaScript -->')
237 ->options($this->definitionNames)
238 ->required(),
239
240 TextFormField::create('objectType')
241 ->objectProperty('name')
242 ->label('wcf.acp.pip.objectType.objectType')
243 ->description('wcf.acp.pip.objectType.objectType.description')
244 ->required()
245 ->addValidator(FormFieldValidatorUtil::getDotSeparatedStringValidator('wcf.acp.pip.objectType.objectType', 4))
246 ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField $formField) {
247 /** @var SingleSelectionFormField $definitionIDField */
248 $definitionIDField = $formField->getDocument()->getNodeById('definitionID');
249
250 $definitionID = $definitionIDField->getSaveValue();
251 if ($definitionID) {
252 $definition = ObjectTypeCache::getInstance()->getDefinition($definitionID);
253
254 $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName(
255 $definition->definitionName,
256 $formField->getValue()
257 );
258
259 // the object type name is not unique if such an object type already exists
260 // and (a) a new object type is added or (b) the existing object type is
261 // different from the edited object type
262 if ($objectType !== null && (
263 $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE ||
264 $this->editedEntry->getElementsByTagName('name')->item(0)->nodeValue !== $formField->getValue() ||
265 $this->editedEntry->getElementsByTagName('definitionname')->item(0)->nodeValue !== $definition->definitionName
266 )) {
267 $formField->addValidationError(
268 new FormFieldValidationError(
269 'notUnique',
270 'wcf.acp.pip.objectType.objectType.error.notUnique'
271 )
272 );
273 }
274 }
275 })),
276
277 ClassNameFormField::create()
278 ->objectProperty('classname')
279 ->description('<!-- will be replaced by JavaScript -->')
280 ->required()
281 ->addValidator(new FormFieldValidator('implementsInterface', function(TextFormField $formField) {
282 /** @var SingleSelectionFormField $definitionIDField */
283 $definitionIDField = $formField->getDocument()->getNodeById('definitionID');
284
285 $definitionID = $definitionIDField->getSaveValue();
286 if ($definitionID) {
287 $definition = ObjectTypeCache::getInstance()->getDefinition($definitionID);
288
289 if (!is_subclass_of($formField->getValue(), $definition->interfaceName)) {
290 $formField->addValidationError(
291 new FormFieldValidationError(
292 'interface',
293 'wcf.form.field.className.error.interface',
294 ['interface' => $definition->interfaceName]
295 )
296 );
297 }
298 }
299 })),
300 ]);
301
302 /** @var SingleSelectionFormField $definitionName */
303 $definitionID = $form->getNodeById('definitionID');
304
305 // add general field dependencies
306 $form->getNodeById('className')->addDependency(
307 ValueFormFieldDependency::create('definitionID')
308 ->field($definitionID)
309 ->values(array_keys($this->definitionInterfaces))
310 );
311
312 // add object type-specific fields
313
314 // com.woltlab.wcf.adLocation
315 $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.adLocation')
316 ->appendChildren([
317 SingleSelectionFormField::create('adLocationPage')
318 ->objectProperty('page')
319 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.page')
320 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.page.description')
321 ->options(function() {
322 $options = [
323 [
324 'depth' => 0,
325 'label' => 'wcf.global.noSelection',
326 'value' => ''
327 ]
328 ];
329
330 $pageNodeTree = new PageNodeTree();
331
332 /** @var PageNode $pageNode */
333 foreach ($pageNodeTree->getNodeList() as $pageNode) {
334 $options[] = [
335 'depth' => $pageNode->getDepth() - 1,
336 'label' => $pageNode->name,
337 'value' => $pageNode->identifier
338 ];
339 }
340
341 return $options;
342 }, true),
343 TextFormField::create('adLocationCategoryName')
344 ->objectProperty('categoryname')
345 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.categoryName')
346 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.categoryName.description')
347 ->addValidator(FormFieldValidatorUtil::getDotSeparatedStringValidator(
348 'wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.categoryName',
349 4
350 )),
351 ItemListFormField::create('adLocationCssClassName')
352 ->objectProperty('cssclassname')
353 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.cssClassName')
354 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.cssClassName.description')
355 ->saveValueType(ItemListFormField::SAVE_VALUE_TYPE_SSV)
356 ->addValidator(new FormFieldValidator('format', function(ItemListFormField $formField) {
357 if (!empty($formField->getValue())) {
358 $invalidClasses = [];
359 foreach ($formField->getValue() as $class) {
360 if (preg_match('~^-?[_A-z][_A-z0-9-]*$~', $class) !== 1) {
361 $invalidClasses[] = $class;
362 }
363 }
364
365 if (!empty($invalidClasses)) {
366 $formField->addValidationError(
367 new FormFieldValidationError(
368 'invalid',
369 'wcf.acp.pip.objectType.com.woltlab.wcf.adLocation.cssClassName.error.invalid',
370 ['invalidClasses' => $invalidClasses]
371 )
372 );
373 }
374 }
375 }))
376 ]);
377 $this->definitionElementChildren['com.woltlab.wcf.adLocation'] = [
378 'page' => '',
379 'categoryname' => '',
380 'cssclassname' => ''
381 ];
382
383 // com.woltlab.wcf.attachment.objectType
384 $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.attachment.objectType')
385 ->appendChild(
386 BooleanFormField::create('attachmentPrivate')
387 ->objectProperty('private')
388 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.attachment.objectType.private')
389 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.attachment.objectType.private.description')
390 );
391 $this->definitionElementChildren['com.woltlab.wcf.attachment.objectType'] = ['private' => 0];
392
393 // com.woltlab.wcf.bulkProcessing.user.action
394 $this->addBulkProcessingActionFields($form, 'com.woltlab.wcf.bulkProcessing.user.action');
395
396 // com.woltlab.wcf.bulkProcessing.user.condition
397 $bulkProcessingUserConditionContainer = $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.bulkProcessing.user.condition');
398 $this->addConditionFields($bulkProcessingUserConditionContainer, 'com.woltlab.wcf.bulkProcessing.user.condition', false, true);
399
400 // com.woltlab.wcf.category
401 $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.category')
402 ->appendChild(
403 BooleanFormField::create('categoryDefaultPermission')
404 ->objectProperty('defaultpermission')
405 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.category.defaultPermission')
406 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.category.defaultPermission.description')
407 );
408 $this->definitionElementChildren['com.woltlab.wcf.category'] = ['defaultpermission' => 0];
409
410 // com.woltlab.wcf.clipboardItem
411 $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.clipboardItem')
412 ->appendChild(
413 ClassNameFormField::create('clipboardItemListClassName')
414 ->objectProperty('listclassname')
415 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.clipboardItem.listClassName')
416 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.clipboardItem.listClassName.description')
417 ->required()
418 ->parentClass(DatabaseObjectList::class)
419 );
420 $this->definitionElementChildren['com.woltlab.wcf.clipboardItem'] = ['listclassname'];
421
422 // com.woltlab.wcf.condition.ad
423 $conditionAdContainer = $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.condition.ad');
424 $this->addConditionFields($conditionAdContainer, 'com.woltlab.wcf.condition.ad', true, true);
425
426 // com.woltlab.wcf.condition.notice
427 $conditionAdContainer = $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.condition.notice');
428 $this->addConditionFields($conditionAdContainer, 'com.woltlab.wcf.condition.notice');
429
430 // com.woltlab.wcf.condition.trophy
431 $conditionAdContainer = $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.condition.trophy');
432 $this->addConditionFields($conditionAdContainer, 'com.woltlab.wcf.condition.trophy', false, true);
433
434 // com.woltlab.wcf.condition.userGroupAssignment
435 $conditionAdContainer = $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.condition.userGroupAssignment');
436 $this->addConditionFields($conditionAdContainer, 'com.woltlab.wcf.condition.userGroupAssignment', false, true);
437
438 // com.woltlab.wcf.condition.userSearch
439 $conditionAdContainer = $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.condition.userSearch');
440 $this->addConditionFields($conditionAdContainer, 'com.woltlab.wcf.condition.userSearch', false, true);
441
442 // com.woltlab.wcf.message
443 $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.message')
444 ->appendChildren([
445 BooleanFormField::create('enableToc')
446 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.message.enableToc')
447 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.message.enableToc.description')
448 ]);
449 $this->definitionElementChildren['com.woltlab.wcf.message'] = ['enableToc' => 0];
450
451 // com.woltlab.wcf.notification.objectType
452 $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.notification.objectType')
453 ->appendChildren([
454 TextFormField::create('notificationObjectTypeCategory')
455 ->objectProperty('category')
456 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.notification.objectType.category')
457 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.notification.objectType.category.description')
458 ->addValidator(FormFieldValidatorUtil::getDotSeparatedStringValidator(
459 'wcf.acp.pip.objectType.com.woltlab.wcf.notification.objectType.category',
460 3
461 )),
462
463 BooleanFormField::create('notificationObjectTypeSupportsReactions')
464 ->objectProperty('supportsReactions')
465 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.notification.objectType.supportsReactions')
466 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.notification.objectType.supportsReactions.description')
467 ]);
468 $this->definitionElementChildren['com.woltlab.wcf.notification.objectType'] = [
469 'category' => 0,
470 'supportsReactions' => 0
471 ];
472
473 // com.woltlab.wcf.rebuildData
474 $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.rebuildData')
475 ->appendChild(
476 IntegerFormField::create('rebuildDataNiceValue')
477 ->objectProperty('nicevalue')
478 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.rebuildData.niceValue')
479 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.rebuildData.niceValue.description')
480 ->nullable()
481 );
482 $this->definitionElementChildren['com.woltlab.wcf.rebuildData'] = ['nicevalue' => null];
483
484 // com.woltlab.wcf.searchableObjectType
485 $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.searchableObjectType')
486 ->appendChild(
487 TextFormField::create('searchableObjectTypeSearchIndex')
488 ->objectProperty('searchindex')
489 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.searchableObjectType.searchIndex')
490 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.searchableObjectType.searchIndex.description')
491 ->required()
492 ->addValidator(new FormFieldValidator('tableName', function(TextFormField $formField) {
493 if ($formField->getValue()) {
494 if (preg_match('~^(?P<app>[A-z]+)1_[A-z_]+$~', $formField->getValue(), $match)) {
495 if (!ApplicationHandler::getInstance()->getApplication($match['app'])) {
496 $formField->addValidationError(
497 new FormFieldValidationError(
498 'unknownApp',
499 'wcf.acp.pip.objectType.com.woltlab.wcf.searchableObjectType.searchIndex.error.unknownApp',
500 ['app' => $match['app']]
501 )
502 );
503 }
504 }
505 else {
506 $formField->addValidationError(
507 new FormFieldValidationError(
508 'invalid',
509 'wcf.acp.pip.objectType.com.woltlab.wcf.searchableObjectType.searchIndex.error.invalid'
510 )
511 );
512 }
513 }
514 }))
515 );
516 $this->definitionElementChildren['com.woltlab.wcf.searchableObjectType'] = ['searchindex'];
517
518 // com.woltlab.wcf.sitemap.object
519 $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.sitemap.object')
520 ->appendChildren([
521 FloatFormField::create('sitemapObjectPriority')
522 ->objectProperty('priority')
523 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.priority')
524 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.priority.description')
525 ->required()
526 ->minimum(0.0)
527 ->maximum(1.0)
528 ->step(0.1)
529 ->value(0.5),
530
531 SingleSelectionFormField::create('sitemapObjectchangeFreq')
532 ->objectProperty('changeFreq')
533 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.changeFreq')
534 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.changeFreq.description')
535 ->options([
536 'always' => 'always',
537 'hourly' => 'hourly',
538 'daily' => 'daily',
539 'weekly' => 'weekly',
540 'monthly' => 'monthly',
541 'yearly' => 'yearly',
542 'never' => 'never'
543 ])
544 ->required(),
545
546 IntegerFormField::create('sitemapObjectRebuildTime')
547 ->objectProperty('rebuildTime')
548 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.rebuildTime')
549 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.sitemap.object.rebuildTime.description')
550 ->suffix('wcf.acp.option.suffix.seconds')
551 ->required()
552 ->minimum(0)
553 ]);
554 $this->definitionElementChildren['com.woltlab.wcf.sitemap.object'] = ['priority', 'changeFreq', 'rebuildTime'];
555
556 // com.woltlab.wcf.statDailyHandler
557 $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.statDailyHandler')
558 ->appendChildren([
559 TextFormField::create('statDailyHandlerCategoryName')
560 ->objectProperty('categoryname')
561 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.categoryName')
562 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.categoryName.description')
563 ->addValidator(FormFieldValidatorUtil::getDotSeparatedStringValidator(
564 'wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.categoryName'
565 )),
566
567 BooleanFormField::create('statDailyHandlerIsDefault')
568 ->objectProperty('default')
569 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.isDefault')
570 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.statDailyHandler.isDefault.description')
571 ]);
572 $this->definitionElementChildren['com.woltlab.wcf.statDailyHandler'] = [
573 'categoryname' => '',
574 'default' => 0
575 ];
576
577 // com.woltlab.wcf.tagging.taggableObject
578 $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.tagging.taggableObject')
579 ->appendChildren([
580 OptionFormField::create('taggingTaggableObjectOptions')
581 ->objectProperty('options')
582 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.tagging.taggableObject.options.description')
583 ->packageIDs(array_merge(
584 [$this->installation->getPackage()->packageID],
585 array_keys($this->installation->getPackage()->getAllRequiredPackages())
586 )),
587
588 UserGroupOptionFormField::create('taggingTaggableObjectPermissions')
589 ->objectProperty('permissions')
590 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.tagging.taggableObject.permissions.description')
591 ->packageIDs(array_merge(
592 [$this->installation->getPackage()->packageID],
593 array_keys($this->installation->getPackage()->getAllRequiredPackages())
594 ))
595 ]);
596 $this->definitionElementChildren['com.woltlab.wcf.tagging.taggableObject'] = [
597 'options' => '',
598 'permissions' => ''
599 ];
600
601 // com.woltlab.wcf.user.activityPointEvent
602 $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.user.activityPointEvent')
603 ->appendChild(
604 IntegerFormField::create('userActivityPointEventPoints')
605 ->objectProperty('points')
606 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.user.activityPointEvent.points')
607 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.user.activityPointEvent.points.description')
608 ->minimum(0)
609 ->required()
610 );
611 $this->definitionElementChildren['com.woltlab.wcf.user.activityPointEvent'] = ['points'];
612
613 // com.woltlab.wcf.user.recentActivityEvent
614 $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.user.recentActivityEvent')
615 ->appendChild(
616 BooleanFormField::create('userRecentActivityEventSupportsReactions')
617 ->objectProperty('supportsReactions')
618 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.user.recentActivityEvent.supportsReactions')
619 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.user.recentActivityEvent.supportsReactions.description')
620 );
621 $this->definitionElementChildren['com.woltlab.wcf.user.recentActivityEvent'] = ['supportsReactions' => 0];
622
623 // com.woltlab.wcf.versionTracker.objectType
624 $this->getObjectTypeDefinitionDataContainer($form, 'com.woltlab.wcf.versionTracker.objectType')
625 ->appendChildren([
626 TextFormField::create('versionTrackerObjectTypeTableName')
627 ->objectProperty('tableName')
628 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tableName')
629 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tableName.description')
630 ->required()
631 ->addValidator(new FormFieldValidator('tableExists', function(TextFormField $formField) {
632 if ($formField->getValue()) {
633 $value = ApplicationHandler::insertRealDatabaseTableNames($formField->getValue());
634
635 if (!in_array($value, WCF::getDB()->getEditor()->getTableNames())) {
636 $formField->addValidationError(new FormFieldValidationError(
637 'nonExistent',
638 'wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tableName.error.nonExistent',
639 ['tableName' => $value]
640 ));
641 }
642 }
643 })),
644
645 TextFormField::create('versionTrackerObjectTypeTablePrimaryKey')
646 ->objectProperty('tablePrimaryKey')
647 ->label('wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tablePrimaryKey')
648 ->description('wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tablePrimaryKey.description')
649 ->required()
650 ->addValidator(new FormFieldValidator('columnExists', function(TextFormField $formField) {
651 if ($formField->getValue()) {
652 /** @var TextFormField $tableName */
653 $tableName = $formField->getDocument()->getNodeById('versionTrackerObjectTypeTableName');
654
655 if (empty($tableName->getValidationErrors())) {
656 // table name has already been validated and table exists
657 $columns = WCF::getDB()->getEditor()->getColumns(ApplicationHandler::insertRealDatabaseTableNames($tableName->getValue()));
658
659 foreach ($columns as $column) {
660 if ($column['name'] === $formField->getValue()) {
661 if ($column['data']['key'] !== 'PRIMARY') {
662 $formField->addValidationError(new FormFieldValidationError(
663 'noPrimaryColumn',
664 'wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tablePrimaryKey.error.noPrimaryColumn'
665 ));
666 }
667
668 return;
669 }
670 }
671
672 $formField->addValidationError(new FormFieldValidationError(
673 'nonExistent',
674 'wcf.acp.pip.objectType.com.woltlab.wcf.versionTracker.objectType.tablePrimaryKey.error.nonExistent'
675 ));
676 }
677 }
678 })),
679 ]);
680 $this->definitionElementChildren['com.woltlab.wcf.versionTracker.objectType'] = ['tableName', 'tablePrimaryKey'];
681 }
682
683 /**
684 * @inheritDoc
685 * @since 3.2
686 */
687 public function getElementIdentifier(\DOMElement $element) {
688 return sha1(
689 $element->getElementsByTagName('name')->item(0)->nodeValue . '/' .
690 $element->getElementsByTagName('definitionname')->item(0)->nodeValue
691 );
692 }
693
694 /**
695 * @inheritDoc
696 * @since 3.2
697 */
698 protected function getEmptyXml() {
699 return <<<XML
700 <?xml version="1.0" encoding="UTF-8"?>
701 <data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/vortex/objectType.xsd">
702 <import></import>
703 </data>
704 XML;
705 }
706
707 /**
708 * @inheritDoc
709 * @since 3.2
710 */
711 public function getEntryList() {
712 $xml = $this->getProjectXml();
713 $xpath = $xml->xpath();
714
715 $entryList = new DevtoolsPipEntryList();
716 $this->setEntryListKeys($entryList);
717
718 /** @var \DOMElement $element */
719 foreach ($this->getImportElements($xpath) as $element) {
720 $entryList->addEntry($this->getElementIdentifier($element), [
721 'name' => $element->getElementsByTagName('name')->item(0)->nodeValue,
722 'definitionName' => $element->getElementsByTagName('definitionname')->item(0)->nodeValue
723 ]);
724 }
725
726 return $entryList;
727 }
728
729 /**
730 * @inheritDoc
731 * @since 3.2
732 */
733 protected function setEntryListKeys(IDevtoolsPipEntryList $entryList) {
734 $entryList->setKeys([
735 'name' => 'wcf.acp.pip.objectType.objectType',
736 'definitionName' => 'wcf.acp.pip.objectType.definitionName'
737 ]);
738 }
739
740 /**
741 * Returns a form container for the object type definition-specific fields
742 * of the the object type definition with the given name.
743 *
744 * The returned form container is already appended to the given form and
745 * has a dependency on the `definitionName` field so that the form container
746 * is only shown for the relevant object type definition.
747 *
748 * @param IFormDocument $form
749 * @param string $definitionName
750 * @return FormContainer
751 * @since 3.2
752 */
753 public function getObjectTypeDefinitionDataContainer(IFormDocument $form, $definitionName) {
754 /** @var SingleSelectionFormField $definitionNameField */
755 $definitionIDField = $form->getNodeById('definitionID');
756
757 $definitionPieces = explode('.', $definitionName);
758
759 $formContainer = FormContainer::create(lcfirst(implode('', array_map('ucfirst', $definitionPieces))) . 'Fields')
760 ->label('wcf.acp.pip.objectType.' . $definitionName . '.data.title')
761 ->addDependency(
762 ValueFormFieldDependency::create('definitionID')
763 ->field($definitionIDField)
764 ->values([ObjectTypeCache::getInstance()->getDefinitionByName($definitionName)->definitionID])
765 );
766
767 $form->appendChild($formContainer);
768
769 return $formContainer;
770 }
771
772 /**
773 * @inheritDoc
774 * @since 3.2
775 */
776 protected function doCreateXmlElement(\DOMDocument $document, IFormDocument $form) {
777 $data = $form->getData()['data'];
778 $definitionName = ObjectTypeCache::getInstance()->getDefinition($data['definitionID'])->definitionName;
779
780 $objectType = $document->createElement($this->tagName);
781 $objectType->appendChild($document->createElement('name', $data['name']));
782 $objectType->appendChild($document->createElement('definitionname', $definitionName));
783
784 $this->appendElementChildren(
785 $objectType,
786 ['classname' => ''],
787 $form
788 );
789
790 if (isset($this->definitionElementChildren[$definitionName])) {
791 $this->appendElementChildren(
792 $objectType,
793 $this->definitionElementChildren[$definitionName],
794 $form
795 );
796 }
797
798 return $objectType;
799 }
800
801 /**
802 * Adds bulk processing action-related fields to the given form for the given bulk
803 * processing action object type definition.
804 *
805 * @param IFormDocument $form
806 * @param string $objectTypeDefinition
807 */
808 public function addBulkProcessingActionFields(IFormDocument $form, $objectTypeDefinition) {
809 $definitionPieces = explode('.', $objectTypeDefinition);
810 $definitionIdString = implode('', array_map('ucfirst', $definitionPieces));
811
812 $this->getObjectTypeDefinitionDataContainer($form, $objectTypeDefinition)
813 ->appendChildren([
814 TextFormField::create('bulkProcessing' . $definitionIdString . 'Action')
815 ->objectProperty('action')
816 ->label('wcf.acp.pip.objectType.bulkProcessing.action')
817 ->description('wcf.acp.pip.objectType.bulkProcessing.action.description')
818 ->addValidator(new FormFieldValidator('format', function(TextFormField $formField) {
819 if (!preg_match('~^[a-z][A-z]+$~', $formField->getValue())) {
820 $formField->addValidationError(
821 new FormFieldValidationError(
822 'format',
823 'wcf.acp.pip.objectType.bulkProcessing.action.error.format'
824 )
825 );
826 }
827 })),
828
829 OptionFormField::create('bulkProcessing' . $definitionIdString . 'Options')
830 ->objectProperty('options')
831 ->description('wcf.acp.pip.objectType.bulkProcessing.action.options.description')
832 ->packageIDs(array_merge(
833 [$this->installation->getPackage()->packageID],
834 array_keys($this->installation->getPackage()->getAllRequiredPackages())
835 )),
836
837 UserGroupOptionFormField::create('bulkProcessing' . $definitionIdString . 'Permissions')
838 ->objectProperty('permissions')
839 ->description('wcf.acp.pip.objectType.bulkProcessing.action.permissions.description')
840 ->packageIDs(array_merge(
841 [$this->installation->getPackage()->packageID],
842 array_keys($this->installation->getPackage()->getAllRequiredPackages())
843 ))
844 ]);
845 $this->definitionElementChildren[$objectTypeDefinition] = [
846 'action' => '',
847 'options' => '',
848 'permissions' => ''
849 ];
850 }
851
852 /**
853 * Adds all condition specific fields to the given form container.
854 *
855 * @param IFormContainer $dataContainer
856 * @param string $objectTypeDefinition
857 * @param bool $addConditionObject
858 * @param bool $addConditionGroup
859 * @since 3.2
860 */
861 public function addConditionFields(IFormContainer $dataContainer, $objectTypeDefinition, $addConditionObject = true, $addConditionGroup = true) {
862 $prefix = preg_replace('~Fields$~', '', $dataContainer->getId());
863
864 $this->definitionElementChildren[$objectTypeDefinition] = [];
865
866 if ($addConditionObject) {
867 $dataContainer->appendChild(
868 TextFormField::create($prefix . 'ConditionObject')
869 ->objectProperty('conditionobject')
870 ->label('wcf.acp.pip.objectType.condition.conditionObject')
871 ->description('wcf.acp.pip.objectType.condition.conditionObject.description')
872 ->required()
873 ->addValidator(FormFieldValidatorUtil::getDotSeparatedStringValidator(
874 'wcf.acp.pip.objectType.condition.conditionObject',
875 4
876 ))
877 );
878 $this->definitionElementChildren[$objectTypeDefinition][] = 'conditionobject';
879 }
880
881 if ($addConditionGroup) {
882 $dataContainer->appendChild(
883 TextFormField::create($prefix . 'ConditionGroup')
884 ->objectProperty('conditiongroup')
885 ->label('wcf.acp.pip.objectType.condition.conditionGroup')
886 ->description('wcf.acp.pip.objectType.condition.conditionGroup.description')
887 ->addValidator(new FormFieldValidator('format', function(TextFormField $formField) {
888 if ($formField->getValue() && !preg_match('~^[a-z][A-z]+$~', $formField->getValue())) {
889 $formField->addValidationError(
890 new FormFieldValidationError(
891 'format',
892 'wcf.acp.pip.objectType.condition.conditionGroup.error.format'
893 )
894 );
895 }
896 }))
897 );
898 $this->definitionElementChildren[$objectTypeDefinition]['conditiongroup'] = '';
899 }
900
901 // classes extending `AbstractIntegerCondition`
902 $integerConditions = [];
903 foreach (ApplicationHandler::getInstance()->getApplications() as $application) {
904 $conditionDir = $application->getPackage()->getAbsolutePackageDir() . 'lib/system/condition/';
905
906 if (file_exists($conditionDir)) {
907 $directory = DirectoryUtil::getInstance($conditionDir);
908 $conditionList = $directory->getFiles(SORT_ASC, new Regex('Condition\.class\.php$'));
909
910 /** @var string $condition */
911 foreach ($conditionList as $condition) {
912 $pathPieces = explode('/', str_replace($conditionDir, '', $condition));
913 $filename = array_pop($pathPieces);
914
915 $className = $application->getAbbreviation() . '\system\condition\\';
916 if (!empty($pathPieces)) {
917 $className .= implode('\\', $pathPieces) . '\\';
918 }
919 $className .= basename($filename, '.class.php');
920 if (class_exists($className) && is_subclass_of($className, AbstractIntegerCondition::class)) {
921 $reflection = new \ReflectionClass($className);
922 if ($reflection->isInstantiable()) {
923 $integerConditions[] = $className;
924 }
925 }
926 }
927 }
928 }
929
930 /** @var TextFormField $className */
931 $className = $dataContainer->getDocument()->getNodeById('className');
932
933 // `UserGroupCondition`
934 $dataContainer->appendChild(
935 BooleanFormField::create($prefix . 'UserGroupIncludeGuests')
936 ->objectProperty('includeguests')
937 ->label('wcf.acp.pip.objectType.condition.userGroup.includeGuests')
938 ->description('wcf.acp.pip.objectType.condition.userGroup.includeGuests.description')
939 ->addDependency(
940 ValueFormFieldDependency::create('className')
941 ->field($className)
942 ->values([UserGroupCondition::class])
943 )
944 );
945 $this->definitionElementChildren[$objectTypeDefinition]['includeguests'] = 0;
946
947 // `UserIntegerPropertyCondition`
948 $dataContainer->appendChild(
949 $this->getIntegerConditionPropertyNameField(
950 $className,
951 UserIntegerPropertyCondition::class,
952 $prefix . 'UserIntegerPropertyName',
953 'wcf' . WCF_N . '_user'
954 )->required()
955 );
956 $this->definitionElementChildren[$objectTypeDefinition]['propertyname'] = '';
957
958 // `UserTimestampPropertyCondition`
959 $dataContainer->appendChild(
960 $this->getIntegerConditionPropertyNameField(
961 $className,
962 UserTimestampPropertyCondition::class,
963 $prefix . 'UserTimestampPropertyName',
964 'wcf' . WCF_N . '_user'
965 )->required()
966 );
967 // already added above:
968 // $this->definitionElementChildren[$objectTypeDefinition]['propertyname'] = '';
969
970 $parameters = [
971 'dataContainer' => $dataContainer,
972 'prefix' => $prefix
973 ];
974 EventHandler::getInstance()->fireAction($this, 'addConditionFields', $parameters);
975
976 // integer property fields should be shown last
977 $dataContainer->appendChildren([
978 IntegerFormField::create($prefix . 'IntegerMinValue')
979 ->objectProperty('minvalue')
980 ->label('wcf.acp.pip.objectType.condition.integer.minValue')
981 ->description('wcf.acp.pip.objectType.condition.integer.minValue.description')
982 ->nullable()
983 ->addDependency(
984 ValueFormFieldDependency::create('className')
985 ->field($className)
986 ->values($integerConditions)
987 ),
988 IntegerFormField::create($prefix . 'IntegerMaxValue')
989 ->objectProperty('maxvalue')
990 ->label('wcf.acp.pip.objectType.condition.integer.maxValue')
991 ->description('wcf.acp.pip.objectType.condition.integer.maxValue.description')
992 ->nullable()
993 ->addDependency(
994 ValueFormFieldDependency::create('className')
995 ->field($className)
996 ->values($integerConditions)
997 )
998 ]);
999 $this->definitionElementChildren[$objectTypeDefinition]['minvalue'] = null;
1000 $this->definitionElementChildren[$objectTypeDefinition]['maxvalue'] = null;
1001 }
1002
1003 /**
1004 * Returns a form field to enter the name of an integer property for an
1005 * integer condition.
1006 *
1007 * @param TextFormField $classNameField class name field on which the visibility of the created field depends
1008 * @param string $conditionClass name of the PHP class the field is created for
1009 * @param string $id id of the created field
1010 * @param string $databaseTableName name of the database table that stores the conditioned objects
1011 * @return TextFormField
1012 */
1013 public function getIntegerConditionPropertyNameField(TextFormField $classNameField, $conditionClass, $id, $databaseTableName) {
1014 return TextFormField::create($id)
1015 ->objectProperty('propertyname')
1016 ->label('wcf.acp.pip.objectType.integerCondition.propertyName')
1017 ->description(
1018 'wcf.acp.pip.objectType.integerCondition.propertyName.description',
1019 ['tableName' => $databaseTableName]
1020 )
1021 ->addDependency(
1022 ValueFormFieldDependency::create('className')
1023 ->field($classNameField)
1024 ->values([$conditionClass])
1025 )
1026 ->addValidator(new FormFieldValidator('userTableIntegerColumn', function(TextFormField $formField) use ($databaseTableName) {
1027 if ($formField->getSaveValue()) {
1028 $columns = WCF::getDB()->getEditor()->getColumns($databaseTableName);
1029
1030 foreach ($columns as $column) {
1031 if ($column['name'] === $formField->getValue()) {
1032 if ($column['data']['type'] !== 'int') {
1033 $formField->addValidationError(new FormFieldValidationError(
1034 'noIntegerColumn',
1035 'wcf.acp.pip.objectType.integerCondition.propertyName.error.noIntegerColumn',
1036 ['tableName' => $databaseTableName]
1037 ));
1038 }
1039
1040 return;
1041 }
1042 }
1043
1044 $formField->addValidationError(new FormFieldValidationError(
1045 'nonExistent',
1046 'wcf.acp.pip.objectType.integerCondition.propertyName.error.nonExistent',
1047 ['tableName' => $databaseTableName]
1048 ));
1049 }
1050 }));
1051 }
1052 }