]> git.mxchange.org Git - friendica.git/blob - src/Model/Tag.php
829567fe5f9096f221ce81544d42bbbe0471d6b1
[friendica.git] / src / Model / Tag.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2020, Friendica
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Model;
23
24 use Friendica\Content\Text\BBCode;
25 use Friendica\Core\Logger;
26 use Friendica\Core\System;
27 use Friendica\Database\DBA;
28 use Friendica\DI;
29 use Friendica\Util\Strings;
30
31 /**
32  * Class Tag
33  *
34  * This Model class handles tag table interactions.
35  * This tables stores relevant tags related to posts, like hashtags and mentions.
36  */
37 class Tag
38 {
39         const UNKNOWN  = 0;
40         const HASHTAG  = 1;
41         const MENTION  = 2;
42         const CATEGORY = 3;
43         const FILE     = 5;
44         /**
45          * An implicit mention is a mention in a comment body that is redundant with the threading information.
46          */
47         const IMPLICIT_MENTION  = 8;
48         /**
49          * An exclusive mention transfers the ownership of the post to the target account, usually a forum.
50          */
51         const EXCLUSIVE_MENTION = 9;
52
53         const TAG_CHARACTER = [
54                 self::HASHTAG           => '#',
55                 self::MENTION           => '@',
56                 self::IMPLICIT_MENTION  => '%',
57                 self::EXCLUSIVE_MENTION => '!',
58         ];
59
60         /**
61          * Store tag/mention elements
62          *
63          * @param integer $uriid
64          * @param integer $type
65          * @param string  $name
66          * @param string  $url
67          * @param boolean $probing
68          */
69         public static function store(int $uriid, int $type, string $name, string $url = '', $probing = true)
70         {
71                 if ($type = self::HASHTAG) {
72                         // Remove some common "garbarge" from tags
73                         $name = trim($name, "\x00..\x20\xFF#!@,;.:'/?!^°$%".'"');
74
75                         $tags = explode(self::TAG_CHARACTER[self::HASHTAG], $name);
76                         if (count($tags) > 1) {
77                                 foreach ($tags as $tag) {
78                                         self::store($uriid, $type, $tag, $url, $probing);
79                                 }
80                                 return;
81                         }
82                 }
83
84                 if (empty($name)) {
85                         return;
86                 }
87
88                 $cid = 0;
89                 $tagid = 0;
90
91                 if (in_array($type, [self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION])) {
92                         if (empty($url)) {
93                                 // No mention without a contact url
94                                 return;
95                         }
96
97                         if (!$probing) {
98                                 $condition = ['nurl' => Strings::normaliseLink($url), 'uid' => 0, 'deleted' => false];
99                                 $contact = DBA::selectFirst('contact', ['id'], $condition, ['order' => ['id']]);
100                                 if (DBA::isResult($contact)) {
101                                         $cid = $contact['id'];
102                                         Logger::info('Got id for contact url', ['cid' => $cid, 'url' => $url]);
103                                 }
104
105                                 if (empty($cid)) {
106                                         $ssl_url = str_replace('http://', 'https://', $url);
107                                         $condition = ['`alias` IN (?, ?, ?) AND `uid` = ? AND NOT `deleted`', $url, Strings::normaliseLink($url), $ssl_url, 0];
108                                         $contact = DBA::selectFirst('contact', ['id'], $condition, ['order' => ['id']]);
109                                         if (DBA::isResult($contact)) {
110                                                 $cid = $contact['id'];
111                                                 Logger::info('Got id for contact alias', ['cid' => $cid, 'url' => $url]);
112                                         }
113                                 }
114                         } else {
115                                 $cid = Contact::getIdForURL($url, 0, true);
116                                 Logger::info('Got id by probing', ['cid' => $cid, 'url' => $url]);
117                         }
118
119                         if (empty($cid)) {
120                                 // The contact wasn't found in the system (most likely some dead account)
121                                 // We ensure that we only store a single entry by overwriting the previous name
122                                 Logger::info('Contact not found, updating tag', ['url' => $url, 'name' => $name]);
123                                 DBA::update('tag', ['name' => substr($name, 0, 96)], ['url' => $url]);
124                         }
125                 }
126
127                 if (empty($cid)) {
128                         $fields = ['name' => substr($name, 0, 96), 'url' => ''];
129
130                         if (($type != self::HASHTAG) && !empty($url) && ($url != $name)) {
131                                 $fields['url'] = strtolower($url);
132                         }
133
134                         $tag = DBA::selectFirst('tag', ['id'], $fields);
135                         if (!DBA::isResult($tag)) {
136                                 DBA::insert('tag', $fields, true);
137                                 $tagid = DBA::lastInsertId();
138                         } else {
139                                 $tagid = $tag['id'];
140                         }
141
142                         if (empty($tagid)) {
143                                 Logger::error('No tag id created', $fields);
144                                 return;
145                         }
146                 }
147
148                 $fields = ['uri-id' => $uriid, 'type' => $type, 'tid' => $tagid, 'cid' => $cid];
149
150                 if (in_array($type, [self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION])) {
151                         $condition = $fields;
152                         $condition['type'] = [self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION];
153                         if (DBA::exists('post-tag', $condition)) {
154                                 Logger::info('Tag already exists', $fields);
155                                 return;
156                         }
157                 }
158
159                 DBA::insert('post-tag', $fields, true);
160
161                 Logger::info('Stored tag/mention', ['uri-id' => $uriid, 'tag-id' => $tagid, 'contact-id' => $cid, 'name' => $name, 'type' => $type, 'callstack' => System::callstack(8)]);
162         }
163
164         /**
165          * Store tag/mention elements
166          *
167          * @param integer $uriid
168          * @param string $hash
169          * @param string $name
170          * @param string $url
171          * @param boolean $probing
172          */
173         public static function storeByHash(int $uriid, string $hash, string $name, string $url = '', $probing = true)
174         {
175                 $type = self::getTypeForHash($hash);
176                 if ($type == self::UNKNOWN) {
177                         return;
178                 }
179
180                 self::store($uriid, $type, $name, $url, $probing);
181         }
182
183         /**
184          * Store tags and mentions from the body
185          * 
186          * @param integer $uriid   URI-Id
187          * @param string  $body    Body of the post
188          * @param string  $tags    Accepted tags
189          * @param boolean $probing Perform a probing for contacts, adding them if needed
190          */
191         public static function storeFromBody(int $uriid, string $body, string $tags = null, $probing = true)
192         {
193                 if (is_null($tags)) {
194                         $tags =  self::TAG_CHARACTER[self::HASHTAG] . self::TAG_CHARACTER[self::MENTION] . self::TAG_CHARACTER[self::EXCLUSIVE_MENTION];
195                 }
196
197                 Logger::info('Check for tags', ['uri-id' => $uriid, 'hash' => $tags, 'callstack' => System::callstack()]);
198
199                 if (!preg_match_all("/([" . $tags . "])\[url\=([^\[\]]*)\]([^\[\]]*)\[\/url\]/ism", $body, $result, PREG_SET_ORDER)) {
200                         return;
201                 }
202
203                 Logger::info('Found tags', ['uri-id' => $uriid, 'hash' => $tags, 'result' => $result]);
204
205                 foreach ($result as $tag) {
206                         self::storeByHash($uriid, $tag[1], $tag[3], $tag[2], $probing);
207                 }
208         }
209
210         /**
211          * Store raw tags (not encapsulated in links) from the body
212          * This function is needed in the intermediate phase.
213          * Later we can call item::setHashtags in advance to have all tags converted.
214          * 
215          * @param integer $uriid URI-Id
216          * @param string  $body   Body of the post
217          */
218         public static function storeRawTagsFromBody(int $uriid, string $body)
219         {
220                 Logger::info('Check for tags', ['uri-id' => $uriid, 'callstack' => System::callstack()]);
221
222                 $result = BBCode::getTags($body);
223                 if (empty($result)) {
224                         return;
225                 }
226
227                 Logger::info('Found tags', ['uri-id' => $uriid, 'result' => $result]);
228
229                 foreach ($result as $tag) {
230                         if (substr($tag, 0, 1) != self::TAG_CHARACTER[self::HASHTAG]) {
231                                 continue;
232                         }
233                         self::storeByHash($uriid, substr($tag, 0, 1), substr($tag, 1));
234                 }
235         }
236
237         /**
238          * Checks for stored hashtags and mentions for the given post
239          *
240          * @param integer $uriid
241          * @return bool
242          */
243         public static function existsForPost(int $uriid)
244         {
245                 return DBA::exists('post-tag', ['uri-id' => $uriid, 'type' => [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION, self::EXCLUSIVE_MENTION]]);
246         }
247
248         /**
249          * Remove tag/mention
250          *
251          * @param integer $uriid
252          * @param integer $type
253          * @param string $name
254          * @param string $url
255          */
256         public static function remove(int $uriid, int $type, string $name, string $url = '')
257         {
258                 $condition = ['uri-id' => $uriid, 'type' => $type, 'url' => $url];
259                 if ($type == self::HASHTAG) {
260                         $condition['name'] = $name;
261                 }
262
263                 $tag = DBA::selectFirst('tag-view', ['tid', 'cid'], $condition);
264                 if (!DBA::isResult($tag)) {
265                         return;
266                 }
267
268                 Logger::info('Removing tag/mention', ['uri-id' => $uriid, 'tid' => $tag['tid'], 'name' => $name, 'url' => $url, 'callstack' => System::callstack(8)]);
269                 DBA::delete('post-tag', ['uri-id' => $uriid, 'type' => $type, 'tid' => $tag['tid'], 'cid' => $tag['cid']]);
270         }
271
272         /**
273          * Remove tag/mention
274          *
275          * @param integer $uriid
276          * @param string $hash
277          * @param string $name
278          * @param string $url
279          */
280         public static function removeByHash(int $uriid, string $hash, string $name, string $url = '')
281         {
282                 $type = self::getTypeForHash($hash);
283                 if ($type == self::UNKNOWN) {
284                         return;
285                 }
286
287                 self::remove($uriid, $type, $name, $url);
288         }
289
290         /**
291          * Get the type for the given hash
292          *
293          * @param string $hash
294          * @return integer type
295          */
296         private static function getTypeForHash(string $hash)
297         {
298                 if ($hash == self::TAG_CHARACTER[self::MENTION]) {
299                         return self::MENTION;
300                 } elseif ($hash == self::TAG_CHARACTER[self::EXCLUSIVE_MENTION]) {
301                         return self::EXCLUSIVE_MENTION;
302                 } elseif ($hash == self::TAG_CHARACTER[self::IMPLICIT_MENTION]) {
303                         return self::IMPLICIT_MENTION;
304                 } elseif ($hash == self::TAG_CHARACTER[self::HASHTAG]) {
305                         return self::HASHTAG;
306                 } else {
307                         return self::UNKNOWN;
308                 }
309         }
310
311         /**
312          * Retrieves the terms from the provided type(s) associated with the provided item ID.
313          *
314          * @param int       $item_id
315          * @param int|array $type
316          * @return array
317          * @throws \Exception
318          */
319         public static function ArrayFromURIId(int $uri_id, array $type = [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION, self::EXCLUSIVE_MENTION])
320         {
321                 $condition = ['uri-id' => $uri_id, 'type' => $type];
322                 $tags = DBA::select('tag-view', ['type', 'name', 'url'], $condition);
323                 if (!DBA::isResult($tags)) {
324                         return [];
325                 }
326
327                 $tag_list = [];
328                 while ($tag = DBA::fetch($tags)) {
329                         $tag['term'] = $tag['name']; /// @todo Remove this line when all occurrences of "term" had been replaced with "name"
330                         $tag_list[] = $tag;
331                 }
332
333                 return $tag_list;
334         }
335
336                 /**
337          * Sorts an item's tags into mentions, hashtags and other tags. Generate personalized URLs by user and modify the
338          * provided item's body with them.
339          *
340          * @param array $item
341          * @return array
342          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
343          * @throws \ImagickException
344          */
345         public static function populateTagsFromItem(&$item)
346         {
347                 $return = [
348                         'tags' => [],
349                         'hashtags' => [],
350                         'mentions' => [],
351                         'implicit_mentions' => [],
352                 ];
353
354                 $searchpath = DI::baseUrl() . "/search?tag=";
355
356                 $taglist = DBA::select('tag-view', ['type', 'name', 'url'],
357                         ['uri-id' => $item['uri-id'], 'type' => [self::HASHTAG, self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION]]);
358                 while ($tag = DBA::fetch($taglist)) {
359                         if ($tag['url'] == '') {
360                                 $tag['url'] = $searchpath . rawurlencode($tag['name']);
361                         }
362
363                         $orig_tag = $tag['url'];
364
365                         $prefix = self::TAG_CHARACTER[$tag['type']];
366                         switch($tag['type']) {
367                                 case self::HASHTAG:
368                                         if ($orig_tag != $tag['url']) {
369                                                 $item['body'] = str_replace($orig_tag, $tag['url'], $item['body']);
370                                         }
371
372                                         $return['hashtags'][] = $prefix . '<a href="' . $tag['url'] . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($tag['name']) . '</a>';
373                                         $return['tags'][] = $prefix . '<a href="' . $tag['url'] . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($tag['name']) . '</a>';
374                                         break;
375                                 case self::MENTION:
376                                 case self::EXCLUSIVE_MENTION:
377                                                 $tag['url'] = Contact::magicLink($tag['url']);
378                                         $return['mentions'][] = $prefix . '<a href="' . $tag['url'] . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($tag['name']) . '</a>';
379                                         $return['tags'][] = $prefix . '<a href="' . $tag['url'] . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($tag['name']) . '</a>';
380                                         break;
381                                 case self::IMPLICIT_MENTION:
382                                         $return['implicit_mentions'][] = $prefix . $tag['name'];
383                                         break;
384                         }
385                 }
386                 DBA::close($taglist);
387
388                 return $return;
389         }
390
391         /**
392          * Search posts for given tag
393          *
394          * @param string $search
395          * @param integer $uid
396          * @param integer $start
397          * @param integer $limit
398          * @return array with URI-ID
399          */
400         public static function getURIIdListForTag(string $search, int $uid = 0, int $start = 0, int $limit = 100)
401         {
402                 $condition = ["`name` = ? AND (NOT `private` OR (`private` AND `uid` = ?))", $search, $uid];
403                 $params = [
404                         'order' => ['uri-id' => true],
405                         'group_by' => ['uri-id'],
406                         'limit' => [$start, $limit]
407                 ];
408
409                 $tags = DBA::select('tag-search-view', ['uri-id'], $condition, $params);
410
411                 $uriids = [];
412                 while ($tag = DBA::fetch($tags)) {
413                         $uriids[] = $tag['uri-id'];
414                 }
415                 DBA::close($tags);
416
417                 return $uriids;
418         }
419 }