Apply PSR-12 code style (#3886)
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / AbstractDatabaseObjectAction.class.php
CommitLineData
11ade432 1<?php
a9229942 2
11ade432 3namespace wcf\data;
a9229942 4
11ade432
AE
5use wcf\system\event\EventHandler;
6use wcf\system\exception\PermissionDeniedException;
4e877829 7use wcf\system\exception\SystemException;
429e91b8 8use wcf\system\exception\UserInputException;
b6628fdc 9use wcf\system\request\RequestHandler;
11ade432 10use wcf\system\WCF;
857ed85d 11use wcf\util\ArrayUtil;
217e4987 12use wcf\util\JSON;
11ade432
AE
13use 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
23abstract 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}