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