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