Commit | Line | Data |
---|---|---|
11ade432 AE |
1 | <?php |
2 | namespace wcf\data; | |
11ade432 AE |
3 | use wcf\system\event\EventHandler; |
4 | use wcf\system\exception\PermissionDeniedException; | |
4e877829 | 5 | use wcf\system\exception\SystemException; |
429e91b8 | 6 | use wcf\system\exception\UserInputException; |
b6628fdc | 7 | use wcf\system\request\RequestHandler; |
11ade432 AE |
8 | use wcf\system\WCF; |
9 | use wcf\util\ClassUtil; | |
217e4987 | 10 | use wcf\util\JSON; |
11ade432 AE |
11 | use wcf\util\StringUtil; |
12 | ||
13 | /** | |
14 | * Default implementation for DatabaseObject-related actions. | |
9f959ced | 15 | * |
11ade432 | 16 | * @author Alexander Ebert |
ca4ba303 | 17 | * @copyright 2001-2014 WoltLab GmbH |
11ade432 AE |
18 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> |
19 | * @package com.woltlab.wcf | |
20 | * @subpackage data | |
9f959ced | 21 | * @category Community Framework |
11ade432 | 22 | */ |
a427a8c8 | 23 | abstract class AbstractDatabaseObjectAction implements IDatabaseObjectAction, IDeleteAction { |
11ade432 AE |
24 | /** |
25 | * pending action | |
11ade432 AE |
26 | * @var string |
27 | */ | |
28 | protected $action = ''; | |
29 | ||
30 | /** | |
31 | * object editor class name | |
11ade432 AE |
32 | * @var string |
33 | */ | |
34 | protected $className = ''; | |
35 | ||
36 | /** | |
37 | * list of object ids | |
11ade432 AE |
38 | * @var array<integer> |
39 | */ | |
40 | protected $objectIDs = array(); | |
41 | ||
42 | /** | |
832371b2 | 43 | * list of object editors |
0ad90fc3 | 44 | * @var array<\wcf\data\DatabaseObjectEditor> |
11ade432 AE |
45 | */ |
46 | protected $objects = array(); | |
47 | ||
48 | /** | |
49 | * multi-dimensional array of parameters required by an action | |
11ade432 AE |
50 | * @var array<array> |
51 | */ | |
52 | protected $parameters = array(); | |
53 | ||
54 | /** | |
55 | * list of permissions required to create objects | |
832371b2 | 56 | * @var array<string> |
11ade432 AE |
57 | */ |
58 | protected $permissionsCreate = array(); | |
59 | ||
60 | /** | |
61 | * list of permissions required to delete objects | |
832371b2 | 62 | * @var array<string> |
11ade432 AE |
63 | */ |
64 | protected $permissionsDelete = array(); | |
65 | ||
66 | /** | |
67 | * list of permissions required to update objects | |
832371b2 | 68 | * @var array<string> |
11ade432 AE |
69 | */ |
70 | protected $permissionsUpdate = array(); | |
71 | ||
bae8dd1e AE |
72 | /** |
73 | * disallow requests for specified methods if the origin is not the ACP | |
74 | * @var array<string> | |
75 | */ | |
76 | protected $requireACP = array(); | |
77 | ||
22d94e94 AE |
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 | ||
11ade432 AE |
84 | /** |
85 | * values returned by executed action | |
11ade432 AE |
86 | * @var mixed |
87 | */ | |
88 | protected $returnValues = null; | |
89 | ||
fca077a6 | 90 | /** |
9f959ced MS |
91 | * allows guest access for all specified methods, by default guest access |
92 | * is completely disabled | |
878d0d80 | 93 | * @var array<string> |
fca077a6 | 94 | */ |
878d0d80 | 95 | protected $allowGuestAccess = array(); |
fca077a6 | 96 | |
429e91b8 AE |
97 | const TYPE_INTEGER = 1; |
98 | const TYPE_STRING = 2; | |
976d79e3 | 99 | const TYPE_BOOLEAN = 3; |
217e4987 | 100 | const TYPE_JSON = 4; |
429e91b8 | 101 | |
11ade432 | 102 | /** |
a90028e5 | 103 | * Initialize a new DatabaseObject-related action. |
9f959ced | 104 | * |
4e877829 | 105 | * @param array<mixed> $objects |
11ade432 AE |
106 | * @param string $action |
107 | * @param array $parameters | |
108 | */ | |
4e877829 | 109 | public function __construct(array $objects, $action, array $parameters = array()) { |
7703d974 MS |
110 | // set class name |
111 | if (empty($this->className)) { | |
112 | $className = get_called_class(); | |
113 | ||
838e315b SG |
114 | if (mb_substr($className, -6) == 'Action') { |
115 | $this->className = mb_substr($className, 0, -6).'Editor'; | |
7703d974 MS |
116 | } |
117 | } | |
118 | ||
4e877829 MW |
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 | } | |
4e877829 | 136 | else { |
f28efd50 | 137 | $this->objectIDs[] = $object; |
4e877829 MW |
138 | } |
139 | } | |
140 | ||
11ade432 AE |
141 | $this->action = $action; |
142 | $this->parameters = $parameters; | |
143 | ||
144 | // fire event action | |
145 | EventHandler::getInstance()->fireAction($this, 'initializeAction'); | |
146 | } | |
147 | ||
148 | /** | |
0ad90fc3 | 149 | * @see \wcf\data\IDatabaseObjectAction::validateAction() |
11ade432 AE |
150 | */ |
151 | public function validateAction() { | |
fca077a6 | 152 | // validate if user is logged in |
878d0d80 | 153 | if (!WCF::getUser()->userID && !in_array($this->getActionName(), $this->allowGuestAccess)) { |
f1a3caba | 154 | throw new PermissionDeniedException(); |
fca077a6 | 155 | } |
bae8dd1e AE |
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 | } | |
fca077a6 | 160 | |
11ade432 AE |
161 | // validate action name |
162 | if (!method_exists($this, $this->getActionName())) { | |
3631f7bd | 163 | throw new SystemException("unknown action '".$this->getActionName()."'"); |
11ade432 AE |
164 | } |
165 | ||
166 | $actionName = 'validate'.StringUtil::firstCharToUpperCase($this->getActionName()); | |
167 | if (!method_exists($this, $actionName)) { | |
3631f7bd | 168 | throw new PermissionDeniedException(); |
11ade432 AE |
169 | } |
170 | ||
171 | // execute action | |
172 | call_user_func_array(array($this, $actionName), $this->getParameters()); | |
173 | } | |
174 | ||
175 | /** | |
0ad90fc3 | 176 | * @see \wcf\data\IDatabaseObjectAction::executeAction() |
11ade432 AE |
177 | */ |
178 | public function executeAction() { | |
179 | // execute action | |
51772f85 MW |
180 | if (!method_exists($this, $this->getActionName())) { |
181 | throw new SystemException("call to undefined function '".$this->getActionName()."'"); | |
351d836a MS |
182 | } |
183 | ||
51772f85 | 184 | $this->returnValues = call_user_func(array($this, $this->getActionName())); |
11ade432 AE |
185 | |
186 | // reset cache | |
22d94e94 AE |
187 | if (in_array($this->getActionName(), $this->resetCache)) { |
188 | $this->resetCache(); | |
11ade432 AE |
189 | } |
190 | ||
191 | // fire event action | |
192 | EventHandler::getInstance()->fireAction($this, 'finalizeAction'); | |
193 | ||
194 | return $this->getReturnValues(); | |
195 | } | |
196 | ||
22d94e94 AE |
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 | ||
11ade432 | 206 | /** |
0ad90fc3 | 207 | * @see \wcf\data\IDatabaseObjectAction::getActionName() |
11ade432 AE |
208 | */ |
209 | public function getActionName() { | |
210 | return $this->action; | |
211 | } | |
212 | ||
213 | /** | |
0ad90fc3 | 214 | * @see \wcf\data\IDatabaseObjectAction::getObjectIDs() |
11ade432 AE |
215 | */ |
216 | public function getObjectIDs() { | |
217 | return $this->objectIDs; | |
218 | } | |
219 | ||
e9aeabca MW |
220 | /** |
221 | * Sets the database objects. | |
222 | * | |
0ad90fc3 | 223 | * @param array<\wcf\data\DatabaseObject> $objects |
e9aeabca MW |
224 | */ |
225 | public function setObjects(array $objects) { | |
226 | $this->objects = $objects; | |
227 | } | |
228 | ||
11ade432 | 229 | /** |
0ad90fc3 | 230 | * @see \wcf\data\IDatabaseObjectAction::getParameters() |
11ade432 AE |
231 | */ |
232 | public function getParameters() { | |
233 | return $this->parameters; | |
234 | } | |
235 | ||
236 | /** | |
0ad90fc3 | 237 | * @see \wcf\data\IDatabaseObjectAction::getReturnValues() |
11ade432 AE |
238 | */ |
239 | public function getReturnValues() { | |
240 | return array( | |
a8f8312d | 241 | 'actionName' => $this->action, |
11ade432 AE |
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 | |
15fa2802 | 252 | if (is_array($this->permissionsCreate) && !empty($this->permissionsCreate)) { |
3631f7bd | 253 | WCF::getSession()->checkPermissions($this->permissionsCreate); |
11ade432 AE |
254 | } |
255 | else { | |
3631f7bd | 256 | throw new PermissionDeniedException(); |
11ade432 AE |
257 | } |
258 | } | |
259 | ||
260 | /** | |
0ad90fc3 | 261 | * @see \wcf\data\IDeleteAction::validateDelete() |
11ade432 AE |
262 | */ |
263 | public function validateDelete() { | |
264 | // validate permissions | |
15fa2802 | 265 | if (is_array($this->permissionsDelete) && !empty($this->permissionsDelete)) { |
3631f7bd | 266 | WCF::getSession()->checkPermissions($this->permissionsDelete); |
11ade432 AE |
267 | } |
268 | else { | |
3631f7bd | 269 | throw new PermissionDeniedException(); |
11ade432 AE |
270 | } |
271 | ||
046f3d7b | 272 | // read objects |
15fa2802 | 273 | if (empty($this->objects)) { |
e9aeabca | 274 | $this->readObjects(); |
15fa2802 MS |
275 | |
276 | if (empty($this->objects)) { | |
3631f7bd | 277 | throw new UserInputException('objectIDs'); |
15fa2802 | 278 | } |
11ade432 AE |
279 | } |
280 | } | |
281 | ||
282 | /** | |
283 | * Validates permissions and parameters. | |
284 | */ | |
285 | public function validateUpdate() { | |
286 | // validate permissions | |
15fa2802 | 287 | if (is_array($this->permissionsUpdate) && !empty($this->permissionsUpdate)) { |
3631f7bd | 288 | WCF::getSession()->checkPermissions($this->permissionsUpdate); |
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(); |
351d836a | 297 | |
15fa2802 | 298 | if (empty($this->objects)) { |
3631f7bd | 299 | throw new UserInputException('objectIDs'); |
15fa2802 | 300 | } |
11ade432 AE |
301 | } |
302 | } | |
303 | ||
304 | /** | |
832371b2 | 305 | * Creates new database object. |
351d836a | 306 | * |
0ad90fc3 | 307 | * @return \wcf\data\DatabaseObject |
11ade432 AE |
308 | */ |
309 | public function create() { | |
11ade432 AE |
310 | return call_user_func(array($this->className, 'create'), $this->parameters['data']); |
311 | } | |
312 | ||
313 | /** | |
0ad90fc3 | 314 | * @see \wcf\data\IDeleteAction::delete() |
11ade432 AE |
315 | */ |
316 | public function delete() { | |
15fa2802 | 317 | if (empty($this->objects)) { |
11ade432 AE |
318 | $this->readObjects(); |
319 | } | |
320 | ||
11ade432 AE |
321 | // get ids |
322 | $objectIDs = array(); | |
323 | foreach ($this->objects as $object) { | |
91a5aa24 | 324 | $objectIDs[] = $object->getObjectID(); |
11ade432 AE |
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() { | |
15fa2802 | 335 | if (empty($this->objects)) { |
11ade432 AE |
336 | $this->readObjects(); |
337 | } | |
338 | ||
046f3d7b MS |
339 | if (isset($this->parameters['data'])) { |
340 | foreach ($this->objects as $object) { | |
341 | $object->update($this->parameters['data']); | |
342 | } | |
11ade432 | 343 | } |
575350ba MS |
344 | |
345 | if (isset($this->parameters['counters'])) { | |
346 | foreach ($this->objects as $object) { | |
347 | $object->updateCounters($this->parameters['counters']); | |
348 | } | |
349 | } | |
11ade432 AE |
350 | } |
351 | ||
352 | /** | |
353 | * Reads data by data id. | |
354 | */ | |
355 | protected function readObjects() { | |
15fa2802 | 356 | if (empty($this->objectIDs)) { |
11ade432 AE |
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 | } | |
764fe46c | 377 | |
429e91b8 | 378 | /** |
56111b4d | 379 | * Returns a single object and throws an UserInputException if no or more than one object is given. |
429e91b8 | 380 | * |
0ad90fc3 | 381 | * @return \wcf\data\DatabaseObject |
429e91b8 AE |
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 | */ | |
61582f5f | 403 | protected function readInteger($variableName, $allowEmpty = false, $arrayIndex = '') { |
429e91b8 AE |
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 | ||
8795c633 MK |
418 | /** |
419 | * Reads a boolean value and validates it. | |
420 | * | |
421 | * @param string $variableName | |
422 | * @param boolean $allowEmpty | |
423 | * @param string $arrayIndex | |
8795c633 | 424 | */ |
976d79e3 AE |
425 | protected function readBoolean($variableName, $allowEmpty = false, $arrayIndex = '') { |
426 | $this->readValue($variableName, $allowEmpty, $arrayIndex, self::TYPE_BOOLEAN); | |
429e91b8 AE |
427 | } |
428 | ||
217e4987 AE |
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 | ||
429e91b8 AE |
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 | ||
57d62a68 | 456 | $target =& $this->parameters[$arrayIndex]; |
429e91b8 AE |
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; | |
8795c633 | 496 | |
976d79e3 | 497 | case self::TYPE_BOOLEAN: |
8795c633 MK |
498 | if (!isset($target[$variableName])) { |
499 | if ($allowEmpty) { | |
500 | $target[$variableName] = false; | |
501 | } | |
502 | else { | |
503 | throw new UserInputException($variableName); | |
504 | } | |
505 | } | |
506 | else { | |
976d79e3 AE |
507 | if (is_numeric($target[$variableName])) { |
508 | $target[$variableName] = (bool) $target[$variableName]; | |
921e346a MK |
509 | } |
510 | else { | |
976d79e3 | 511 | $target[$variableName] = $target[$variableName] != 'false'; |
6c611ea7 | 512 | } |
8795c633 MK |
513 | } |
514 | break; | |
217e4987 AE |
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; | |
429e91b8 | 538 | } |
429e91b8 AE |
539 | } |
540 | ||
764fe46c AE |
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 | * | |
0ad90fc3 | 553 | * @return array<\wcf\data\IEditableObject> |
764fe46c AE |
554 | */ |
555 | public function getObjects() { | |
556 | return $this->objects; | |
557 | } | |
11ade432 | 558 | } |