Add explicit `return null;` statements
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / clipboard / ClipboardHandler.class.php
CommitLineData
0d6ea23f 1<?php
a9229942 2
0d6ea23f 3namespace wcf\system\clipboard;
a9229942 4
7a23a706 5use wcf\data\DatabaseObject;
e4499881 6use wcf\data\DatabaseObjectList;
a9229942
TD
7use wcf\data\object\type\ObjectType;
8use wcf\data\object\type\ObjectTypeCache;
b401cd0d
AE
9use wcf\system\cache\builder\ClipboardActionCacheBuilder;
10use wcf\system\cache\builder\ClipboardPageCacheBuilder;
157054c9 11use wcf\system\clipboard\action\IClipboardAction;
0d6ea23f 12use wcf\system\database\util\PreparedStatementConditionBuilder;
7b9ff46b 13use wcf\system\exception\ImplementationException;
0d6ea23f
AE
14use wcf\system\exception\SystemException;
15use wcf\system\SingletonFactory;
16use wcf\system\WCF;
0d6ea23f
AE
17
18/**
19 * Handles clipboard-related actions.
a9229942
TD
20 *
21 * @author Alexander Ebert
22 * @copyright 2001-2019 WoltLab GmbH
23 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
24 * @package WoltLabSuite\Core\System\Clipboard
0d6ea23f 25 */
a9229942
TD
26class ClipboardHandler extends SingletonFactory
27{
28 /**
29 * cached list of actions
30 * @var array
31 */
32 protected $actionCache;
33
34 /**
35 * cached list of clipboard item types
36 * @var mixed[][]
37 */
38 protected $cache;
39
40 /**
41 * list of marked items
42 * @var DatabaseObject[][]
43 */
44 protected $markedItems;
45
46 /**
47 * cached list of page actions
48 * @var array
49 */
50 protected $pageCache;
51
52 /**
53 * list of page class names
54 * @var string[]
55 */
56 protected $pageClasses = [];
57
58 /**
59 * page object id
60 * @var int
61 */
62 protected $pageObjectID = 0;
63
64 /**
65 * @inheritDoc
66 */
67 protected function init()
68 {
69 $this->cache = [
70 'objectTypes' => [],
71 'objectTypeNames' => [],
72 ];
73 $cache = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.clipboardItem');
74 foreach ($cache as $objectType) {
75 $this->cache['objectTypes'][$objectType->objectTypeID] = $objectType;
76 $this->cache['objectTypeNames'][$objectType->objectType] = $objectType->objectTypeID;
77 }
78
79 $this->pageCache = ClipboardPageCacheBuilder::getInstance()->getData();
80 }
81
82 /**
83 * Loads action cache.
84 */
85 protected function loadActionCache()
86 {
87 if ($this->actionCache !== null) {
88 return;
89 }
90
91 $this->actionCache = ClipboardActionCacheBuilder::getInstance()->getData();
92 }
93
94 /**
95 * Marks objects as marked.
96 *
97 * @param array $objectIDs
98 * @param int $objectTypeID
99 */
100 public function mark(array $objectIDs, $objectTypeID)
101 {
102 // remove existing entries first, prevents conflict with INSERT
103 $this->unmark($objectIDs, $objectTypeID);
104
105 $sql = "INSERT INTO wcf" . WCF_N . "_clipboard_item
106 (objectTypeID, userID, objectID)
107 VALUES (?, ?, ?)";
108 $statement = WCF::getDB()->prepareStatement($sql);
109 foreach ($objectIDs as $objectID) {
110 $statement->execute([
111 $objectTypeID,
112 WCF::getUser()->userID,
113 $objectID,
114 ]);
115 }
116 }
117
118 /**
119 * Removes an object marking.
120 *
121 * @param array $objectIDs
122 * @param int $objectTypeID
123 */
124 public function unmark(array $objectIDs, $objectTypeID)
125 {
126 $conditions = new PreparedStatementConditionBuilder();
127 $conditions->add("objectTypeID = ?", [$objectTypeID]);
128 $conditions->add("objectID IN (?)", [$objectIDs]);
129 $conditions->add("userID = ?", [WCF::getUser()->userID]);
130
131 $sql = "DELETE FROM wcf" . WCF_N . "_clipboard_item
132 " . $conditions;
133 $statement = WCF::getDB()->prepareStatement($sql);
134 $statement->execute($conditions->getParameters());
135 }
136
137 /**
138 * Unmarks all items of given type.
139 *
140 * @param int $objectTypeID
141 */
142 public function unmarkAll($objectTypeID)
143 {
144 $sql = "DELETE FROM wcf" . WCF_N . "_clipboard_item
145 WHERE objectTypeID = ?
146 AND userID = ?";
147 $statement = WCF::getDB()->prepareStatement($sql);
148 $statement->execute([
149 $objectTypeID,
150 WCF::getUser()->userID,
151 ]);
152 }
153
154 /**
155 * Returns the id of the clipboard object type with the given name or `null` if no such
156 * clipboard object type exists.
157 *
158 * @param string $typeName
159 * @return int|null
160 */
161 public function getObjectTypeID($typeName)
162 {
813c41ce 163 return $this->cache['objectTypeNames'][$typeName] ?? null;
a9229942
TD
164 }
165
166 /**
167 * Returns the clipboard object type with the given id or `null` if no such
168 * clipboard object type exists.
169 *
170 * @param int $objectTypeID
171 * @return ObjectType|null
172 */
173 public function getObjectType($objectTypeID)
174 {
813c41ce 175 return $this->cache['objectTypes'][$objectTypeID] ?? null;
a9229942
TD
176 }
177
178 /**
179 * Returns the id of the clipboard object type with the given name or `null` if no such
180 * clipboard object type exists.
181 *
182 * @param string $objectType
183 * @return int|null
184 */
185 public function getObjectTypeByName($objectType)
186 {
187 foreach ($this->cache['objectTypes'] as $objectTypeID => $objectTypeObj) {
188 if ($objectTypeObj->objectType == $objectType) {
189 return $objectTypeID;
190 }
191 }
5227ebc7
MS
192
193 return null;
a9229942
TD
194 }
195
196 /**
197 * Loads a list of marked items grouped by type name.
198 *
199 * @param int $objectTypeID
200 * @throws SystemException
201 */
202 protected function loadMarkedItems($objectTypeID = null)
203 {
204 if ($this->markedItems === null) {
205 $this->markedItems = [];
206 }
207
208 if ($objectTypeID !== null) {
209 $objectType = $this->getObjectType($objectTypeID);
210 if ($objectType === null) {
211 throw new SystemException("object type id " . $objectTypeID . " is invalid");
212 }
213
214 if (!isset($this->markedItems[$objectType->objectType])) {
215 $this->markedItems[$objectType->objectType] = [];
216 }
217 }
218
219 $conditions = new PreparedStatementConditionBuilder();
220 $conditions->add("userID = ?", [WCF::getUser()->userID]);
221 if ($objectTypeID !== null) {
222 $conditions->add("objectTypeID = ?", [$objectTypeID]);
223 }
224
225 // fetch object ids
226 $sql = "SELECT objectTypeID, objectID
227 FROM wcf" . WCF_N . "_clipboard_item
228 " . $conditions;
229 $statement = WCF::getDB()->prepareStatement($sql);
230 $statement->execute($conditions->getParameters());
231
232 // group object ids by type name
233 $data = [];
234 while ($row = $statement->fetchArray()) {
235 $objectType = $this->getObjectType($row['objectTypeID']);
236 if ($objectType === null) {
237 continue;
238 }
239
240 if (!isset($data[$objectType->objectType])) {
241 /** @noinspection PhpUndefinedFieldInspection */
242 $listClassName = $objectType->listclassname;
243 if ($listClassName == '') {
244 throw new SystemException("Missing list class for object type '" . $objectType->objectType . "'");
245 }
246
247 $data[$objectType->objectType] = [
248 'className' => $listClassName,
249 'objectIDs' => [],
250 ];
251 }
252
253 $data[$objectType->objectType]['objectIDs'][] = $row['objectID'];
254 }
255
256 // read objects
257 foreach ($data as $objectType => $objectData) {
258 /** @var DatabaseObjectList $objectList */
259 $objectList = new $objectData['className']();
260 $objectList->getConditionBuilder()->add(
261 $objectList->getDatabaseTableAlias() . "." . $objectList->getDatabaseTableIndexName() . " IN (?)",
262 [$objectData['objectIDs']]
263 );
264 $objectList->readObjects();
265
266 $this->markedItems[$objectType] = $objectList->getObjects();
267
268 // validate object ids against loaded items (check for zombie object ids)
269 $indexName = $objectList->getDatabaseTableIndexName();
270 foreach ($this->markedItems[$objectType] as $object) {
271 /** @noinspection PhpVariableVariableInspection */
272 $index = \array_search($object->{$indexName}, $objectData['objectIDs']);
273 unset($objectData['objectIDs'][$index]);
274 }
275
276 if (!empty($objectData['objectIDs'])) {
277 $conditions = new PreparedStatementConditionBuilder();
278 $conditions->add("objectTypeID = ?", [$this->getObjectTypeByName($objectType)]);
279 $conditions->add("userID = ?", [WCF::getUser()->userID]);
280 $conditions->add("objectID IN (?)", [$objectData['objectIDs']]);
281
282 $sql = "DELETE FROM wcf" . WCF_N . "_clipboard_item
283 " . $conditions;
284 $statement = WCF::getDB()->prepareStatement($sql);
285 $statement->execute($conditions->getParameters());
286 }
287 }
288 }
289
290 /**
291 * Loads a list of marked items grouped by type name.
292 *
293 * @param int $objectTypeID
294 * @return array
295 */
296 public function getMarkedItems($objectTypeID = null)
297 {
298 if ($this->markedItems === null) {
299 $this->loadMarkedItems($objectTypeID);
300 }
301
302 if ($objectTypeID !== null) {
303 $objectType = $this->getObjectType($objectTypeID);
304 if (!isset($this->markedItems[$objectType->objectType])) {
305 $this->loadMarkedItems($objectTypeID);
306 }
307
308 return $this->markedItems[$objectType->objectType];
309 }
310
311 return $this->markedItems;
312 }
313
b6128f97
MS
314 /**
315 * Returns `true` if the object with the given data is marked.
316 */
317 public function isMarked(int $objectTypeID, int $objectID): bool
318 {
319 return isset($this->getMarkedItems($objectTypeID)[$objectID]);
320 }
321
a9229942
TD
322 /**
323 * Returns the data of the items for clipboard editor or `null` if no items are marked.
324 *
325 * @param string|string[] $page
326 * @param int $pageObjectID
327 * @return array|null
328 * @throws ImplementationException
329 */
330 public function getEditorItems($page, $pageObjectID)
331 {
332 $pages = $page;
333 if (!\is_array($pages)) {
334 $pages = [$page];
335 }
336
337 $this->pageClasses = [];
338 $this->pageObjectID = 0;
339
340 // get objects
341 $this->loadMarkedItems();
342 if (empty($this->markedItems)) {
c0b28aa2 343 return null;
a9229942
TD
344 }
345
346 $this->pageClasses = $pages;
347 $this->pageObjectID = $pageObjectID;
348
349 // fetch action ids
350 $this->loadActionCache();
351 $actionIDs = [];
352 foreach ($pages as $page) {
353 foreach ($this->pageCache[$page] as $actionID) {
354 if (isset($this->actionCache[$actionID])) {
355 $actionIDs[] = $actionID;
356 }
357 }
358 }
359 $actionIDs = \array_unique($actionIDs);
360
361 // load actions
362 $actions = [];
363 foreach ($actionIDs as $actionID) {
364 $actionObject = $this->actionCache[$actionID];
365 $actionClassName = $actionObject->actionClassName;
366 if (!isset($actions[$actionClassName])) {
367 // validate class
368 if (!\is_subclass_of($actionClassName, IClipboardAction::class)) {
369 throw new ImplementationException($actionClassName, IClipboardAction::class);
370 }
371
372 $actions[$actionClassName] = [
373 'actions' => [],
374 'object' => new $actionClassName(),
375 ];
376 }
377
378 $actions[$actionClassName]['actions'][] = $actionObject;
379 }
380
381 // execute actions
382 $editorData = [];
383 foreach ($actions as $actionData) {
384 /** @var IClipboardAction $clipboardAction */
385 $clipboardAction = $actionData['object'];
386
387 // get accepted objects
388 $typeName = $clipboardAction->getTypeName();
389 if (!isset($this->markedItems[$typeName]) || empty($this->markedItems[$typeName])) {
390 continue;
391 }
392
393 if (!isset($editorData[$typeName])) {
394 $editorData[$typeName] = [
395 'label' => $clipboardAction->getEditorLabel($this->markedItems[$typeName]),
396 'items' => [],
397 'reloadPageOnSuccess' => $clipboardAction->getReloadPageOnSuccess(),
398 ];
c23f877c 399 } else {
c14276ab 400 $editorData[$typeName]['reloadPageOnSuccess'] = \array_unique(\array_merge(
c23f877c
MS
401 $editorData[$typeName]['reloadPageOnSuccess'],
402 $clipboardAction->getReloadPageOnSuccess()
403 ));
a9229942
TD
404 }
405
406 foreach ($actionData['actions'] as $actionObject) {
407 $data = $clipboardAction->execute($this->markedItems[$typeName], $actionObject);
408 if ($data === null) {
409 continue;
410 }
411
412 $editorData[$typeName]['items'][$actionObject->showOrder] = $data;
413 }
414 }
415
416 return $editorData;
417 }
418
419 /**
420 * Removes items from clipboard.
421 *
422 * @param int $typeID
423 */
424 public function removeItems($typeID = null)
425 {
426 $conditions = new PreparedStatementConditionBuilder();
427 $conditions->add("userID = ?", [WCF::getUser()->userID]);
428 if ($typeID !== null) {
429 $conditions->add("objectTypeID = ?", [$typeID]);
430 }
431
432 $sql = "DELETE FROM wcf" . WCF_N . "_clipboard_item
433 " . $conditions;
434 $statement = WCF::getDB()->prepareStatement($sql);
435 $statement->execute($conditions->getParameters());
436 }
437
438 /**
439 * Returns true (1) if at least one item (of the given object type) is marked.
440 *
441 * @param int $objectTypeID
442 * @return int
443 */
444 public function hasMarkedItems($objectTypeID = null)
445 {
446 if (!WCF::getUser()->userID) {
447 return 0;
448 }
449
450 $conditionBuilder = new PreparedStatementConditionBuilder();
451 $conditionBuilder->add("userID = ?", [WCF::getUser()->userID]);
452 if ($objectTypeID !== null) {
453 $conditionBuilder->add("objectTypeID = ?", [$objectTypeID]);
454 }
455
456 $sql = "SELECT COUNT(*)
457 FROM wcf" . WCF_N . "_clipboard_item
458 " . $conditionBuilder;
459 $statement = WCF::getDB()->prepareStatement($sql);
460 $statement->execute($conditionBuilder->getParameters());
461
462 return $statement->fetchSingleColumn() ? 1 : 0;
463 }
464
465 /**
466 * Returns the list of page class names.
467 *
468 * @return string[]
469 */
470 public function getPageClasses()
471 {
472 return $this->pageClasses;
473 }
474
475 /**
476 * Returns page object id.
477 *
478 * @return int
479 */
480 public function getPageObjectID()
481 {
482 return $this->pageObjectID;
483 }
0d6ea23f 484}