Merge branch '5.2' into 5.3
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / message / embedded / object / MessageEmbeddedObjectManager.class.php
1 <?php
2 namespace wcf\system\message\embedded\object;
3 use wcf\data\object\type\ObjectTypeCache;
4 use wcf\system\database\util\PreparedStatementConditionBuilder;
5 use wcf\system\exception\InvalidObjectTypeException;
6 use wcf\system\html\input\HtmlInputProcessor;
7 use wcf\system\SingletonFactory;
8 use wcf\system\WCF;
9
10 /**
11 * Default interface of embedded object handler.
12 *
13 * @author Marcel Werk
14 * @copyright 2001-2019 WoltLab GmbH
15 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
16 * @package WoltLabSuite\Core\System\Message\Embedded\Object
17 */
18 class MessageEmbeddedObjectManager extends SingletonFactory {
19 /**
20 * caches message to embedded object assignments
21 * @var array
22 */
23 protected $messageEmbeddedObjects = [];
24
25 /**
26 * caches embedded objects
27 * @var array
28 */
29 protected $embeddedObjects = [];
30
31 /**
32 * object type of the active message
33 * @var integer
34 */
35 protected $activeMessageObjectTypeID;
36
37 /**
38 * id of the active message
39 * @var integer
40 */
41 protected $activeMessageID;
42
43 /**
44 * language id of the active message
45 * @var integer
46 */
47 protected $activeMessageLanguageID;
48
49 /**
50 * list of embedded object handlers
51 * @var array
52 */
53 protected $embeddedObjectHandlers;
54
55 /**
56 * content language id
57 * @var integer
58 */
59 protected $contentLanguageID;
60
61 /**
62 * local cache for bulk operations
63 * @var mixed[][]
64 */
65 protected $bulkData = [
66 'insert' => [],
67 'remove' => []
68 ];
69
70 /**
71 * A list of previous active message settings used to restore
72 * the internal state in case of nested message processing.
73 */
74 protected $activeMessageHistory = [];
75
76 /**
77 * Registers the embedded objects found in given message.
78 *
79 * @param HtmlInputProcessor $htmlInputProcessor html input processor instance holding embedded object data
80 * @param boolean $isBulk true for bulk operations
81 * @return boolean true if at least one embedded object was found
82 */
83 public function registerObjects(HtmlInputProcessor $htmlInputProcessor, $isBulk = false) {
84 $context = $htmlInputProcessor->getContext();
85
86 $messageObjectType = $context['objectType'];
87 $messageObjectTypeID = $context['objectTypeID'];
88 $messageID = $context['objectID'];
89
90 // delete existing assignments
91 if ($isBulk) {
92 if (!isset($this->bulkData['remove'][$messageObjectType])) $this->bulkData['remove'][$messageObjectType] = [];
93 $this->bulkData['remove'][$messageObjectType][] = $messageID;
94 }
95 else {
96 $this->removeObjects($messageObjectType, [$messageID]);
97 }
98
99 $statement = null;
100 if (!$isBulk) {
101 // prepare statement
102 $sql = "INSERT INTO wcf".WCF_N."_message_embedded_object
103 (messageObjectTypeID, messageID, embeddedObjectTypeID, embeddedObjectID)
104 VALUES (?, ?, ?, ?)";
105 $statement = WCF::getDB()->prepareStatement($sql);
106
107 WCF::getDB()->beginTransaction();
108 }
109
110 $embeddedData = $htmlInputProcessor->getEmbeddedContent();
111 $returnValue = false;
112
113 /** @var IMessageEmbeddedObjectHandler $handler */
114 foreach ($this->getEmbeddedObjectHandlers() as $handler) {
115 $objectIDs = $handler->parse($htmlInputProcessor, $embeddedData);
116
117 if (!empty($objectIDs)) {
118 foreach ($objectIDs as $objectID) {
119 $parameters = [$messageObjectTypeID, $messageID, $handler->objectTypeID, $objectID];
120 if ($isBulk) {
121 $this->bulkData['insert'][] = $parameters;
122 }
123 else {
124 $statement->execute($parameters);
125 }
126 }
127
128 $returnValue = true;
129 }
130 }
131
132 if (!$isBulk) {
133 WCF::getDB()->commitTransaction();
134 }
135
136 return $returnValue;
137 }
138
139 /**
140 * Commits the bulk operation by performing all deletes and inserts
141 * in one big transaction to save performance.
142 */
143 public function commitBulkOperation() {
144 // delete existing data
145 WCF::getDB()->beginTransaction();
146 foreach ($this->bulkData['remove'] as $objectType => $objectIDs) {
147 $this->removeObjects($objectType, $objectIDs);
148 }
149 WCF::getDB()->commitTransaction();
150
151 // prepare statement
152 $sql = "INSERT INTO wcf".WCF_N."_message_embedded_object
153 (messageObjectTypeID, messageID, embeddedObjectTypeID, embeddedObjectID)
154 VALUES (?, ?, ?, ?)";
155 $statement = WCF::getDB()->prepareStatement($sql);
156
157 WCF::getDB()->beginTransaction();
158 foreach ($this->bulkData['insert'] as $parameters) {
159 $statement->execute($parameters);
160 }
161 WCF::getDB()->commitTransaction();
162
163 // reset cache
164 $this->bulkData = [
165 'insert' => [],
166 'remove' => []
167 ];
168 }
169
170 /**
171 * Registers the embedded objects found in a message using the simplified syntax.
172 *
173 * @param string $messageObjectType object type identifier
174 * @param integer $messageID object id
175 * @param integer[][] $embeddedContent list of object ids for embedded objects by object type id
176 * @return boolean true if at least one embedded object was found
177 */
178 public function registerSimpleObjects($messageObjectType, $messageID, array $embeddedContent) {
179 $messageObjectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType);
180
181 // delete existing assignments
182 $this->removeObjects($messageObjectType, [$messageID]);
183
184 if (empty($embeddedContent)) {
185 return false;
186 }
187
188 // prepare statement
189 $sql = "INSERT INTO wcf".WCF_N."_message_embedded_object
190 (messageObjectTypeID, messageID, embeddedObjectTypeID, embeddedObjectID)
191 VALUES (?, ?, ?, ?)";
192 $statement = WCF::getDB()->prepareStatement($sql);
193
194 // call embedded object handlers
195 WCF::getDB()->beginTransaction();
196 foreach ($embeddedContent as $objectTypeID => $objectIDs) {
197 foreach ($objectIDs as $objectID) {
198 $statement->execute([$messageObjectTypeID, $messageID, $objectTypeID, $objectID]);
199 }
200 }
201 WCF::getDB()->commitTransaction();
202
203 return true;
204 }
205
206 /**
207 * Removes embedded object assignments for given messages.
208 *
209 * @param string $messageObjectType
210 * @param integer[] $messageIDs
211 */
212 public function removeObjects($messageObjectType, array $messageIDs) {
213 $conditionBuilder = new PreparedStatementConditionBuilder();
214 $conditionBuilder->add('messageObjectTypeID = ?', [ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType)]);
215 $conditionBuilder->add('messageID IN (?)', [$messageIDs]);
216
217 $sql = "DELETE FROM wcf".WCF_N."_message_embedded_object
218 ".$conditionBuilder;
219 $statement = WCF::getDB()->prepareStatement($sql);
220 $statement->execute($conditionBuilder->getParameters());
221 }
222
223 /**
224 * Loads the embedded objects for given messages.
225 *
226 * @param string $messageObjectType
227 * @param integer[] $messageIDs
228 * @param integer $contentLanguageID
229 * @throws InvalidObjectTypeException
230 */
231 public function loadObjects($messageObjectType, array $messageIDs, $contentLanguageID = null) {
232 $messageObjectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType);
233 if ($messageObjectTypeID === null) {
234 throw new InvalidObjectTypeException($messageObjectType, 'com.woltlab.wcf.message');
235 }
236
237 $conditionBuilder = new PreparedStatementConditionBuilder();
238 $conditionBuilder->add('messageObjectTypeID = ?', [$messageObjectTypeID]);
239 $conditionBuilder->add('messageID IN (?)', [$messageIDs]);
240
241 // get object ids
242 $sql = "SELECT *
243 FROM wcf".WCF_N."_message_embedded_object
244 ".$conditionBuilder;
245 $statement = WCF::getDB()->prepareStatement($sql);
246 $statement->execute($conditionBuilder->getParameters());
247 $embeddedObjects = [];
248 while ($row = $statement->fetchArray()) {
249 if (!isset($this->embeddedObjects[$row['embeddedObjectTypeID']][$row['embeddedObjectID']])) {
250 // group objects by object type
251 if (!isset($embeddedObjects[$row['embeddedObjectTypeID']])) $embeddedObjects[$row['embeddedObjectTypeID']] = [];
252 $embeddedObjects[$row['embeddedObjectTypeID']][] = $row['embeddedObjectID'];
253 }
254
255 // store message to embedded object assignment
256 if (!isset($this->messageEmbeddedObjects[$row['messageObjectTypeID']][$row['messageID']][$row['embeddedObjectTypeID']])) {
257 $this->messageEmbeddedObjects[$row['messageObjectTypeID']][$row['messageID']][$row['embeddedObjectTypeID']] = [];
258 }
259 $this->messageEmbeddedObjects[$row['messageObjectTypeID']][$row['messageID']][$row['embeddedObjectTypeID']][] = $row['embeddedObjectID'];
260 }
261
262 $this->contentLanguageID = $contentLanguageID;
263
264 // load objects
265 foreach ($embeddedObjects as $embeddedObjectTypeID => $objectIDs) {
266 if (!isset($this->embeddedObjects[$embeddedObjectTypeID])) $this->embeddedObjects[$embeddedObjectTypeID] = [];
267 foreach ($this->getEmbeddedObjectHandler($embeddedObjectTypeID)->loadObjects(array_unique($objectIDs)) as $objectID => $object) {
268 $this->embeddedObjects[$embeddedObjectTypeID][$objectID] = $object;
269 }
270 }
271
272 $this->contentLanguageID = null;
273 }
274
275 /**
276 * Returns the content language id or null.
277 *
278 * @return integer
279 */
280 public function getContentLanguageID() {
281 return $this->contentLanguageID;
282 }
283
284 /**
285 * Sets active message information.
286 *
287 * @param string $messageObjectType
288 * @param integer $messageID
289 * @param integer $languageID
290 */
291 public function setActiveMessage($messageObjectType, $messageID, $languageID = null) {
292 if ($this->activeMessageObjectTypeID) {
293 $this->activeMessageHistory[] = [
294 'activeMessageID' => $this->activeMessageID,
295 'activeMessageLanguageID' => $this->activeMessageLanguageID,
296 'activeMessageObjectTypeID' => $this->activeMessageObjectTypeID,
297 ];
298 }
299
300 $this->activeMessageObjectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType);
301 $this->activeMessageID = $messageID;
302 $this->activeMessageLanguageID = $languageID;
303 }
304
305 /**
306 * Restores the internal state in case of nested message processing.
307 */
308 public function reset() {
309 $newState = \array_pop($this->activeMessageHistory);
310 if ($newState === null) {
311 $newState = [
312 'activeMessageID' => null,
313 'activeMessageLanguageID' => null,
314 'activeMessageObjectTypeID' => null,
315 ];
316 }
317
318 $this->activeMessageID = $newState['activeMessageID'];
319 $this->activeMessageLanguageID = $newState['activeMessageLanguageID'];
320 $this->activeMessageObjectTypeID = $newState['activeMessageObjectTypeID'];
321 }
322
323 /**
324 * Returns the language id of the active message.
325 *
326 * @return integer
327 */
328 public function getActiveMessageLanguageID() {
329 return $this->activeMessageLanguageID;
330 }
331
332 /**
333 * Returns all embedded objects of a specific type.
334 *
335 * @param string $embeddedObjectType
336 * @return array
337 */
338 public function getObjects($embeddedObjectType) {
339 $embeddedObjectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message.embeddedObject', $embeddedObjectType);
340 $returnValue = [];
341 if (!empty($this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$embeddedObjectTypeID])) {
342 foreach ($this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$embeddedObjectTypeID] as $embeddedObjectID) {
343 if (isset($this->embeddedObjects[$embeddedObjectTypeID][$embeddedObjectID])) {
344 $returnValue[] = $this->embeddedObjects[$embeddedObjectTypeID][$embeddedObjectID];
345 }
346 }
347 }
348
349 return $returnValue;
350 }
351
352 /**
353 * Returns a specific embedded object.
354 *
355 * @param string $embeddedObjectType
356 * @param integer $objectID
357 * @return \wcf\data\DatabaseObject
358 */
359 public function getObject($embeddedObjectType, $objectID) {
360 $embeddedObjectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message.embeddedObject', $embeddedObjectType);
361 if (!empty($this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$embeddedObjectTypeID])) {
362 foreach ($this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$embeddedObjectTypeID] as $embeddedObjectID) {
363 if ($embeddedObjectID == $objectID) {
364 if (isset($this->embeddedObjects[$embeddedObjectTypeID][$embeddedObjectID])) {
365 return $this->embeddedObjects[$embeddedObjectTypeID][$embeddedObjectID];
366 }
367 }
368 }
369 }
370
371 return null;
372 }
373
374 /**
375 * Temporarily registers a message, the parsed data will not be stored.
376 *
377 * @param HtmlInputProcessor $htmlInputProcessor html input processor
378 */
379 public function registerTemporaryMessage(HtmlInputProcessor $htmlInputProcessor) {
380 $context = $htmlInputProcessor->getContext();
381
382 // set active message information
383 $this->activeMessageObjectTypeID = $context['objectTypeID'];
384 $this->activeMessageID = $context['objectID'];
385
386 $embeddedData = $htmlInputProcessor->getEmbeddedContent();
387
388 /** @var IMessageEmbeddedObjectHandler $handler */
389 foreach ($this->getEmbeddedObjectHandlers() as $handler) {
390 $objectIDs = $handler->parse($htmlInputProcessor, $embeddedData);
391
392 if (!empty($objectIDs)) {
393 // save assignments
394 $this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$handler->objectTypeID] = $objectIDs;
395
396 // loads objects
397 $this->embeddedObjects[$handler->objectTypeID] = $handler->loadObjects($objectIDs);
398 }
399 }
400 }
401
402 /**
403 * @return ISimpleMessageEmbeddedObjectHandler[];
404 */
405 public function getSimpleMessageEmbeddedObjectHandlers() {
406 $handlers = [];
407 foreach ($this->getEmbeddedObjectHandlers() as $handler) {
408 if ($handler instanceof ISimpleMessageEmbeddedObjectHandler) {
409 $name = lcfirst(preg_replace('~^.*\\\\([A-Z][a-zA-Z]+)MessageEmbeddedObjectHandler$~', '$1', get_class($handler)));
410 $handlers[$name] = $handler;
411 }
412 }
413
414 return $handlers;
415 }
416
417 /**
418 * Returns all embedded object handlers.
419 *
420 * @return IMessageEmbeddedObjectHandler[]
421 */
422 protected function getEmbeddedObjectHandlers() {
423 if ($this->embeddedObjectHandlers === null) {
424 $this->embeddedObjectHandlers = [];
425 foreach (ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.message.embeddedObject') as $objectType) {
426 $this->embeddedObjectHandlers[$objectType->objectTypeID] = $objectType->getProcessor();
427 }
428 }
429
430 return $this->embeddedObjectHandlers;
431 }
432
433 /**
434 * Returns a specific embedded object handler.
435 *
436 * @param integer $objectTypeID
437 * @return IMessageEmbeddedObjectHandler
438 */
439 protected function getEmbeddedObjectHandler($objectTypeID) {
440 $this->getEmbeddedObjectHandlers();
441
442 return $this->embeddedObjectHandlers[$objectTypeID];
443 }
444
445 /**
446 * @deprecated 3.0
447 */
448 public function parseTemporaryMessage() {
449 throw new \BadMethodCallException("parseTemporaryMessage() has been removed, please use registerTemporaryMessage() instead.");
450 }
451 }