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