]> git.mxchange.org Git - friendica.git/blob - src/Model/Term.php
Merge pull request #6751 from MrPetovan/bug/fatal-errors
[friendica.git] / src / Model / Term.php
1 <?php
2 /**
3  * @file src/Model/Term.php
4  */
5 namespace Friendica\Model;
6
7 use Friendica\Core\Logger;
8 use Friendica\Core\System;
9 use Friendica\Database\DBA;
10 use Friendica\Util\Strings;
11
12 /**
13  * Class Term
14  *
15  * This Model class handles term table interactions.
16  * This tables stores relevant terms related to posts, photos and searches, like hashtags, mentions and
17  * user-applied categories.
18  *
19  * @package Friendica\Model
20  */
21 class Term
22 {
23     const UNKNOWN           = 0;
24     const HASHTAG           = 1;
25     const MENTION           = 2;
26     const CATEGORY          = 3;
27     const PCATEGORY         = 4;
28     const FILE              = 5;
29     const SAVEDSEARCH       = 6;
30     const CONVERSATION      = 7;
31         /**
32          * An implicit mention is a mention in a comment body that is redundant with the threading information.
33          */
34     const IMPLICIT_MENTION  = 8;
35         /**
36          * An exclusive mention transfers the ownership of the post to the target account, usually a forum.
37          */
38     const EXCLUSIVE_MENTION = 9;
39
40     const TAG_CHARACTER = [
41         self::HASHTAG           => '#',
42         self::MENTION           => '@',
43         self::IMPLICIT_MENTION  => '%',
44         self::EXCLUSIVE_MENTION => '!',
45     ];
46
47     const OBJECT_TYPE_POST  = 1;
48     const OBJECT_TYPE_PHOTO = 2;
49
50         /**
51          * Generates the legacy item.tag field comma-separated BBCode string from an item ID.
52          * Includes only hashtags, implicit and explicit mentions.
53          *
54          * @param int $item_id
55          * @return string
56          * @throws \Exception
57          */
58         public static function tagTextFromItemId($item_id)
59         {
60                 $tag_list = [];
61                 $tags = self::tagArrayFromItemId($item_id, [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION]);
62                 foreach ($tags as $tag) {
63                         $tag_list[] = self::TAG_CHARACTER[$tag['type']] . '[url=' . $tag['url'] . ']' . $tag['term'] . '[/url]';
64                 }
65
66                 return implode(',', $tag_list);
67         }
68
69         /**
70          * Retrieves the terms from the provided type(s) associated with the provided item ID.
71          *
72          * @param int       $item_id
73          * @param int|array $type
74          * @return array
75          * @throws \Exception
76          */
77         public static function tagArrayFromItemId($item_id, $type = [self::HASHTAG, self::MENTION])
78         {
79                 $condition = ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'type' => $type];
80                 $tags = DBA::select('term', ['type', 'term', 'url'], $condition);
81                 if (!DBA::isResult($tags)) {
82                         return [];
83                 }
84
85                 return DBA::toArray($tags);
86         }
87
88         /**
89          * Generates the legacy item.file field string from an item ID.
90          * Includes only file and category terms.
91          *
92          * @param int $item_id
93          * @return string
94          * @throws \Exception
95          */
96         public static function fileTextFromItemId($item_id)
97         {
98                 $file_text = '';
99                 $tags = self::tagArrayFromItemId($item_id, [self::FILE, self::CATEGORY]);
100                 foreach ($tags as $tag) {
101                         if ($tag['type'] == self::CATEGORY) {
102                                 $file_text .= '<' . $tag['term'] . '>';
103                         } else {
104                                 $file_text .= '[' . $tag['term'] . ']';
105                         }
106                 }
107
108                 return $file_text;
109         }
110
111         /**
112          * Inserts new terms for the provided item ID based on the legacy item.tag field BBCode content.
113          * Deletes all previous tag terms for the same item ID.
114          * Sets both the item.mention and thread.mentions field flags if a mention concerning the item UID is found.
115          *
116          * @param int    $item_id
117          * @param string $tag_str
118          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
119          */
120         public static function insertFromTagFieldByItemId($item_id, $tag_str)
121         {
122                 $profile_base = System::baseUrl();
123                 $profile_data = parse_url($profile_base);
124                 $profile_path = defaults($profile_data, 'path', '');
125                 $profile_base_friendica = $profile_data['host'] . $profile_path . '/profile/';
126                 $profile_base_diaspora = $profile_data['host'] . $profile_path . '/u/';
127
128                 $fields = ['guid', 'uid', 'id', 'edited', 'deleted', 'created', 'received', 'title', 'body', 'parent'];
129                 $item = Item::selectFirst($fields, ['id' => $item_id]);
130                 if (!DBA::isResult($item)) {
131                         return;
132                 }
133
134                 $item['tag'] = $tag_str;
135
136                 // Clean up all tags
137                 self::deleteByItemId($item_id);
138
139                 if ($item['deleted']) {
140                         return;
141                 }
142
143                 $taglist = explode(',', $item['tag']);
144
145                 $tags_string = '';
146                 foreach ($taglist as $tag) {
147                         if (Strings::startsWith($tag, self::TAG_CHARACTER)) {
148                                 $tags_string .= ' ' . trim($tag);
149                         } else {
150                                 $tags_string .= ' #' . trim($tag);
151                         }
152                 }
153
154                 $data = ' ' . $item['title'] . ' ' . $item['body'] . ' ' . $tags_string . ' ';
155
156                 // ignore anything in a code block
157                 $data = preg_replace('/\[code\](.*?)\[\/code\]/sm', '', $data);
158
159                 $tags = [];
160
161                 $pattern = '/\W\#([^\[].*?)[\s\'".,:;\?!\[\]\/]/ism';
162                 if (preg_match_all($pattern, $data, $matches)) {
163                         foreach ($matches[1] as $match) {
164                                 $tags['#' . $match] = '';
165                         }
166                 }
167
168                 $pattern = '/\W([\#@!%])\[url\=(.*?)\](.*?)\[\/url\]/ism';
169                 if (preg_match_all($pattern, $data, $matches, PREG_SET_ORDER)) {
170                         foreach ($matches as $match) {
171
172                                 if (in_array($match[1], [
173                                         self::TAG_CHARACTER[self::MENTION],
174                                         self::TAG_CHARACTER[self::IMPLICIT_MENTION],
175                                         self::TAG_CHARACTER[self::EXCLUSIVE_MENTION]
176                                 ])) {
177                                         $contact = Contact::getDetailsByURL($match[2], 0);
178                                         if (!empty($contact['addr'])) {
179                                                 $match[3] = $contact['addr'];
180                                         }
181
182                                         if (!empty($contact['url'])) {
183                                                 $match[2] = $contact['url'];
184                                         }
185                                 }
186
187                                 $tags[$match[2]] = $match[1] . trim($match[3], ',.:;[]/\"?!');
188                         }
189                 }
190
191                 foreach ($tags as $link => $tag) {
192                         if (self::isType($tag, self::HASHTAG)) {
193                                 // try to ignore #039 or #1 or anything like that
194                                 if (ctype_digit(substr(trim($tag), 1))) {
195                                         continue;
196                                 }
197
198                                 // try to ignore html hex escapes, e.g. #x2317
199                                 if ((substr(trim($tag), 1, 1) == 'x' || substr(trim($tag), 1, 1) == 'X') && ctype_digit(substr(trim($tag), 2))) {
200                                         continue;
201                                 }
202
203                                 $type = self::HASHTAG;
204                                 $term = substr($tag, 1);
205                                 $link = '';
206                         } elseif (self::isType($tag, self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION)) {
207                                 if (self::isType($tag, self::MENTION, self::EXCLUSIVE_MENTION)) {
208                                         $type = self::MENTION;
209                                 } else {
210                                         $type = self::IMPLICIT_MENTION;
211                                 }
212
213                                 $contact = Contact::getDetailsByURL($link, 0);
214                                 if (!empty($contact['name'])) {
215                                         $term = $contact['name'];
216                                 } else {
217                                         $term = substr($tag, 1);
218                                 }
219                         } else { // This shouldn't happen
220                                 $type = self::HASHTAG;
221                                 $term = $tag;
222                                 $link = '';
223
224                                 Logger::notice('Unknown term type', ['tag' => $tag]);
225                         }
226
227                         if (DBA::exists('term', ['uid' => $item['uid'], 'otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'term' => $term, 'type' => $type])) {
228                                 continue;
229                         }
230
231                         if ($item['uid'] == 0) {
232                                 $global = true;
233                                 DBA::update('term', ['global' => true], ['otype' => self::OBJECT_TYPE_POST, 'guid' => $item['guid']]);
234                         } else {
235                                 $global = DBA::exists('term', ['uid' => 0, 'otype' => self::OBJECT_TYPE_POST, 'guid' => $item['guid']]);
236                         }
237
238                         DBA::insert('term', [
239                                 'uid'      => $item['uid'],
240                                 'oid'      => $item_id,
241                                 'otype'    => self::OBJECT_TYPE_POST,
242                                 'type'     => $type,
243                                 'term'     => $term,
244                                 'url'      => $link,
245                                 'guid'     => $item['guid'],
246                                 'created'  => $item['created'],
247                                 'received' => $item['received'],
248                                 'global'   => $global
249                         ]);
250
251                         // Search for mentions
252                         if (self::isType($tag, self::MENTION, self::EXCLUSIVE_MENTION)
253                                 && (
254                                         strpos($link, $profile_base_friendica) !== false
255                                         || strpos($link, $profile_base_diaspora) !== false
256                                 )
257                         ) {
258                                 $users_stmt = DBA::p("SELECT `uid` FROM `contact` WHERE self AND (`url` = ? OR `nurl` = ?)", $link, $link);
259                                 $users = DBA::toArray($users_stmt);
260                                 foreach ($users AS $user) {
261                                         if ($user['uid'] == $item['uid']) {
262                                                 /// @todo This function is called from Item::update - so we mustn't call that function here
263                                                 DBA::update('item', ['mention' => true], ['id' => $item_id]);
264                                                 DBA::update('thread', ['mention' => true], ['iid' => $item['parent']]);
265                                         }
266                                 }
267                         }
268                 }
269         }
270
271         /**
272          * Inserts new terms for the provided item ID based on the legacy item.file field BBCode content.
273          * Deletes all previous file terms for the same item ID.
274          *
275          * @param integer $item_id item id
276          * @param         $files
277          * @return void
278          * @throws \Exception
279          */
280         public static function insertFromFileFieldByItemId($item_id, $files)
281         {
282                 $message = Item::selectFirst(['uid', 'deleted'], ['id' => $item_id]);
283                 if (!DBA::isResult($message)) {
284                         return;
285                 }
286
287                 // Clean up all tags
288                 DBA::delete('term', ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'type' => [self::FILE, self::CATEGORY]]);
289
290                 if ($message["deleted"]) {
291                         return;
292                 }
293
294                 $message['file'] = $files;
295
296                 if (preg_match_all("/\[(.*?)\]/ism", $message["file"], $files)) {
297                         foreach ($files[1] as $file) {
298                                 DBA::insert('term', [
299                                         'uid' => $message["uid"],
300                                         'oid' => $item_id,
301                                         'otype' => self::OBJECT_TYPE_POST,
302                                         'type' => self::FILE,
303                                         'term' => $file
304                                 ]);
305                         }
306                 }
307
308                 if (preg_match_all("/\<(.*?)\>/ism", $message["file"], $files)) {
309                         foreach ($files[1] as $file) {
310                                 DBA::insert('term', [
311                                         'uid' => $message["uid"],
312                                         'oid' => $item_id,
313                                         'otype' => self::OBJECT_TYPE_POST,
314                                         'type' => self::CATEGORY,
315                                         'term' => $file
316                                 ]);
317                         }
318                 }
319         }
320
321         /**
322          * Sorts an item's tags into mentions, hashtags and other tags. Generate personalized URLs by user and modify the
323          * provided item's body with them.
324          *
325          * @param array $item
326          * @return array
327          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
328          * @throws \ImagickException
329          */
330         public static function populateTagsFromItem(&$item)
331         {
332                 $return = [
333                         'tags' => [],
334                         'hashtags' => [],
335                         'mentions' => [],
336                         'implicit_mentions' => [],
337                 ];
338
339                 $searchpath = System::baseUrl() . "/search?tag=";
340
341                 $taglist = DBA::select(
342                         'term',
343                         ['type', 'term', 'url'],
344                         ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item['id'], 'type' => [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION]],
345                         ['order' => ['tid']]
346                 );
347                 while ($tag = DBA::fetch($taglist)) {
348                         if ($tag['url'] == '') {
349                                 $tag['url'] = $searchpath . rawurlencode($tag['term']);
350                         }
351
352                         $orig_tag = $tag['url'];
353
354                         $prefix = self::TAG_CHARACTER[$tag['type']];
355                         switch($tag['type']) {
356                                 case self::HASHTAG:
357                                         if ($orig_tag != $tag['url']) {
358                                                 $item['body'] = str_replace($orig_tag, $tag['url'], $item['body']);
359                                         }
360
361                                         $return['hashtags'][] = $prefix . '<a href="' . $tag['url'] . '" target="_blank">' . $tag['term'] . '</a>';
362                                         $return['tags'][] = $prefix . '<a href="' . $tag['url'] . '" target="_blank">' . $tag['term'] . '</a>';
363                                         break;
364                                 case self::MENTION:
365                                         $tag['url'] = Contact::magicLink($tag['url']);
366                                         $return['mentions'][] = $prefix . '<a href="' . $tag['url'] . '" target="_blank">' . $tag['term'] . '</a>';
367                                         $return['tags'][] = $prefix . '<a href="' . $tag['url'] . '" target="_blank">' . $tag['term'] . '</a>';
368                                         break;
369                                 case self::IMPLICIT_MENTION:
370                                         $return['implicit_mentions'][] = $prefix . $tag['term'];
371                                         break;
372                         }
373                 }
374                 DBA::close($taglist);
375
376                 return $return;
377         }
378
379         /**
380          * Delete tags of the specific type(s) from an item
381          *
382          * @param int       $item_id
383          * @param int|array $type
384          * @throws \Exception
385          */
386         public static function deleteByItemId($item_id, $type = [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION])
387         {
388                 if (empty($item_id)) {
389                         return;
390                 }
391
392                 // Clean up all tags
393                 DBA::delete('term', ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'type' => $type]);
394         }
395
396         /**
397          * Check if the provided tag is of one of the provided term types.
398          *
399          * @param string $tag
400          * @param int    ...$types
401          * @return bool
402          */
403         public static function isType($tag, ...$types)
404         {
405                 $tag_chars = [];
406                 foreach ($types as $type) {
407                         if (isset(self::TAG_CHARACTER[$type])) {
408                                 $tag_chars[] = self::TAG_CHARACTER[$type];
409                         }
410                 }
411
412                 return Strings::startsWith($tag, $tag_chars);
413         }
414 }