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