Merge branch '5.2' into 5.3
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / tagging / TagEngine.class.php
CommitLineData
04c06e85
MW
1<?php
2namespace wcf\system\tagging;
3use wcf\data\object\type\ObjectTypeCache;
4use wcf\data\tag\Tag;
5use wcf\data\tag\TagAction;
1bcacda8 6use wcf\data\tag\TagList;
04c06e85 7use wcf\system\database\util\PreparedStatementConditionBuilder;
79bbb75a 8use wcf\system\exception\InvalidObjectTypeException;
95939e32 9use wcf\system\language\LanguageFactory;
04c06e85
MW
10use wcf\system\SingletonFactory;
11use wcf\system\WCF;
32775cf9 12use wcf\util\ArrayUtil;
04c06e85
MW
13
14/**
15 * Manages the tagging of objects.
16 *
17 * @author Marcel Werk
7b7b9764 18 * @copyright 2001-2019 WoltLab GmbH
04c06e85 19 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
e71525e4 20 * @package WoltLabSuite\Core\System\Tagging
04c06e85
MW
21 */
22class TagEngine extends SingletonFactory {
23 /**
24 * Adds tags to a tagged object.
25 *
26 * @param string $objectType
27 * @param integer $objectID
28 * @param array $tags
29 * @param integer $languageID
30 * @param boolean $replace
31 */
32 public function addObjectTags($objectType, $objectID, array $tags, $languageID, $replace = true) {
33 $objectTypeID = $this->getObjectTypeID($objectType);
32775cf9
TD
34 $tags = array_unique(array_reduce(ArrayUtil::trim(array_map(function($tag) {
35 return explode(',', $tag);
36 }, $tags)), 'array_merge', []));
04c06e85
MW
37
38 // remove tags prior to apply the new ones (prevents duplicate entries)
39 if ($replace) {
40 $sql = "DELETE FROM wcf".WCF_N."_tag_to_object
41 WHERE objectTypeID = ?
42 AND objectID = ?
43 AND languageID = ?";
44 $statement = WCF::getDB()->prepareStatement($sql);
058cbd6a 45 $statement->execute([
04c06e85
MW
46 $objectTypeID,
47 $objectID,
48 $languageID
058cbd6a 49 ]);
04c06e85
MW
50 }
51
52 // get tag ids
058cbd6a 53 $tagIDs = [];
04c06e85
MW
54 foreach ($tags as $tag) {
55 if (empty($tag)) continue;
56
41520250 57 // enforce max length
838e315b
SG
58 if (mb_strlen($tag) > TAGGING_MAX_TAG_LENGTH) {
59 $tag = mb_substr($tag, 0, TAGGING_MAX_TAG_LENGTH);
41520250
MW
60 }
61
04c06e85
MW
62 // find existing tag
63 $tagObj = Tag::getTag($tag, $languageID);
64 if ($tagObj === null) {
04c06e85 65 // create new tag
058cbd6a 66 $tagAction = new TagAction([], 'create', ['data' => [
04c06e85
MW
67 'name' => $tag,
68 'languageID' => $languageID
058cbd6a 69 ]]);
04c06e85
MW
70
71 $tagAction->executeAction();
72 $returnValues = $tagAction->getReturnValues();
73 $tagObj = $returnValues['returnValues'];
74 }
75
76 if ($tagObj->synonymFor !== null) $tagIDs[$tagObj->synonymFor] = $tagObj->synonymFor;
77 else $tagIDs[$tagObj->tagID] = $tagObj->tagID;
78 }
79
80 // save tags
81 $sql = "INSERT INTO wcf".WCF_N."_tag_to_object
82 (objectID, tagID, objectTypeID, languageID)
06355ec3 83 VALUES (?, ?, ?, ?)";
04c06e85
MW
84 WCF::getDB()->beginTransaction();
85 $statement = WCF::getDB()->prepareStatement($sql);
86 foreach ($tagIDs as $tagID) {
058cbd6a 87 $statement->execute([$objectID, $tagID, $objectTypeID, $languageID]);
04c06e85
MW
88 }
89 WCF::getDB()->commitTransaction();
90 }
91
92 /**
93 * Deletes all tags assigned to given tagged object.
94 *
95 * @param string $objectType
96 * @param integer $objectID
97 * @param integer $languageID
98 */
99 public function deleteObjectTags($objectType, $objectID, $languageID = null) {
100 $objectTypeID = $this->getObjectTypeID($objectType);
101
102 $sql = "DELETE FROM wcf".WCF_N."_tag_to_object
103 WHERE objectTypeID = ?
104 AND objectID = ?
105 ".($languageID !== null ? "AND languageID = ?" : "");
106 $statement = WCF::getDB()->prepareStatement($sql);
058cbd6a 107 $parameters = [
04c06e85
MW
108 $objectTypeID,
109 $objectID
058cbd6a 110 ];
04c06e85
MW
111 if ($languageID !== null) $parameters[] = $languageID;
112 $statement->execute($parameters);
113 }
114
115 /**
116 * Deletes all tags assigned to given tagged objects.
117 *
118 * @param string $objectType
7a23a706 119 * @param integer[] $objectIDs
04c06e85
MW
120 */
121 public function deleteObjects($objectType, array $objectIDs) {
122 $objectTypeID = $this->getObjectTypeID($objectType);
123
124 $conditionsBuilder = new PreparedStatementConditionBuilder();
058cbd6a
MS
125 $conditionsBuilder->add('objectTypeID = ?', [$objectTypeID]);
126 $conditionsBuilder->add('objectID IN (?)', [$objectIDs]);
04c06e85
MW
127
128 $sql = "DELETE FROM wcf".WCF_N."_tag_to_object
129 ".$conditionsBuilder;
130 $statement = WCF::getDB()->prepareStatement($sql);
131 $statement->execute($conditionsBuilder->getParameters());
132 }
133
134 /**
135 * Returns all tags set for given object.
136 *
137 * @param string $objectType
138 * @param integer $objectID
7a23a706
MS
139 * @param integer[] $languageIDs
140 * @return Tag[]
04c06e85 141 */
058cbd6a
MS
142 public function getObjectTags($objectType, $objectID, array $languageIDs = []) {
143 $tags = $this->getObjectsTags($objectType, [$objectID], $languageIDs);
0caefc08 144
058cbd6a 145 return isset($tags[$objectID]) ? $tags[$objectID] : [];
0caefc08
MS
146 }
147
148 /**
149 * Returns all tags set for given objects.
150 *
151 * @param string $objectType
7a23a706
MS
152 * @param integer[] $objectIDs
153 * @param integer[] $languageIDs
0caefc08
MS
154 * @return array
155 */
058cbd6a 156 public function getObjectsTags($objectType, array $objectIDs, array $languageIDs = []) {
04c06e85
MW
157 $objectTypeID = $this->getObjectTypeID($objectType);
158
04c06e85 159 $conditions = new PreparedStatementConditionBuilder();
058cbd6a
MS
160 $conditions->add("tag_to_object.objectTypeID = ?", [$objectTypeID]);
161 $conditions->add("tag_to_object.objectID IN (?)", [$objectIDs]);
04c06e85
MW
162 if (!empty($languageIDs)) {
163 foreach ($languageIDs as $index => $languageID) {
164 if (!$languageID) unset($languageIDs[$index]);
165 }
166
95939e32
AE
167 // The `languageID` is part of the index, skipping it will cause MySQL to skip the
168 // `objectID` column, causing a partial table scan.
169 if (empty($languageIDs)) {
170 // The `languageID` column is never null, tags are always assigned to a language
171 // thus we cannot use the content language ids here.
172 foreach (LanguageFactory::getInstance()->getLanguages() as $language) {
173 $languageIDs[] = $language->languageID;
174 }
04c06e85 175 }
95939e32
AE
176
177 $conditions->add("tag_to_object.languageID IN (?)", [$languageIDs]);
04c06e85
MW
178 }
179
0caefc08 180 $sql = "SELECT tag.*, tag_to_object.objectID
04c06e85
MW
181 FROM wcf".WCF_N."_tag_to_object tag_to_object
182 LEFT JOIN wcf".WCF_N."_tag tag
183 ON (tag.tagID = tag_to_object.tagID)
184 ".$conditions;
185 $statement = WCF::getDB()->prepareStatement($sql);
186 $statement->execute($conditions->getParameters());
187
058cbd6a 188 $tags = [];
157054c9 189 while ($tag = $statement->fetchObject(Tag::class)) {
3b13d4ea
MS
190 /** @noinspection PhpUndefinedFieldInspection */
191 $objectID = $tag->objectID;
192 if (!isset($tags[$objectID])) {
193 $tags[$objectID] = [];
0caefc08 194 }
3b13d4ea 195 $tags[$objectID][$tag->tagID] = $tag;
04c06e85
MW
196 }
197
198 return $tags;
199 }
200
201 /**
202 * Returns id of the object type with the given name.
203 *
204 * @param string $objectType
205 * @return integer
79bbb75a 206 * @throws InvalidObjectTypeException
04c06e85
MW
207 */
208 public function getObjectTypeID($objectType) {
209 // get object type
210 $objectTypeObj = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.tagging.taggableObject', $objectType);
211 if ($objectTypeObj === null) {
79bbb75a 212 throw new InvalidObjectTypeException($objectType, 'com.woltlab.wcf.tagging.taggableObject');
04c06e85
MW
213 }
214
215 return $objectTypeObj->objectTypeID;
216 }
c43a45b4
AE
217
218 /**
219 * Returns the implicit language id based on the language id of existing tags.
220 *
221 * NULL indicates that there are no tags, otherwise the language id with the most
222 * associated tags for that object is returned, but can still be arbitrary if
223 * there are two or more top language ids with the same amount of tags.
224 *
225 * @param string $objectType
226 * @param integer $objectID
227 * @return integer|null
228 */
229 public function getImplicitLanguageID($objectType, $objectID) {
230 $existingTags = $this->getObjectTags($objectType, $objectID);
231 if (empty($existingTags)) {
232 return null;
233 }
234
c1f318fd 235 $languageIDs = [];
c43a45b4
AE
236 foreach ($existingTags as $tag) {
237 if (!isset($languageIDs[$tag->languageID])) $languageIDs[$tag->languageID] = 0;
238 $languageIDs[$tag->languageID]++;
239 }
240
241 arsort($languageIDs, SORT_NUMERIC);
242
243 return key($languageIDs);
244 }
1bcacda8 245
c9613f13
AE
246 /**
247 * @param Tag[] $tags
248 * @return int[]
249 */
250 public function getTagIDs($tags) {
251 return array_map(function($tag) {
252 return $tag->tagID;
253 }, $tags);
254 }
255
1bcacda8
AE
256 /**
257 * Generates the inner SQL statement to fetch object ids that have all listed
258 * tags assigned to them.
259 *
260 * @param string $objectType
261 * @param Tag[] $tags
262 * @return array
dd2d8c0c 263 * @since 5.2
1bcacda8 264 */
c9613f13 265 public function getSubselectForObjectsByTags($objectType, array $tags) {
1bcacda8 266 $parameters = [$this->getObjectTypeID($objectType)];
c9613f13 267 $tagIDs = implode(',', array_map(function(Tag $tag) use (&$parameters) {
1bcacda8
AE
268 $parameters[] = $tag->tagID;
269
270 return '?';
271 }, $tags));
272 $parameters[] = count($tags);
273
274 $sql = "SELECT objectID
275 FROM wcf".WCF_N."_tag_to_object
276 WHERE objectTypeID = ?
277 AND tagID IN (".$tagIDs.")
278 GROUP BY objectID
279 HAVING COUNT(objectID) = ?";
280
281 return [
282 'sql' => $sql,
283 'parameters' => $parameters,
284 ];
285 }
286
287 /**
288 * Returns the matching tags by name.
289 *
290 * @param string[] $names
291 * @param int $languageID
292 * @return Tag[]
dd2d8c0c 293 * @since 5.2
1bcacda8
AE
294 */
295 public function getTagsByName(array $names, $languageID) {
296 $tagList = new TagList();
297 $tagList->getConditionBuilder()->add('name IN (?)', [$names]);
298 $tagList->getConditionBuilder()->add('languageID = ?', [$languageID ?: WCF::getLanguage()->languageID]);
299 $tagList->readObjects();
300
301 return $tagList->getObjects();
302 }
04c06e85 303}