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