]> git.mxchange.org Git - friendica.git/blob - src/Model/Tag.php
cf453f2431356ec6e9498b3662050cae36ce773b
[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\Util\Strings;
29
30 /**
31  * Class Tag
32  *
33  * This Model class handles tag table interactions.
34  * This tables stores relevant tags related to posts, like hashtags and mentions.
35  */
36 class Tag
37 {
38         const UNKNOWN  = 0;
39         const HASHTAG  = 1;
40         const MENTION  = 2;
41         const CATEGORY = 3;
42         const FILE     = 5;
43         /**
44          * An implicit mention is a mention in a comment body that is redundant with the threading information.
45          */
46         const IMPLICIT_MENTION  = 8;
47         /**
48          * An exclusive mention transfers the ownership of the post to the target account, usually a forum.
49          */
50         const EXCLUSIVE_MENTION = 9;
51
52         const TAG_CHARACTER = [
53                 self::HASHTAG           => '#',
54                 self::MENTION           => '@',
55                 self::IMPLICIT_MENTION  => '%',
56                 self::EXCLUSIVE_MENTION => '!',
57         ];
58
59         /**
60          * Store tag/mention elements
61          *
62          * @param integer $uriid
63          * @param integer $type
64          * @param string  $name
65          * @param string  $url
66          * @param boolean $probing
67          */
68         public static function store(int $uriid, int $type, string $name, string $url = '', $probing = true)
69         {
70                 $name = trim($name, "\x00..\x20\xFF#!@");
71                 if (empty($name)) {
72                         return;
73                 }
74
75                 $cid = 0;
76                 $tagid = 0;
77
78                 if (in_array($type, [Tag::MENTION, Tag::EXCLUSIVE_MENTION, Tag::IMPLICIT_MENTION])) {
79                         if (empty($url)) {
80                                 // No mention without a contact url
81                                 return;
82                         }
83
84                         if (!$probing) {
85                                 $condition = ['nurl' => Strings::normaliseLink($url), 'uid' => 0, 'deleted' => false];
86                                 $contact = DBA::selectFirst('contact', ['id'], $condition, ['order' => ['id']]);
87                                 if (DBA::isResult($contact)) {
88                                         $cid = $contact['id'];
89                                         Logger::info('Got id for contact url', ['cid' => $cid, 'url' => $url]);
90                                 }
91
92                                 if (empty($cid)) {
93                                         $ssl_url = str_replace('http://', 'https://', $url);
94                                         $condition = ['`alias` IN (?, ?, ?) AND `uid` = ? AND NOT `deleted`', $url, Strings::normaliseLink($url), $ssl_url, 0];
95                                         $contact = DBA::selectFirst('contact', ['id'], $condition, ['order' => ['id']]);
96                                         if (DBA::isResult($contact)) {
97                                                 $cid = $contact['id'];
98                                                 Logger::info('Got id for contact alias', ['cid' => $cid, 'url' => $url]);
99                                         }
100                                 }
101                         } else {
102                                 $cid = Contact::getIdForURL($url, 0, true);
103                                 Logger::info('Got id by probing', ['cid' => $cid, 'url' => $url]);
104                         }
105
106                         if (empty($cid)) {
107                                 // The contact wasn't found in the system (most likely some dead account)
108                                 // We ensure that we only store a single entry by overwriting the previous name
109                                 Logger::info('Contact not found, updating tag', ['url' => $url, 'name' => $name]);
110                                 DBA::update('tag', ['name' => substr($name, 0, 96)], ['url' => $url]);
111                         }
112                 }
113
114                 if (empty($cid)) {
115                         $fields = ['name' => substr($name, 0, 96), 'url' => ''];
116
117                         if (($type != Tag::HASHTAG) && !empty($url) && ($url != $name)) {
118                                 $fields['url'] = strtolower($url);
119                         }
120
121                         $tag = DBA::selectFirst('tag', ['id'], $fields);
122                         if (!DBA::isResult($tag)) {
123                                 DBA::insert('tag', $fields, true);
124                                 $tagid = DBA::lastInsertId();
125                         } else {
126                                 $tagid = $tag['id'];
127                         }
128
129                         if (empty($tagid)) {
130                                 Logger::error('No tag id created', $fields);
131                                 return;
132                         }
133                 }
134
135                 $fields = ['uri-id' => $uriid, 'type' => $type, 'tid' => $tagid, 'cid' => $cid];
136
137                 if (in_array($type, [Tag::MENTION, Tag::EXCLUSIVE_MENTION, Tag::IMPLICIT_MENTION])) {
138                         $condition = $fields;
139                         $condition['type'] = [Tag::MENTION, Tag::EXCLUSIVE_MENTION, Tag::IMPLICIT_MENTION];
140                         if (DBA::exists('post-tag', $condition)) {
141                                 Logger::info('Tag already exists', $fields);
142                                 return;
143                         }
144                 }
145
146                 DBA::insert('post-tag', $fields, true);
147
148                 Logger::info('Stored tag/mention', ['uri-id' => $uriid, 'tag-id' => $tagid, 'contact-id' => $cid, 'name' => $name, 'type' => $type, 'callstack' => System::callstack(8)]);
149         }
150
151         /**
152          * Store tag/mention elements
153          *
154          * @param integer $uriid
155          * @param string $hash
156          * @param string $name
157          * @param string $url
158          * @param boolean $probing
159          */
160         public static function storeByHash(int $uriid, string $hash, string $name, string $url = '', $probing = true)
161         {
162                 $type = self::getTypeForHash($hash);
163                 if ($type == self::UNKNOWN) {
164                         return;
165                 }
166
167                 self::store($uriid, $type, $name, $url, $probing);
168         }
169
170         /**
171          * Store tags and mentions from the body
172          * 
173          * @param integer $uriid   URI-Id
174          * @param string  $body    Body of the post
175          * @param string  $tags    Accepted tags
176          * @param boolean $probing Perform a probing for contacts, adding them if needed
177          */
178         public static function storeFromBody(int $uriid, string $body, string $tags = null, $probing = true)
179         {
180                 if (is_null($tags)) {
181                         $tags =  self::TAG_CHARACTER[self::HASHTAG] . self::TAG_CHARACTER[self::MENTION] . self::TAG_CHARACTER[self::EXCLUSIVE_MENTION];
182                 }
183
184                 Logger::info('Check for tags', ['uri-id' => $uriid, 'hash' => $tags, 'callstack' => System::callstack()]);
185
186                 if (!preg_match_all("/([" . $tags . "])\[url\=([^\[\]]*)\]([^\[\]]*)\[\/url\]/ism", $body, $result, PREG_SET_ORDER)) {
187                         return;
188                 }
189
190                 Logger::info('Found tags', ['uri-id' => $uriid, 'hash' => $tags, 'result' => $result]);
191
192                 foreach ($result as $tag) {
193                         self::storeByHash($uriid, $tag[1], $tag[3], $tag[2], $probing);
194                 }
195         }
196
197         /**
198          * Store raw tags (not encapsulated in links) from the body
199          * This function is needed in the intermediate phase.
200          * Later we can call item::setHashtags in advance to have all tags converted.
201          * 
202          * @param integer $uriid URI-Id
203          * @param string  $body   Body of the post
204          */
205         public static function storeRawTagsFromBody(int $uriid, string $body)
206         {
207                 Logger::info('Check for tags', ['uri-id' => $uriid, 'callstack' => System::callstack()]);
208
209                 $result = BBCode::getTags($body);
210                 if (empty($result)) {
211                         return;
212                 }
213
214                 Logger::info('Found tags', ['uri-id' => $uriid, 'result' => $result]);
215
216                 foreach ($result as $tag) {
217                         if (substr($tag, 0, 1) != self::TAG_CHARACTER[self::HASHTAG]) {
218                                 continue;
219                         }
220                         self::storeByHash($uriid, substr($tag, 0, 1), substr($tag, 1));
221                 }
222         }
223
224         /**
225          * Remove tag/mention
226          *
227          * @param integer $uriid
228          * @param integer $type
229          * @param string $name
230          * @param string $url
231          */
232         public static function remove(int $uriid, int $type, string $name, string $url = '')
233         {
234                 $condition = ['uri-id' => $uriid, 'type' => $type, 'url' => $url];
235                 if ($type == self::HASHTAG) {
236                         $condition['name'] = $name;
237                 }
238
239                 $tag = DBA::selectFirst('tag-view', ['tid', 'cid'], $condition);
240                 if (!DBA::isResult($tag)) {
241                         return;
242                 }
243
244                 Logger::info('Removing tag/mention', ['uri-id' => $uriid, 'tid' => $tag['tid'], 'name' => $name, 'url' => $url, 'callstack' => System::callstack(8)]);
245                 DBA::delete('post-tag', ['uri-id' => $uriid, 'type' => $type, 'tid' => $tag['tid'], 'cid' => $tag['cid']]);
246         }
247
248         /**
249          * Remove tag/mention
250          *
251          * @param integer $uriid
252          * @param string $hash
253          * @param string $name
254          * @param string $url
255          */
256         public static function removeByHash(int $uriid, string $hash, string $name, string $url = '')
257         {
258                 $type = self::getTypeForHash($hash);
259                 if ($type == self::UNKNOWN) {
260                         return;
261                 }
262
263                 self::remove($uriid, $type, $name, $url);
264         }
265
266         /**
267          * Get the type for the given hash
268          *
269          * @param string $hash
270          * @return integer type
271          */
272         private static function getTypeForHash(string $hash)
273         {
274                 if ($hash == self::TAG_CHARACTER[self::MENTION]) {
275                         return self::MENTION;
276                 } elseif ($hash == self::TAG_CHARACTER[self::EXCLUSIVE_MENTION]) {
277                         return self::EXCLUSIVE_MENTION;
278                 } elseif ($hash == self::TAG_CHARACTER[self::IMPLICIT_MENTION]) {
279                         return self::IMPLICIT_MENTION;
280                 } elseif ($hash == self::TAG_CHARACTER[self::HASHTAG]) {
281                         return self::HASHTAG;
282                 } else {
283                         return self::UNKNOWN;
284                 }
285
286         }
287 }