966950b5249754ddb48be650c33cfa30ce0d48a8
[GitHub/WoltLab/WCF.git] /
1 <?php
2
3 namespace wcf\system\package\plugin;
4
5 use wcf\data\acp\template\ACPTemplate;
6 use wcf\data\acp\template\ACPTemplateList;
7 use wcf\data\template\listener\TemplateListenerEditor;
8 use wcf\data\template\listener\TemplateListenerList;
9 use wcf\data\template\Template;
10 use wcf\data\template\TemplateList;
11 use wcf\system\cache\builder\TemplateListenerCodeCacheBuilder;
12 use wcf\system\devtools\pip\IDevtoolsPipEntryList;
13 use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
14 use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
15 use wcf\system\form\builder\container\FormContainer;
16 use wcf\system\form\builder\data\processor\CustomFormDataProcessor;
17 use wcf\system\form\builder\field\dependency\ValueFormFieldDependency;
18 use wcf\system\form\builder\field\IntegerFormField;
19 use wcf\system\form\builder\field\MultilineTextFormField;
20 use wcf\system\form\builder\field\option\OptionFormField;
21 use wcf\system\form\builder\field\SingleSelectionFormField;
22 use wcf\system\form\builder\field\TextFormField;
23 use wcf\system\form\builder\field\user\group\option\UserGroupOptionFormField;
24 use wcf\system\form\builder\field\validation\FormFieldValidationError;
25 use wcf\system\form\builder\field\validation\FormFieldValidator;
26 use wcf\system\form\builder\IFormDocument;
27 use wcf\system\WCF;
28 use wcf\util\StringUtil;
29
30 /**
31 * Installs, updates and deletes template listeners.
32 *
33 * @author Alexander Ebert, Matthias Schmidt
34 * @copyright 2001-2019 WoltLab GmbH
35 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
36 * @package WoltLabSuite\Core\System\Package\Plugin
37 */
38 class TemplateListenerPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements
39 IGuiPackageInstallationPlugin
40 {
41 use TXmlGuiPackageInstallationPlugin {
42 setEntryData as defaultSetEntryData;
43 }
44
45 /**
46 * @inheritDoc
47 */
48 public $className = TemplateListenerEditor::class;
49
50 /**
51 * @inheritDoc
52 */
53 protected function handleDelete(array $items)
54 {
55 $sql = "DELETE FROM wcf" . WCF_N . "_" . $this->tableName . "
56 WHERE packageID = ?
57 AND environment = ?
58 AND eventName = ?
59 AND name = ?
60 AND templateName = ?";
61 $statement = WCF::getDB()->prepareStatement($sql);
62 foreach ($items as $item) {
63 $statement->execute([
64 $this->installation->getPackageID(),
65 $item['elements']['environment'],
66 $item['elements']['eventname'],
67 $item['attributes']['name'],
68 $item['elements']['templatename'],
69 ]);
70 }
71 }
72
73 /**
74 * @inheritDoc
75 */
76 protected function prepareImport(array $data)
77 {
78 $niceValue = isset($data['elements']['nice']) ? \intval($data['elements']['nice']) : 0;
79 if ($niceValue < -128) {
80 $niceValue = -128;
81 } elseif ($niceValue > 127) {
82 $niceValue = 127;
83 }
84
85 return [
86 'environment' => $data['elements']['environment'],
87 'eventName' => $data['elements']['eventname'],
88 'niceValue' => $niceValue,
89 'name' => $data['attributes']['name'],
90 'options' => isset($data['elements']['options']) ? StringUtil::normalizeCsv($data['elements']['options']) : '',
91 'permissions' => isset($data['elements']['permissions']) ? StringUtil::normalizeCsv($data['elements']['permissions']) : '',
92 'templateCode' => $data['elements']['templatecode'],
93 'templateName' => $data['elements']['templatename'],
94 ];
95 }
96
97 /**
98 * @inheritDoc
99 */
100 protected function findExistingItem(array $data)
101 {
102 $sql = "SELECT *
103 FROM wcf" . WCF_N . "_" . $this->tableName . "
104 WHERE packageID = ?
105 AND name = ?
106 AND templateName = ?
107 AND eventName = ?
108 AND environment = ?";
109 $parameters = [
110 $this->installation->getPackageID(),
111 $data['name'],
112 $data['templateName'],
113 $data['eventName'],
114 $data['environment'],
115 ];
116
117 return [
118 'sql' => $sql,
119 'parameters' => $parameters,
120 ];
121 }
122
123 /**
124 * @inheritDoc
125 */
126 protected function cleanup()
127 {
128 // clear cache immediately
129 TemplateListenerCodeCacheBuilder::getInstance()->reset();
130 }
131
132 /**
133 * @inheritDoc
134 * @since 3.1
135 */
136 public static function getSyncDependencies()
137 {
138 return [];
139 }
140
141 /**
142 * @inheritDoc
143 * @since 5.2
144 */
145 protected function addFormFields(IFormDocument $form)
146 {
147 $ldq = \preg_quote(WCF::getTPL()->getCompiler()->getLeftDelimiter(), '~');
148 $rdq = \preg_quote(WCF::getTPL()->getCompiler()->getRightDelimiter(), '~');
149
150 $getEvents = static function ($templateList) use ($ldq, $rdq) {
151 $templateEvents = [];
152 /** @var ACPTemplate|Template $template */
153 foreach ($templateList as $template) {
154 if (
155 \preg_match_all(
156 "~{$ldq}event\\ name\\=\\'(?<event>[\\w]+)\\'{$rdq}~",
157 $template->getSource(),
158 $matches
159 )
160 ) {
161 $templates[$template->templateName] = $template->templateName;
162
163 foreach ($matches['event'] as $event) {
164 if (!isset($templateEvents[$template->templateName])) {
165 $templateEvents[$template->templateName] = [];
166 }
167
168 $templateEvents[$template->templateName][] = $event;
169 }
170 }
171 }
172
173 foreach ($templateEvents as &$events) {
174 \sort($events);
175 }
176 unset($events);
177
178 return $templateEvents;
179 };
180
181 $templateList = new TemplateList();
182 $templateList->getConditionBuilder()->add(
183 'template.packageID IN (?)',
184 [
185 \array_merge(
186 [$this->installation->getPackage()->packageID],
187 \array_keys($this->installation->getPackage()->getAllRequiredPackages())
188 ),
189 ]
190 );
191 $templateList->getConditionBuilder()->add('template.templateGroupID IS NULL');
192 $templateList->sqlOrderBy = 'template.templateName ASC';
193 $templateList->readObjects();
194
195 $templateEvents = $getEvents($templateList);
196
197 $acpTemplateList = new ACPTemplateList();
198 $acpTemplateList->getConditionBuilder()->add(
199 'acp_template.packageID IN (?)',
200 [
201 \array_merge(
202 [$this->installation->getPackage()->packageID],
203 \array_keys($this->installation->getPackage()->getAllRequiredPackages())
204 ),
205 ]
206 );
207 $acpTemplateList->sqlOrderBy = 'acp_template.templateName ASC';
208 $acpTemplateList->readObjects();
209
210 $acpTemplateEvents = $getEvents($acpTemplateList);
211
212 /** @var FormContainer $dataContainer */
213 $dataContainer = $form->getNodeById('data');
214
215 $dataContainer->appendChildren([
216 TextFormField::create('name')
217 ->label('wcf.acp.pip.templateListener.name')
218 ->description('wcf.acp.pip.templateListener.name.description')
219 ->required()
220 ->addValidator(new FormFieldValidator('format', static function (TextFormField $formField) {
221 if (!\preg_match('~^[a-z][A-z]+$~', $formField->getValue())) {
222 $formField->addValidationError(
223 new FormFieldValidationError(
224 'format',
225 'wcf.acp.pip.templateListener.name.error.format'
226 )
227 );
228 }
229 })),
230
231 SingleSelectionFormField::create('environment')
232 ->label('wcf.acp.pip.templateListener.environment')
233 ->description('wcf.acp.pip.templateListener.environment.description')
234 ->required()
235 ->options([
236 'admin' => 'admin',
237 'user' => 'user',
238 ])
239 ->value('user')
240 ->addValidator(new FormFieldValidator('uniqueness', function (SingleSelectionFormField $formField) {
241 /** @var TextFormField $nameField */
242 $nameField = $formField->getDocument()->getNodeById('name');
243
244 /** @var SingleSelectionFormField $templateNameFormField */
245 $templateNameFormField = $formField->getDocument()->getNodeById('frontendTemplateName');
246
247 /** @var SingleSelectionFormField $acpTemplateNameFormField */
248 $acpTemplateNameFormField = $formField->getDocument()->getNodeById('acpTemplateName');
249
250 if (
251 $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE
252 || $this->editedEntry->getAttribute('name') !== $nameField->getSaveValue()
253 || $this->editedEntry->getElementsByTagName('environment')->item(0)->nodeValue !== $formField->getSaveValue()
254 || (
255 $formField->getSaveValue() === 'admin'
256 && $this->editedEntry->getElementsByTagName('templatename')->item(0)->nodeValue !== $acpTemplateNameFormField->getSaveValue()
257 )
258 || (
259 $formField->getSaveValue() === 'user'
260 && $this->editedEntry->getElementsByTagName('templatename')->item(0)->nodeValue !== $templateNameFormField->getSaveValue()
261 )
262 ) {
263 $listenerList = new TemplateListenerList();
264 $listenerList->getConditionBuilder()->add(
265 'name = ?',
266 [$nameField->getSaveValue()]
267 );
268
269 if ($formField->getSaveValue() === 'admin') {
270 /** @var SingleSelectionFormField $templateNameField */
271 $templateNameField = $formField->getDocument()->getNodeById('acpTemplateName');
272
273 /** @var SingleSelectionFormField $eventNameField */
274 $eventNameField = $formField->getDocument()->getNodeById('acp_' . $templateNameField->getSaveValue() . '_eventName');
275 } else {
276 /** @var SingleSelectionFormField $templateNameField */
277 $templateNameField = $formField->getDocument()->getNodeById('frontendTemplateName');
278
279 /** @var SingleSelectionFormField $eventNameField */
280 $eventNameField = $formField->getDocument()->getNodeById($templateNameField->getSaveValue() . '_eventName');
281 }
282
283 $templateName = $templateNameField->getSaveValue();
284 $eventName = $eventNameField->getSaveValue();
285
286 $listenerList->getConditionBuilder()->add('templateName = ?', [$templateName]);
287 $listenerList->getConditionBuilder()->add('eventName = ?', [$eventName]);
288 $listenerList->getConditionBuilder()->add('environment = ?', [$formField->getSaveValue()]);
289
290 if ($listenerList->countObjects() > 0) {
291 $nameField->addValidationError(
292 new FormFieldValidationError(
293 'notUnique',
294 'wcf.acp.pip.templateListener.name.error.notUnique'
295 )
296 );
297 }
298 }
299 })),
300
301 SingleSelectionFormField::create('frontendTemplateName')
302 ->objectProperty('templatename')
303 ->label('wcf.acp.pip.templateListener.templateName')
304 ->description('wcf.acp.pip.templateListener.templateName.description')
305 ->required()
306 ->options(\array_combine(\array_keys($templateEvents), \array_keys($templateEvents)))
307 ->filterable()
308 ->addDependency(
309 ValueFormFieldDependency::create('environment')
310 ->fieldId('environment')
311 ->values(['user'])
312 ),
313
314 SingleSelectionFormField::create('acpTemplateName')
315 ->objectProperty('templatename')
316 ->label('wcf.acp.pip.templateListener.templateName')
317 ->description('wcf.acp.pip.templateListener.templateName.description')
318 ->required()
319 ->options(\array_combine(\array_keys($acpTemplateEvents), \array_keys($acpTemplateEvents)))
320 ->filterable()
321 ->addDependency(
322 ValueFormFieldDependency::create('environment')
323 ->fieldId('environment')
324 ->values(['admin'])
325 ),
326 ]);
327
328 /** @var SingleSelectionFormField $frontendTemplateName */
329 $frontendTemplateName = $form->getNodeById('frontendTemplateName');
330 foreach ($templateEvents as $templateName => $events) {
331 $dataContainer->appendChild(
332 SingleSelectionFormField::create($templateName . '_eventName')
333 ->objectProperty('eventname')
334 ->label('wcf.acp.pip.templateListener.eventName')
335 ->description('wcf.acp.pip.templateListener.eventName.description')
336 ->required()
337 ->options(\array_combine($events, $events))
338 ->addDependency(
339 ValueFormFieldDependency::create('templateName')
340 ->field($frontendTemplateName)
341 ->values([$templateName])
342 )
343 );
344 }
345
346 /** @var SingleSelectionFormField $acpTemplateName */
347 $acpTemplateName = $form->getNodeById('acpTemplateName');
348 foreach ($acpTemplateEvents as $templateName => $events) {
349 $dataContainer->appendChild(
350 SingleSelectionFormField::create('acp_' . $templateName . '_eventName')
351 ->objectProperty('eventname')
352 ->label('wcf.acp.pip.templateListener.eventName')
353 ->description('wcf.acp.pip.templateListener.eventName.description')
354 ->required()
355 ->options(\array_combine($events, $events))
356 ->addDependency(
357 ValueFormFieldDependency::create('acpTemplateName')
358 ->field($acpTemplateName)
359 ->values([$templateName])
360 )
361 );
362 }
363
364 $dataContainer->appendChildren([
365 MultilineTextFormField::create('templateCode')
366 ->objectProperty('templatecode')
367 ->label('wcf.acp.pip.templateListener.templateCode')
368 ->description('wcf.acp.pip.templateListener.templateCode.description')
369 ->required()
370 ->addFieldClass('monospace'),
371
372 IntegerFormField::create('niceValue')
373 ->objectProperty('nice')
374 ->label('wcf.acp.pip.templateListener.niceValue')
375 ->description('wcf.acp.pip.templateListener.niceValue.description')
376 ->nullable()
377 ->minimum(-128)
378 ->maximum(127),
379
380 OptionFormField::create()
381 ->description('wcf.acp.pip.templateListener.options.description')
382 ->packageIDs(\array_merge(
383 [$this->installation->getPackage()->packageID],
384 \array_keys($this->installation->getPackage()->getAllRequiredPackages())
385 )),
386
387 UserGroupOptionFormField::create()
388 ->description('wcf.acp.pip.templateListener.permissions.description')
389 ->packageIDs(\array_merge(
390 [$this->installation->getPackage()->packageID],
391 \array_keys($this->installation->getPackage()->getAllRequiredPackages())
392 )),
393 ]);
394
395 // ensure proper normalization of template code
396 $form->getDataHandler()->addProcessor(new CustomFormDataProcessor(
397 'templateCode',
398 static function (IFormDocument $document, array $parameters) {
399 $parameters['data']['templatecode'] = StringUtil::unifyNewlines(StringUtil::escapeCDATA($parameters['data']['templatecode']));
400
401 return $parameters;
402 }
403 ));
404 }
405
406 /**
407 * @inheritDoc
408 * @since 5.2
409 */
410 protected function fetchElementData(\DOMElement $element, $saveData)
411 {
412 $data = [
413 'environment' => $element->getElementsByTagName('environment')->item(0)->nodeValue,
414 'eventName' => $element->getElementsByTagName('eventname')->item(0)->nodeValue,
415 'name' => $element->getAttribute('name'),
416 'packageID' => $this->installation->getPackage()->packageID,
417 'templateCode' => $element->getElementsByTagName('templatecode')->item(0)->nodeValue,
418 'templateName' => $element->getElementsByTagName('templatename')->item(0)->nodeValue,
419 ];
420
421 $nice = $element->getElementsByTagName('nice')->item(0);
422 if ($nice !== null) {
423 $data['niceValue'] = $nice->nodeValue;
424 } elseif ($saveData) {
425 $data['niceValue'] = 0;
426 }
427
428 foreach (['options', 'permissions'] as $elementName) {
429 $optionalElement = $element->getElementsByTagName($elementName)->item(0);
430 if ($optionalElement !== null) {
431 $data[$elementName] = $optionalElement->nodeValue;
432 } elseif ($saveData) {
433 $data[$elementName] = '';
434 }
435 }
436
437 return $data;
438 }
439
440 /**
441 * @inheritDoc
442 * @since 5.2
443 */
444 public function getElementIdentifier(\DOMElement $element)
445 {
446 return \sha1(
447 $element->getElementsByTagName('templatename')->item(0)->nodeValue . '/'
448 . $element->getElementsByTagName('eventname')->item(0)->nodeValue . '/'
449 . $element->getElementsByTagName('environment')->item(0)->nodeValue . '/'
450 . $element->getAttribute('name')
451 );
452 }
453
454 /**
455 * @inheritDoc
456 * @since 5.2
457 */
458 public function setEntryData($identifier, IFormDocument $document)
459 {
460 if ($this->defaultSetEntryData($identifier, $document)) {
461 $data = $this->getElementData($this->getElementByIdentifier($this->getProjectXml(), $identifier));
462
463 switch ($data['environment']) {
464 case 'admin':
465 /** @var SingleSelectionFormField $templateName */
466 $templateName = $document->getNodeById('acpTemplateName');
467
468 /** @var SingleSelectionFormField $eventName */
469 $eventName = $document->getNodeById('acp_' . $data['templateName'] . '_eventName');
470 break;
471
472 case 'user':
473 /** @var SingleSelectionFormField $templateName */
474 $templateName = $document->getNodeById('frontendTemplateName');
475
476 /** @var SingleSelectionFormField $eventName */
477 $eventName = $document->getNodeById($data['templateName'] . '_eventName');
478 break;
479
480 default:
481 throw new \LogicException("Unknown enviornment '{$data['environment']}'.");
482 }
483
484 $templateName->value($data['templateName']);
485 $eventName->value($data['eventName']);
486
487 return true;
488 }
489
490 return false;
491 }
492
493 /**
494 * @inheritDoc
495 * @since 5.2
496 */
497 protected function setEntryListKeys(IDevtoolsPipEntryList $entryList)
498 {
499 $entryList->setKeys([
500 'name' => 'wcf.acp.pip.templateListener.name',
501 'templateName' => 'wcf.acp.pip.templateListener.templateName',
502 'eventName' => 'wcf.acp.pip.templateListener.eventName',
503 'environment' => 'wcf.acp.pip.templateListener.environment',
504 ]);
505 }
506
507 /**
508 * @inheritDoc
509 * @since 5.2
510 */
511 protected function prepareXmlElement(\DOMDocument $document, IFormDocument $form)
512 {
513 $data = $form->getData()['data'];
514
515 $listener = $document->createElement($this->tagName);
516 $listener->setAttribute('name', $data['name']);
517
518 $this->appendElementChildren(
519 $listener,
520 [
521 'environment',
522 'templatename',
523 'eventname',
524 'templatecode' => [
525 'cdata' => true,
526 ],
527 'nice' => 0,
528 'options' => '',
529 'permissions' => '',
530 ],
531 $form
532 );
533
534 return $listener;
535 }
536
537 /**
538 * @inheritDoc
539 * @since 5.2
540 */
541 protected function prepareDeleteXmlElement(\DOMElement $element)
542 {
543 $templateListener = $element->ownerDocument->createElement($this->tagName);
544 $templateListener->setAttribute('name', $element->getAttribute('name'));
545
546 foreach (['environment', 'templatename', 'eventname'] as $childElement) {
547 $templateListener->appendChild($element->ownerDocument->createElement(
548 $childElement,
549 $element->getElementsByTagName($childElement)->item(0)->nodeValue
550 ));
551 }
552
553 return $templateListener;
554 }
555
556 /**
557 * @inheritDoc
558 * @since 5.2
559 */
560 protected function deleteObject(\DOMElement $element)
561 {
562 $elements = [];
563 foreach (['environment', 'templatename', 'eventname'] as $childElement) {
564 $elements[$childElement] = $element->getElementsByTagName($childElement)->item(0)->nodeValue;
565 }
566
567 $this->handleDelete([
568 [
569 'attributes' => ['name' => $element->getAttribute('name')],
570 'elements' => $elements,
571 ],
572 ]);
573 }
574 }