Commit | Line | Data |
---|---|---|
11ade432 | 1 | <?php |
a9229942 | 2 | |
11ade432 | 3 | namespace wcf\data; |
a9229942 | 4 | |
11ade432 AE |
5 | use wcf\system\event\EventHandler; |
6 | use wcf\system\exception\PermissionDeniedException; | |
4e877829 | 7 | use wcf\system\exception\SystemException; |
429e91b8 | 8 | use wcf\system\exception\UserInputException; |
b6628fdc | 9 | use wcf\system\request\RequestHandler; |
11ade432 | 10 | use wcf\system\WCF; |
857ed85d | 11 | use wcf\util\ArrayUtil; |
217e4987 | 12 | use wcf\util\JSON; |
11ade432 AE |
13 | use wcf\util\StringUtil; |
14 | ||
15 | /** | |
16 | * Default implementation for DatabaseObject-related actions. | |
a9229942 TD |
17 | * |
18 | * @author Alexander Ebert | |
19 | * @copyright 2001-2020 WoltLab GmbH | |
20 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> | |
21 | * @package WoltLabSuite\Core\Data | |
11ade432 | 22 | */ |
a9229942 TD |
23 | abstract class AbstractDatabaseObjectAction implements IDatabaseObjectAction, IDeleteAction |
24 | { | |
25 | /** | |
26 | * pending action | |
27 | * @var string | |
28 | */ | |
29 | protected $action = ''; | |
30 | ||
31 | /** | |
32 | * object editor class name | |
33 | * @var string | |
34 | */ | |
35 | protected $className = ''; | |
36 | ||
37 | /** | |
38 | * list of object ids | |
39 | * @var int[] | |
40 | */ | |
41 | protected $objectIDs = []; | |
42 | ||
43 | /** | |
44 | * list of object editors | |
45 | * @var DatabaseObjectEditor[] | |
46 | */ | |
47 | protected $objects = []; | |
48 | ||
49 | /** | |
50 | * multi-dimensional array of parameters required by an action | |
51 | * @var array | |
52 | */ | |
53 | protected $parameters = []; | |
54 | ||
55 | /** | |
56 | * list of permissions required to create objects | |
57 | * @var string[] | |
58 | */ | |
59 | protected $permissionsCreate = []; | |
60 | ||
61 | /** | |
62 | * list of permissions required to delete objects | |
63 | * @var string[] | |
64 | */ | |
65 | protected $permissionsDelete = []; | |
66 | ||
67 | /** | |
68 | * list of permissions required to update objects | |
69 | * @var string[] | |
70 | */ | |
71 | protected $permissionsUpdate = []; | |
72 | ||
73 | /** | |
74 | * disallow requests for specified methods if the origin is not the ACP | |
75 | * @var string[] | |
76 | */ | |
77 | protected $requireACP = []; | |
78 | ||
79 | /** | |
80 | * Resets cache if any of the listed actions is invoked | |
81 | * @var string[] | |
82 | */ | |
83 | protected $resetCache = ['create', 'delete', 'toggle', 'update', 'updatePosition']; | |
84 | ||
85 | /** | |
86 | * values returned by executed action | |
87 | * @var mixed | |
88 | */ | |
89 | protected $returnValues; | |
90 | ||
91 | /** | |
92 | * allows guest access for all specified methods, by default guest access | |
93 | * is completely disabled | |
94 | * @var string[] | |
95 | */ | |
96 | protected $allowGuestAccess = []; | |
97 | ||
98 | const TYPE_INTEGER = 1; | |
99 | ||
100 | const TYPE_STRING = 2; | |
101 | ||
102 | const TYPE_BOOLEAN = 3; | |
103 | ||
104 | const TYPE_JSON = 4; | |
105 | ||
106 | const STRUCT_FLAT = 1; | |
107 | ||
108 | const STRUCT_ARRAY = 2; | |
109 | ||
110 | /** | |
111 | * Initialize a new DatabaseObject-related action. | |
112 | * | |
113 | * @param mixed[] $objects | |
114 | * @param string $action | |
115 | * @param array $parameters | |
116 | * @throws SystemException | |
117 | */ | |
118 | public function __construct(array $objects, $action, array $parameters = []) | |
119 | { | |
120 | // set class name | |
121 | if (empty($this->className)) { | |
122 | $className = \get_called_class(); | |
123 | ||
124 | if (\mb_substr($className, -6) == 'Action') { | |
125 | $this->className = \mb_substr($className, 0, -6) . 'Editor'; | |
126 | } | |
127 | } | |
128 | ||
129 | $indexName = \call_user_func([$this->className, 'getDatabaseTableIndexName']); | |
130 | $baseClass = \call_user_func([$this->className, 'getBaseClass']); | |
131 | ||
132 | foreach ($objects as $object) { | |
133 | if (\is_object($object)) { | |
134 | if ($object instanceof $this->className) { | |
135 | $this->objects[] = $object; | |
136 | } elseif ($object instanceof $baseClass) { | |
137 | $this->objects[] = new $this->className($object); | |
138 | } else { | |
139 | throw new SystemException('invalid value of parameter objects given'); | |
140 | } | |
141 | ||
142 | /** @noinspection PhpVariableVariableInspection */ | |
143 | $this->objectIDs[] = $object->{$indexName}; | |
144 | } else { | |
145 | $this->objectIDs[] = $object; | |
146 | } | |
147 | } | |
148 | ||
149 | $this->action = $action; | |
150 | $this->parameters = $parameters; | |
151 | ||
152 | // initialize further settings | |
153 | $this->__init($baseClass, $indexName); | |
154 | ||
155 | // fire event action | |
156 | EventHandler::getInstance()->fireAction($this, 'initializeAction'); | |
157 | } | |
158 | ||
159 | /** | |
160 | * This function can be overridden in children to perform custom initialization | |
161 | * of a DBOAction before the 'initializeAction' event is fired. | |
162 | * | |
163 | * @param string $baseClass | |
164 | * @param string $indexName | |
165 | */ | |
166 | protected function __init($baseClass, $indexName) | |
167 | { | |
168 | // does nothing | |
169 | } | |
170 | ||
171 | /** | |
172 | * @inheritDoc | |
173 | */ | |
174 | public function validateAction() | |
175 | { | |
176 | // validate if user is logged in | |
177 | if (!WCF::getUser()->userID && !\in_array($this->getActionName(), $this->allowGuestAccess)) { | |
178 | throw new PermissionDeniedException(); | |
179 | } elseif ( | |
180 | !RequestHandler::getInstance()->isACPRequest() && \in_array( | |
181 | $this->getActionName(), | |
182 | $this->requireACP | |
183 | ) | |
184 | ) { | |
185 | // attempt to invoke method, but origin is not the ACP | |
186 | throw new PermissionDeniedException(); | |
187 | } | |
188 | ||
189 | // validate action name | |
190 | if (!\method_exists($this, $this->getActionName())) { | |
191 | throw new SystemException("unknown action '" . $this->getActionName() . "'"); | |
192 | } | |
193 | ||
194 | $actionName = 'validate' . StringUtil::firstCharToUpperCase($this->getActionName()); | |
195 | if (!\method_exists($this, $actionName)) { | |
196 | throw new PermissionDeniedException(); | |
197 | } | |
198 | ||
199 | // validate action | |
200 | $this->{$actionName}(); | |
201 | ||
202 | // fire event action | |
203 | EventHandler::getInstance()->fireAction($this, 'validateAction'); | |
204 | } | |
205 | ||
206 | /** | |
207 | * @inheritDoc | |
208 | */ | |
209 | public function executeAction() | |
210 | { | |
211 | // execute action | |
212 | if (!\method_exists($this, $this->getActionName())) { | |
213 | throw new SystemException("call to undefined function '" . $this->getActionName() . "'"); | |
214 | } | |
215 | ||
216 | $this->returnValues = $this->{$this->getActionName()}(); | |
217 | ||
218 | // reset cache | |
219 | if (\in_array($this->getActionName(), $this->resetCache)) { | |
220 | $this->resetCache(); | |
221 | } | |
222 | ||
223 | // fire event action | |
224 | EventHandler::getInstance()->fireAction($this, 'finalizeAction'); | |
225 | ||
226 | return $this->getReturnValues(); | |
227 | } | |
228 | ||
229 | /** | |
230 | * Resets cache of database object. | |
231 | */ | |
232 | protected function resetCache() | |
233 | { | |
234 | if (\is_subclass_of($this->className, IEditableCachedObject::class)) { | |
235 | \call_user_func([$this->className, 'resetCache']); | |
236 | } | |
237 | } | |
238 | ||
239 | /** | |
240 | * @inheritDoc | |
241 | */ | |
242 | public function getActionName() | |
243 | { | |
244 | return $this->action; | |
245 | } | |
246 | ||
247 | /** | |
248 | * @inheritDoc | |
249 | */ | |
250 | public function getObjectIDs() | |
251 | { | |
252 | return $this->objectIDs; | |
253 | } | |
254 | ||
255 | /** | |
256 | * Sets the database objects. | |
257 | * | |
258 | * @param DatabaseObject[] $objects | |
259 | */ | |
260 | public function setObjects(array $objects) | |
261 | { | |
262 | $this->objects = $objects; | |
263 | ||
264 | // update object IDs | |
265 | $this->objectIDs = []; | |
266 | foreach ($this->getObjects() as $object) { | |
267 | $this->objectIDs[] = $object->getObjectID(); | |
268 | } | |
269 | } | |
270 | ||
271 | /** | |
272 | * @inheritDoc | |
273 | */ | |
274 | public function getParameters() | |
275 | { | |
276 | return $this->parameters; | |
277 | } | |
278 | ||
279 | /** | |
280 | * @inheritDoc | |
281 | */ | |
282 | public function getReturnValues() | |
283 | { | |
284 | return [ | |
285 | 'actionName' => $this->action, | |
286 | 'objectIDs' => $this->getObjectIDs(), | |
287 | 'returnValues' => $this->returnValues, | |
288 | ]; | |
289 | } | |
290 | ||
291 | /** | |
292 | * Validates permissions and parameters. | |
293 | */ | |
294 | public function validateCreate() | |
295 | { | |
296 | // validate permissions | |
297 | if (\is_array($this->permissionsCreate) && !empty($this->permissionsCreate)) { | |
298 | WCF::getSession()->checkPermissions($this->permissionsCreate); | |
299 | } else { | |
300 | throw new PermissionDeniedException(); | |
301 | } | |
302 | } | |
303 | ||
304 | /** | |
305 | * @inheritDoc | |
306 | */ | |
307 | public function validateDelete() | |
308 | { | |
309 | // validate permissions | |
310 | if (\is_array($this->permissionsDelete) && !empty($this->permissionsDelete)) { | |
311 | WCF::getSession()->checkPermissions($this->permissionsDelete); | |
312 | } else { | |
313 | throw new PermissionDeniedException(); | |
314 | } | |
315 | ||
316 | // read objects | |
317 | if (empty($this->objects)) { | |
318 | $this->readObjects(); | |
319 | ||
320 | if (empty($this->objects)) { | |
321 | throw new UserInputException('objectIDs'); | |
322 | } | |
323 | } | |
324 | } | |
325 | ||
326 | /** | |
327 | * Validates permissions and parameters. | |
328 | */ | |
329 | public function validateUpdate() | |
330 | { | |
331 | // validate permissions | |
332 | if (\is_array($this->permissionsUpdate) && !empty($this->permissionsUpdate)) { | |
333 | WCF::getSession()->checkPermissions($this->permissionsUpdate); | |
334 | } else { | |
335 | throw new PermissionDeniedException(); | |
336 | } | |
337 | ||
338 | // read objects | |
339 | if (empty($this->objects)) { | |
340 | $this->readObjects(); | |
341 | ||
342 | if (empty($this->objects)) { | |
343 | throw new UserInputException('objectIDs'); | |
344 | } | |
345 | } | |
346 | } | |
347 | ||
348 | /** | |
349 | * Creates new database object. | |
350 | * | |
351 | * @return DatabaseObject | |
352 | */ | |
353 | public function create() | |
354 | { | |
355 | return \call_user_func([$this->className, 'create'], $this->parameters['data']); | |
356 | } | |
357 | ||
358 | /** | |
359 | * @inheritDoc | |
360 | */ | |
361 | public function delete() | |
362 | { | |
363 | if (empty($this->objects)) { | |
364 | $this->readObjects(); | |
365 | } | |
366 | ||
367 | // get ids | |
368 | $objectIDs = []; | |
369 | foreach ($this->getObjects() as $object) { | |
370 | $objectIDs[] = $object->getObjectID(); | |
371 | } | |
372 | ||
373 | // execute action | |
374 | return \call_user_func([$this->className, 'deleteAll'], $objectIDs); | |
375 | } | |
376 | ||
377 | /** | |
378 | * Updates data. | |
379 | */ | |
380 | public function update() | |
381 | { | |
382 | if (empty($this->objects)) { | |
383 | $this->readObjects(); | |
384 | } | |
385 | ||
386 | if (isset($this->parameters['data'])) { | |
387 | foreach ($this->getObjects() as $object) { | |
388 | $object->update($this->parameters['data']); | |
389 | } | |
390 | } | |
391 | ||
392 | if (isset($this->parameters['counters'])) { | |
393 | foreach ($this->getObjects() as $object) { | |
394 | $object->updateCounters($this->parameters['counters']); | |
395 | } | |
396 | } | |
397 | } | |
398 | ||
399 | /** | |
400 | * Reads data by data id. | |
401 | */ | |
402 | protected function readObjects() | |
403 | { | |
404 | if (empty($this->objectIDs)) { | |
405 | return; | |
406 | } | |
407 | ||
408 | // get base class | |
409 | $baseClass = \call_user_func([$this->className, 'getBaseClass']); | |
410 | ||
411 | // get db information | |
412 | $tableName = \call_user_func([$this->className, 'getDatabaseTableName']); | |
413 | $indexName = \call_user_func([$this->className, 'getDatabaseTableIndexName']); | |
414 | ||
415 | // get objects | |
416 | $sql = "SELECT * | |
417 | FROM " . $tableName . " | |
418 | WHERE " . $indexName . " IN (" . \str_repeat('?,', \count($this->objectIDs) - 1) . "?)"; | |
419 | $statement = WCF::getDB()->prepareStatement($sql); | |
420 | $statement->execute($this->objectIDs); | |
421 | while ($object = $statement->fetchObject($baseClass)) { | |
422 | $this->objects[] = new $this->className($object); | |
423 | } | |
424 | } | |
425 | ||
426 | /** | |
427 | * Returns a single object and throws a UserInputException if no or more than one object is given. | |
428 | * | |
429 | * @return DatabaseObject | |
430 | * @throws UserInputException | |
431 | */ | |
432 | protected function getSingleObject() | |
433 | { | |
434 | if (empty($this->objects)) { | |
435 | $this->readObjects(); | |
436 | } | |
437 | ||
438 | if (\count($this->objects) != 1) { | |
439 | throw new UserInputException('objectIDs'); | |
440 | } | |
441 | ||
442 | \reset($this->objects); | |
443 | ||
444 | return \current($this->objects); | |
445 | } | |
446 | ||
447 | /** | |
448 | * Reads an integer value and validates it. | |
449 | * | |
450 | * @param string $variableName | |
451 | * @param bool $allowEmpty | |
452 | * @param string $arrayIndex | |
453 | */ | |
454 | protected function readInteger($variableName, $allowEmpty = false, $arrayIndex = '') | |
455 | { | |
456 | $this->readValue($variableName, $allowEmpty, $arrayIndex, self::TYPE_INTEGER, self::STRUCT_FLAT); | |
457 | } | |
458 | ||
459 | /** | |
460 | * Reads an integer array and validates it. | |
461 | * | |
462 | * @param string $variableName | |
463 | * @param bool $allowEmpty | |
464 | * @param string $arrayIndex | |
465 | * @since 3.0 | |
466 | */ | |
467 | protected function readIntegerArray($variableName, $allowEmpty = false, $arrayIndex = '') | |
468 | { | |
469 | $this->readValue($variableName, $allowEmpty, $arrayIndex, self::TYPE_INTEGER, self::STRUCT_ARRAY); | |
470 | } | |
471 | ||
472 | /** | |
473 | * Reads a string value and validates it. | |
474 | * | |
475 | * @param string $variableName | |
476 | * @param bool $allowEmpty | |
477 | * @param string $arrayIndex | |
478 | */ | |
479 | protected function readString($variableName, $allowEmpty = false, $arrayIndex = '') | |
480 | { | |
481 | $this->readValue($variableName, $allowEmpty, $arrayIndex, self::TYPE_STRING, self::STRUCT_FLAT); | |
482 | } | |
483 | ||
484 | /** | |
485 | * Reads a string array and validates it. | |
486 | * | |
487 | * @param string $variableName | |
488 | * @param bool $allowEmpty | |
489 | * @param string $arrayIndex | |
490 | * @since 3.0 | |
491 | */ | |
492 | protected function readStringArray($variableName, $allowEmpty = false, $arrayIndex = '') | |
493 | { | |
494 | $this->readValue($variableName, $allowEmpty, $arrayIndex, self::TYPE_STRING, self::STRUCT_ARRAY); | |
495 | } | |
496 | ||
497 | /** | |
498 | * Reads a boolean value and validates it. | |
499 | * | |
500 | * @param string $variableName | |
501 | * @param bool $allowEmpty | |
502 | * @param string $arrayIndex | |
503 | */ | |
504 | protected function readBoolean($variableName, $allowEmpty = false, $arrayIndex = '') | |
505 | { | |
506 | $this->readValue($variableName, $allowEmpty, $arrayIndex, self::TYPE_BOOLEAN, self::STRUCT_FLAT); | |
507 | } | |
508 | ||
509 | /** | |
510 | * Reads a json-encoded value and validates it. | |
511 | * | |
512 | * @param string $variableName | |
513 | * @param bool $allowEmpty | |
514 | * @param string $arrayIndex | |
515 | */ | |
516 | protected function readJSON($variableName, $allowEmpty = false, $arrayIndex = '') | |
517 | { | |
518 | $this->readValue($variableName, $allowEmpty, $arrayIndex, self::TYPE_JSON, self::STRUCT_FLAT); | |
519 | } | |
520 | ||
521 | /** | |
522 | * Reads a value and validates it. If you set $allowEmpty to true, no exception will | |
523 | * be thrown if the variable evaluates to 0 (int) or '' (string). Furthermore the | |
524 | * variable will be always created with a sane value if it does not exist. | |
525 | * | |
526 | * @param string $variableName | |
527 | * @param bool $allowEmpty | |
528 | * @param string $arrayIndex | |
529 | * @param int $type | |
530 | * @param int $structure | |
531 | * @throws SystemException | |
532 | * @throws UserInputException | |
533 | */ | |
534 | protected function readValue($variableName, $allowEmpty, $arrayIndex, $type, $structure) | |
535 | { | |
536 | if ($arrayIndex) { | |
537 | if (!isset($this->parameters[$arrayIndex])) { | |
538 | if ($allowEmpty) { | |
539 | // Implicitly create the structure to permit implicitly defined values. | |
540 | $this->parameters[$arrayIndex] = []; | |
541 | } else { | |
542 | throw new SystemException("Corrupt parameters, index '" . $arrayIndex . "' is missing"); | |
543 | } | |
544 | } | |
545 | ||
546 | $target = &$this->parameters[$arrayIndex]; | |
547 | } else { | |
548 | $target = &$this->parameters; | |
549 | } | |
550 | ||
551 | switch ($type) { | |
552 | case self::TYPE_INTEGER: | |
553 | if (!isset($target[$variableName])) { | |
554 | if ($allowEmpty) { | |
555 | $target[$variableName] = ($structure === self::STRUCT_FLAT) ? 0 : []; | |
556 | } else { | |
557 | throw new UserInputException($variableName); | |
558 | } | |
559 | } else { | |
560 | if ($structure === self::STRUCT_FLAT) { | |
561 | $target[$variableName] = \intval($target[$variableName]); | |
562 | if (!$allowEmpty && !$target[$variableName]) { | |
563 | throw new UserInputException($variableName); | |
564 | } | |
565 | } else { | |
566 | $target[$variableName] = ArrayUtil::toIntegerArray($target[$variableName]); | |
567 | if (!\is_array($target[$variableName])) { | |
568 | throw new UserInputException($variableName); | |
569 | } | |
570 | ||
571 | for ($i = 0, $length = \count($target[$variableName]); $i < $length; $i++) { | |
572 | if ($target[$variableName][$i] === 0) { | |
573 | throw new UserInputException($variableName); | |
574 | } | |
575 | } | |
576 | } | |
577 | } | |
578 | break; | |
579 | ||
580 | case self::TYPE_STRING: | |
581 | if (!isset($target[$variableName])) { | |
582 | if ($allowEmpty) { | |
583 | $target[$variableName] = ($structure === self::STRUCT_FLAT) ? '' : []; | |
584 | } else { | |
585 | throw new UserInputException($variableName); | |
586 | } | |
587 | } else { | |
588 | if ($structure === self::STRUCT_FLAT) { | |
589 | $target[$variableName] = StringUtil::trim($target[$variableName]); | |
590 | if (!$allowEmpty && $target[$variableName] === '') { | |
591 | throw new UserInputException($variableName); | |
592 | } | |
593 | } else { | |
594 | $target[$variableName] = ArrayUtil::trim($target[$variableName]); | |
595 | if (!\is_array($target[$variableName])) { | |
596 | throw new UserInputException($variableName); | |
597 | } | |
598 | ||
599 | for ($i = 0, $length = \count($target[$variableName]); $i < $length; $i++) { | |
600 | if ($target[$variableName][$i] === '') { | |
601 | throw new UserInputException($variableName); | |
602 | } | |
603 | } | |
604 | } | |
605 | } | |
606 | break; | |
607 | ||
608 | case self::TYPE_BOOLEAN: | |
609 | if (!isset($target[$variableName])) { | |
610 | if ($allowEmpty) { | |
611 | $target[$variableName] = false; | |
612 | } else { | |
613 | throw new UserInputException($variableName); | |
614 | } | |
615 | } else { | |
616 | if (\is_numeric($target[$variableName])) { | |
617 | $target[$variableName] = (bool)$target[$variableName]; | |
618 | } else { | |
619 | $target[$variableName] = $target[$variableName] != 'false'; | |
620 | } | |
621 | } | |
622 | break; | |
623 | ||
624 | case self::TYPE_JSON: | |
625 | if (!isset($target[$variableName])) { | |
626 | if ($allowEmpty) { | |
627 | $target[$variableName] = []; | |
628 | } else { | |
629 | throw new UserInputException($variableName); | |
630 | } | |
631 | } else { | |
632 | try { | |
633 | $target[$variableName] = JSON::decode($target[$variableName]); | |
634 | } catch (SystemException $e) { | |
635 | throw new UserInputException($variableName); | |
636 | } | |
637 | ||
638 | if (!$allowEmpty && empty($target[$variableName])) { | |
639 | throw new UserInputException($variableName); | |
640 | } | |
641 | } | |
642 | break; | |
643 | } | |
644 | } | |
645 | ||
646 | /** | |
647 | * Returns object class name. | |
648 | * | |
649 | * @return string | |
650 | */ | |
651 | public function getClassName() | |
652 | { | |
653 | return $this->className; | |
654 | } | |
655 | ||
656 | /** | |
657 | * Returns a list of currently loaded objects. | |
658 | * | |
659 | * @return DatabaseObjectEditor[] | |
660 | */ | |
661 | public function getObjects() | |
662 | { | |
663 | return $this->objects; | |
664 | } | |
11ade432 | 665 | } |