Commit | Line | Data |
---|---|---|
0d6ea23f | 1 | <?php |
a9229942 | 2 | |
0d6ea23f | 3 | namespace wcf\system\clipboard; |
a9229942 | 4 | |
7a23a706 | 5 | use wcf\data\DatabaseObject; |
e4499881 | 6 | use wcf\data\DatabaseObjectList; |
a9229942 TD |
7 | use wcf\data\object\type\ObjectType; |
8 | use wcf\data\object\type\ObjectTypeCache; | |
b401cd0d AE |
9 | use wcf\system\cache\builder\ClipboardActionCacheBuilder; |
10 | use wcf\system\cache\builder\ClipboardPageCacheBuilder; | |
157054c9 | 11 | use wcf\system\clipboard\action\IClipboardAction; |
0d6ea23f | 12 | use wcf\system\database\util\PreparedStatementConditionBuilder; |
7b9ff46b | 13 | use wcf\system\exception\ImplementationException; |
0d6ea23f AE |
14 | use wcf\system\exception\SystemException; |
15 | use wcf\system\SingletonFactory; | |
16 | use 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 |
26 | class 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 | } |