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