Commit | Line | Data |
---|---|---|
04c06e85 MW |
1 | <?php |
2 | namespace wcf\system\tagging; | |
3 | use wcf\data\object\type\ObjectTypeCache; | |
4 | use wcf\data\tag\Tag; | |
5 | use wcf\data\tag\TagAction; | |
1bcacda8 | 6 | use wcf\data\tag\TagList; |
04c06e85 | 7 | use wcf\system\database\util\PreparedStatementConditionBuilder; |
79bbb75a | 8 | use wcf\system\exception\InvalidObjectTypeException; |
95939e32 | 9 | use wcf\system\language\LanguageFactory; |
04c06e85 MW |
10 | use wcf\system\SingletonFactory; |
11 | use wcf\system\WCF; | |
32775cf9 | 12 | use 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 | */ |
22 | class 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 | } |