diff --git a/boot.php b/boot.php index 14c0a88ed..830a636ac 100644 --- a/boot.php +++ b/boot.php @@ -25,6 +25,7 @@ use Friendica\Core\Protocol; use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\Model\Contact; +use Friendica\Model\Term; use Friendica\Util\BasePath; use Friendica\Util\DateTimeFormat; @@ -171,23 +172,27 @@ define('NOTIFY_SYSTEM', 32768); /* @}*/ -/** - * @name Term - * - * Tag/term types - * @{ - */ -define('TERM_UNKNOWN', 0); -define('TERM_HASHTAG', 1); -define('TERM_MENTION', 2); -define('TERM_CATEGORY', 3); -define('TERM_PCATEGORY', 4); -define('TERM_FILE', 5); -define('TERM_SAVEDSEARCH', 6); -define('TERM_CONVERSATION', 7); +/** @deprecated since 2019.03, use Term::UNKNOWN instead */ +define('TERM_UNKNOWN', Term::UNKNOWN); +/** @deprecated since 2019.03, use Term::HASHTAG instead */ +define('TERM_HASHTAG', Term::HASHTAG); +/** @deprecated since 2019.03, use Term::MENTION instead */ +define('TERM_MENTION', Term::MENTION); +/** @deprecated since 2019.03, use Term::CATEGORY instead */ +define('TERM_CATEGORY', Term::CATEGORY); +/** @deprecated since 2019.03, use Term::PCATEGORY instead */ +define('TERM_PCATEGORY', Term::PCATEGORY); +/** @deprecated since 2019.03, use Term::FILE instead */ +define('TERM_FILE', Term::FILE); +/** @deprecated since 2019.03, use Term::SAVEDSEARCH instead */ +define('TERM_SAVEDSEARCH', Term::SAVEDSEARCH); +/** @deprecated since 2019.03, use Term::CONVERSATION instead */ +define('TERM_CONVERSATION', Term::CONVERSATION); -define('TERM_OBJ_POST', 1); -define('TERM_OBJ_PHOTO', 2); +/** @deprecated since 2019.03, use Term::OBJECT_TYPE_POST instead */ +define('TERM_OBJ_POST', Term::OBJECT_TYPE_POST); +/** @deprecated since 2019.03, use Term::OBJECT_TYPE_PHOTO instead */ +define('TERM_OBJ_PHOTO', Term::OBJECT_TYPE_PHOTO); /** * @name Namespaces diff --git a/src/Model/Term.php b/src/Model/Term.php index 6e3425524..5266627ac 100644 --- a/src/Model/Term.php +++ b/src/Model/Term.php @@ -6,32 +6,61 @@ namespace Friendica\Model; use Friendica\Core\System; use Friendica\Database\DBA; +use Friendica\Util\Strings; class Term { - public static function tagTextFromItemId($itemid) - { - $tag_text = ''; - $condition = ['otype' => TERM_OBJ_POST, 'oid' => $itemid, 'type' => [TERM_HASHTAG, TERM_MENTION]]; - $tags = DBA::select('term', [], $condition); - while ($tag = DBA::fetch($tags)) { - if ($tag_text != '') { - $tag_text .= ','; - } + const UNKNOWN = 0; + const HASHTAG = 1; + const MENTION = 2; + const CATEGORY = 3; + const PCATEGORY = 4; + const FILE = 5; + const SAVEDSEARCH = 6; + const CONVERSATION = 7; + const IMPLICIT_MENTION = 8; + const EXCLUSIVE_MENTION = 9; - if ($tag['type'] == 1) { - $tag_text .= '#'; - } else { - $tag_text .= '@'; - } - $tag_text .= '[url=' . $tag['url'] . ']' . $tag['term'] . '[/url]'; + const TAG_CHARACTER = [ + self::HASHTAG => '#', + self::MENTION => '@', + self::IMPLICIT_MENTION => '%', + self::EXCLUSIVE_MENTION => '!', + ]; + + const OBJECT_TYPE_POST = 1; + const OBJECT_TYPE_PHOTO = 2; + + /** + * Generates the legacy item.tag field comma-separated BBCode string from an item ID. + * Includes only hashtags, implicit and explicit mentions. + * + * @param int $item_id + * @return string + * @throws \Exception + */ + public static function tagTextFromItemId($item_id) + { + $tag_list = []; + $tags = self::tagArrayFromItemId($item_id, [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION]); + foreach ($tags as $tag) { + $tag_list[] = self::TAG_CHARACTER[$tag['type']] . '[url=' . $tag['url'] . ']' . $tag['term'] . '[/url]'; } - return $tag_text; + + return implode(',', $tag_list); } - public static function tagArrayFromItemId($itemid, $type = [TERM_HASHTAG, TERM_MENTION]) + /** + * Retrieves the terms from the provided type(s) associated with the provided item ID. + * + * @param int $item_id + * @param int|array $type + * @return array + * @throws \Exception + */ + public static function tagArrayFromItemId($item_id, $type = [self::HASHTAG, self::MENTION]) { - $condition = ['otype' => TERM_OBJ_POST, 'oid' => $itemid, 'type' => $type]; + $condition = ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'type' => $type]; $tags = DBA::select('term', ['type', 'term', 'url'], $condition); if (!DBA::isResult($tags)) { return []; @@ -40,22 +69,39 @@ class Term return DBA::toArray($tags); } - public static function fileTextFromItemId($itemid) + /** + * Generates the legacy item.file field string from an item ID. + * Includes only file and category terms. + * + * @param int $item_id + * @return string + * @throws \Exception + */ + public static function fileTextFromItemId($item_id) { $file_text = ''; - $condition = ['otype' => TERM_OBJ_POST, 'oid' => $itemid, 'type' => [TERM_FILE, TERM_CATEGORY]]; - $tags = DBA::select('term', [], $condition); - while ($tag = DBA::fetch($tags)) { - if ($tag['type'] == TERM_CATEGORY) { + $tags = self::tagArrayFromItemId($item_id, [self::FILE, self::CATEGORY]); + foreach ($tags as $tag) { + if ($tag['type'] == self::CATEGORY) { $file_text .= '<' . $tag['term'] . '>'; } else { $file_text .= '[' . $tag['term'] . ']'; } } + return $file_text; } - public static function insertFromTagFieldByItemId($itemid, $tags) + /** + * Inserts new terms for the provided item ID based on the legacy item.tag field BBCode content. + * Deletes all previous tag terms for the same item ID. + * Sets both the item.mention and thread.mentions field flags if a mention concerning the item UID is found. + * + * @param int $item_id + * @param string $tag_str + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public static function insertFromTagFieldByItemId($item_id, $tag_str) { $profile_base = System::baseUrl(); $profile_data = parse_url($profile_base); @@ -64,32 +110,32 @@ class Term $profile_base_diaspora = $profile_data['host'] . $profile_path . '/u/'; $fields = ['guid', 'uid', 'id', 'edited', 'deleted', 'created', 'received', 'title', 'body', 'parent']; - $message = Item::selectFirst($fields, ['id' => $itemid]); - if (!DBA::isResult($message)) { + $item = Item::selectFirst($fields, ['id' => $item_id]); + if (!DBA::isResult($item)) { return; } - $message['tag'] = $tags; + $item['tag'] = $tag_str; // Clean up all tags - self::deleteByItemId($itemid); + self::deleteByItemId($item_id); - if ($message['deleted']) { + if ($item['deleted']) { return; } - $taglist = explode(',', $message['tag']); + $taglist = explode(',', $item['tag']); $tags_string = ''; foreach ($taglist as $tag) { - if ((substr(trim($tag), 0, 1) == '#') || (substr(trim($tag), 0, 1) == '@') || (substr(trim($tag), 0, 1) == '!')) { + if (Strings::startsWith($tag, self::TAG_CHARACTER)) { $tags_string .= ' ' . trim($tag); } else { $tags_string .= ' #' . trim($tag); } } - $data = ' ' . $message['title'] . ' ' . $message['body'] . ' ' . $tags_string . ' '; + $data = ' ' . $item['title'] . ' ' . $item['body'] . ' ' . $tags_string . ' '; // ignore anything in a code block $data = preg_replace('/\[code\](.*?)\[\/code\]/sm', '', $data); @@ -103,11 +149,15 @@ class Term } } - $pattern = '/\W([\#@!])\[url\=(.*?)\](.*?)\[\/url\]/ism'; + $pattern = '/\W([\#@!%])\[url\=(.*?)\](.*?)\[\/url\]/ism'; if (preg_match_all($pattern, $data, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { - if (($match[1] == '@') || ($match[1] == '!')) { + if (in_array($match[1], [ + self::TAG_CHARACTER[self::MENTION], + self::TAG_CHARACTER[self::IMPLICIT_MENTION], + self::TAG_CHARACTER[self::EXCLUSIVE_MENTION] + ])) { $contact = Contact::getDetailsByURL($match[2], 0); if (!empty($contact['addr'])) { $match[3] = $contact['addr']; @@ -118,12 +168,12 @@ class Term } } - $tags[$match[1] . trim($match[3], ',.:;[]/\"?!')] = $match[2]; + $tags[$match[2]] = $match[1] . trim($match[3], ',.:;[]/\"?!'); } } - foreach ($tags as $tag => $link) { - if (substr(trim($tag), 0, 1) == '#') { + foreach ($tags as $link => $tag) { + if (self::isType($tag, self::HASHTAG)) { // try to ignore #039 or #1 or anything like that if (ctype_digit(substr(trim($tag), 1))) { continue; @@ -134,11 +184,15 @@ class Term continue; } - $type = TERM_HASHTAG; + $type = self::HASHTAG; $term = substr($tag, 1); $link = ''; - } elseif ((substr(trim($tag), 0, 1) == '@') || (substr(trim($tag), 0, 1) == '!')) { - $type = TERM_MENTION; + } elseif (self::isType($tag, self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION)) { + if (self::isType($tag, self::MENTION, self::EXCLUSIVE_MENTION)) { + $type = self::MENTION; + } else { + $type = self::IMPLICIT_MENTION; + } $contact = Contact::getDetailsByURL($link, 0); if (!empty($contact['name'])) { @@ -147,43 +201,49 @@ class Term $term = substr($tag, 1); } } else { // This shouldn't happen - $type = TERM_HASHTAG; + $type = self::HASHTAG; $term = $tag; $link = ''; } - if (DBA::exists('term', ['uid' => $message['uid'], 'otype' => TERM_OBJ_POST, 'oid' => $itemid, 'term' => $term])) { + if (DBA::exists('term', ['uid' => $item['uid'], 'otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'term' => $term, 'type' => $type])) { continue; } - if ($message['uid'] == 0) { + if ($item['uid'] == 0) { $global = true; - DBA::update('term', ['global' => true], ['otype' => TERM_OBJ_POST, 'guid' => $message['guid']]); + DBA::update('term', ['global' => true], ['otype' => self::OBJECT_TYPE_POST, 'guid' => $item['guid']]); } else { - $global = DBA::exists('term', ['uid' => 0, 'otype' => TERM_OBJ_POST, 'guid' => $message['guid']]); + $global = DBA::exists('term', ['uid' => 0, 'otype' => self::OBJECT_TYPE_POST, 'guid' => $item['guid']]); } DBA::insert('term', [ - 'uid' => $message['uid'], - 'oid' => $itemid, - 'otype' => TERM_OBJ_POST, + 'uid' => $item['uid'], + 'oid' => $item_id, + 'otype' => self::OBJECT_TYPE_POST, 'type' => $type, 'term' => $term, 'url' => $link, - 'guid' => $message['guid'], - 'created' => $message['created'], - 'received' => $message['received'], + 'guid' => $item['guid'], + 'created' => $item['created'], + 'received' => $item['received'], 'global' => $global ]); // Search for mentions - if (((substr($tag, 0, 1) == '@') || (substr($tag, 0, 1) == '!')) && (strpos($link, $profile_base_friendica) || strpos($link, $profile_base_diaspora))) { - $users = q("SELECT `uid` FROM `contact` WHERE self AND (`url` = '%s' OR `nurl` = '%s')", $link, $link); + if (self::isType($tag, self::MENTION, self::EXCLUSIVE_MENTION) + && ( + strpos($link, $profile_base_friendica) !== false + || strpos($link, $profile_base_diaspora) !== false + ) + ) { + $users_stmt = DBA::p("SELECT `uid` FROM `contact` WHERE self AND (`url` = ? OR `nurl` = ?)", $link, $link); + $users = DBA::toArray($users_stmt); foreach ($users AS $user) { - if ($user['uid'] == $message['uid']) { - /// @todo This function is called frim Item::update - so we mustn't call that function here - DBA::update('item', ['mention' => true], ['id' => $itemid]); - DBA::update('thread', ['mention' => true], ['iid' => $message['parent']]); + if ($user['uid'] == $item['uid']) { + /// @todo This function is called from Item::update - so we mustn't call that function here + DBA::update('item', ['mention' => true], ['id' => $item_id]); + DBA::update('thread', ['mention' => true], ['iid' => $item['parent']]); } } } @@ -191,20 +251,23 @@ class Term } /** - * @param integer $itemid item id + * Inserts new terms for the provided item ID based on the legacy item.file field BBCode content. + * Deletes all previous file terms for the same item ID. + * + * @param integer $item_id item id * @param $files * @return void * @throws \Exception */ - public static function insertFromFileFieldByItemId($itemid, $files) + public static function insertFromFileFieldByItemId($item_id, $files) { - $message = Item::selectFirst(['uid', 'deleted'], ['id' => $itemid]); + $message = Item::selectFirst(['uid', 'deleted'], ['id' => $item_id]); if (!DBA::isResult($message)) { return; } // Clean up all tags - DBA::delete('term', ['otype' => TERM_OBJ_POST, 'oid' => $itemid, 'type' => [TERM_FILE, TERM_CATEGORY]]); + DBA::delete('term', ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'type' => [self::FILE, self::CATEGORY]]); if ($message["deleted"]) { return; @@ -216,9 +279,9 @@ class Term foreach ($files[1] as $file) { DBA::insert('term', [ 'uid' => $message["uid"], - 'oid' => $itemid, - 'otype' => TERM_OBJ_POST, - 'type' => TERM_FILE, + 'oid' => $item_id, + 'otype' => self::OBJECT_TYPE_POST, + 'type' => self::FILE, 'term' => $file ]); } @@ -228,9 +291,9 @@ class Term foreach ($files[1] as $file) { DBA::insert('term', [ 'uid' => $message["uid"], - 'oid' => $itemid, - 'otype' => TERM_OBJ_POST, - 'type' => TERM_CATEGORY, + 'oid' => $item_id, + 'otype' => self::OBJECT_TYPE_POST, + 'type' => self::CATEGORY, 'term' => $file ]); } @@ -252,6 +315,7 @@ class Term 'tags' => [], 'hashtags' => [], 'mentions' => [], + 'implicit_mentions' => [], ]; $searchpath = System::baseUrl() . "/search?tag="; @@ -259,10 +323,9 @@ class Term $taglist = DBA::select( 'term', ['type', 'term', 'url'], - ["`otype` = ? AND `oid` = ? AND `type` IN (?, ?)", TERM_OBJ_POST, $item['id'], TERM_HASHTAG, TERM_MENTION], + ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item['id'], 'type' => [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION]], ['order' => ['tid']] ); - while ($tag = DBA::fetch($taglist)) { if ($tag['url'] == '') { $tag['url'] = $searchpath . rawurlencode($tag['term']); @@ -270,25 +333,25 @@ class Term $orig_tag = $tag['url']; - $author = ['uid' => 0, 'id' => $item['author-id'], - 'network' => $item['author-network'], 'url' => $item['author-link']]; + $prefix = self::TAG_CHARACTER[$tag['type']]; + switch($tag['type']) { + case self::HASHTAG: + if ($orig_tag != $tag['url']) { + $item['body'] = str_replace($orig_tag, $tag['url'], $item['body']); + } - $prefix = ''; - if ($tag['type'] == TERM_HASHTAG) { - $tag['url'] = Contact::magicLinkByContact($author, $tag['url']); - if ($orig_tag != $tag['url']) { - $item['body'] = str_replace($orig_tag, $tag['url'], $item['body']); - } - - $return['hashtags'][] = '#' . $tag['term'] . ''; - $prefix = '#'; - } elseif ($tag['type'] == TERM_MENTION) { - $tag['url'] = Contact::magicLink($tag['url']); - $return['mentions'][] = '@' . $tag['term'] . ''; - $prefix = '@'; + $return['hashtags'][] = $prefix . '' . $tag['term'] . ''; + $return['tags'][] = $prefix . '' . $tag['term'] . ''; + break; + case self::MENTION: + $tag['url'] = Contact::magicLink($tag['url']); + $return['mentions'][] = $prefix . '' . $tag['term'] . ''; + $return['tags'][] = $prefix . '' . $tag['term'] . ''; + break; + case self::IMPLICIT_MENTION: + $return['implicit_mentions'][] = $prefix . $tag['term']; + break; } - - $return['tags'][] = $prefix . '' . $tag['term'] . ''; } DBA::close($taglist); @@ -296,20 +359,38 @@ class Term } /** - * Delete all tags from an item + * Delete tags of the specific type(s) from an item * - * @param int itemid - choose from which item the tags will be removed - * @param array $type + * @param int $item_id + * @param int|array $type * @throws \Exception */ - public static function deleteByItemId($itemid, $type = [TERM_HASHTAG, TERM_MENTION]) + public static function deleteByItemId($item_id, $type = [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION]) { - if (empty($itemid)) { + if (empty($item_id)) { return; } // Clean up all tags - DBA::delete('term', ['otype' => TERM_OBJ_POST, 'oid' => $itemid, 'type' => $type]); + DBA::delete('term', ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'type' => $type]); + } + /** + * Check if the provided tag is of one of the provided term types. + * + * @param string $tag + * @param int ...$types + * @return bool + */ + public static function isType($tag, ...$types) + { + $tag_chars = []; + foreach ($types as $type) { + if (isset(self::TAG_CHARACTER[$type])) { + $tag_chars[] = self::TAG_CHARACTER[$type]; + } + } + + return Strings::startsWith($tag, $tag_chars); } } diff --git a/src/Util/Strings.php b/src/Util/Strings.php index 0c63749c8..55751d8d8 100644 --- a/src/Util/Strings.php +++ b/src/Util/Strings.php @@ -331,4 +331,19 @@ class Strings return $uri; } + + + /** + * Check if the trimmed provided string is starting with one of the provided characters + * + * @param string $string + * @param array $chars + * @return bool + */ + public static function startsWith($string, array $chars) + { + $return = in_array(substr(trim($string), 0, 1), $chars); + + return $return; + } }