From fed0895d98a595609df7c821123ec9d6a22fbbc9 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 28 Oct 2015 00:11:54 +0000 Subject: [PATCH] Move the functionality to a plugin Use an associated model to prevent race conditions on creating the profile object. --- classes/Profile.php | 42 ------- lib/util.php | 103 +++------------ plugins/MentionURL/MentionURLPlugin.php | 70 +++++++++++ .../classes/Mention_url_profile.php | 118 ++++++++++++++++++ plugins/MentionURL/lib/util.php | 57 +++++++++ 5 files changed, 264 insertions(+), 126 deletions(-) create mode 100644 plugins/MentionURL/MentionURLPlugin.php create mode 100644 plugins/MentionURL/classes/Mention_url_profile.php create mode 100644 plugins/MentionURL/lib/util.php diff --git a/classes/Profile.php b/classes/Profile.php index f3252f2f60..5ef77a9506 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -1520,48 +1520,6 @@ class Profile extends Managed_DataObject return $profile; } - /* - * Get or create a profile given a possible URL - */ - static function ensureFromUrl($url, $mf2=null) { - common_debug('Trying to find a profile for ' . $url); - - $url = preg_replace('#https?://#', 'https://', $url); - try { - $profile = Profile::fromUri($url); - } catch(UnknownUriException $ex) {} - - if(!($profile instanceof Profile)) { - $profile = Profile::getKV('profileurl', $url); - } - - $url = str_replace('https://', 'http://', $url); - if(!($profile instanceof Profile)) { - try { - $profile = Profile::fromUri($url); - } catch(UnknownUriException $ex) {} - } - - if(!($profile instanceof Profile)) { - $profile = Profile::getKV('profileurl', $url); - } - - if(!($profile instanceof Profile)) { - $hcard = common_representative_hcard($url, null, $mf2); - if(!$hcard) return null; - - $profile = new Profile(); - $profile->profileurl = $hcard['url'][0]; - $profile->fullname = $hcard['name'][0]; - preg_match_all('/'.Nickname::DISPLAY_FMT.'/', $profile->fullname, $altnick); - $profile->nickname = $hcard['nickname'] ? $hcard['nickname'][0] : implode($altnick[0]); - $profile->created = common_sql_now(); - $profile->insert(); - } - - return $profile; - } - function canRead(Notice $notice) { if ($notice->scope & Notice::SITE_SCOPE) { diff --git a/lib/util.php b/lib/util.php index b842bbaa93..f31812349b 100644 --- a/lib/util.php +++ b/lib/util.php @@ -730,33 +730,24 @@ function common_find_mentions($text, Notice $notice) $matches = common_find_mentions_raw($text); foreach ($matches as $match) { - // Try to process it as @URL - $url = $match[0]; - if(!common_valid_http_url($url)) { $url = 'http://' . $url; } - if(common_valid_http_url($url)) { - $mentioned = Profile::ensureFromUrl($url); - $text = mb_strlen($mentioned->nickname) <= mb_strlen($match[0]) ? $mentioned->nickname : $match[0]; + try { + $nickname = Nickname::normalize($match[0]); + } catch (NicknameException $e) { + // Bogus match? Drop it. + continue; + } + + // Try to get a profile for this nickname. + // Start with conversation context, then go to + // sender context. + + if ($origAuthor instanceof Profile && $origAuthor->nickname == $nickname) { + $mentioned = $origAuthor; + } else if (!empty($origMentions) && + array_key_exists($nickname, $origMentions)) { + $mentioned = $origMentions[$nickname]; } else { - try { - $nickname = Nickname::normalize($match[0]); - } catch (NicknameException $e) { - // Bogus match? Drop it. - continue; - } - - // Try to get a profile for this nickname. - // Start with conversation context, then go to - // sender context. - - if ($origAuthor instanceof Profile && $origAuthor->nickname == $nickname) { - $mentioned = $origAuthor; - } else if (!empty($origMentions) && - array_key_exists($nickname, $origMentions)) { - $mentioned = $origMentions[$nickname]; - } else { - $mentioned = common_relative_profile($sender, $nickname); - } - $text = $match[0]; + $mentioned = common_relative_profile($sender, $nickname); } if ($mentioned instanceof Profile) { @@ -770,7 +761,7 @@ function common_find_mentions($text, Notice $notice) $mention = array('mentioned' => array($mentioned), 'type' => 'mention', - 'text' => $text, + 'text' => $match[0], 'position' => $match[1], 'length' => mb_strlen($match[0]), 'url' => $url); @@ -848,7 +839,7 @@ function common_find_mentions_raw($text) PREG_OFFSET_CAPTURE); $atmatches = array(); - preg_match_all('/(?:^|\s+)@((?:[A-Za-z0-9_:\-\.\/%]+)|(?:' . Nickname::DISPLAY_FMT . '))\b/', + preg_match_all('/(?:^|\s+)@(' . Nickname::DISPLAY_FMT . ')\b/', $text, $atmatches, PREG_OFFSET_CAPTURE); @@ -2440,62 +2431,6 @@ function common_strip_html($html, $trim=true, $save_whitespace=false) return $trim ? trim($text) : $text; } -function common_representative_hcard($url, $fn=null, $mf2=null) { - if(!$mf2) { - $request = HTTPClient::start(); - - try { - $response = $request->get($url); - } catch(Exception $ex) { - return null; - } - - $url = $response->getEffectiveUrl(); - $mf2 = new Mf2\Parser($response->getBody(), $url); - $mf2 = $mf2->parse(); - } - - $hcard = null; - - if(!empty($mf2['items'])) { - $hcards = array(); - foreach($mf2['items'] as $item) { - if(!in_array('h-card', $item['type'])) { - continue; - } - - // We found a match, return it immediately - if(isset($item['properties']['url']) && in_array($url, $item['properties']['url'])) { - $hcard = $item['properties']; - break; - } - - // Let's keep all the hcards for later, to return one of them at least - $hcards[] = $item['properties']; - } - - // No match immediately for the url we expected, but there were h-cards found - if (count($hcards) > 0) { - $hcard = $hcards[0]; - } - } - - if(!$hcard && $fn) { - $hcard = array('name' => array($fn)); - } - - if(!$hcard && $response) { - preg_match('/([^<]+)/', $response->getBody(), $match); - $hcard = array('name' => array($match[1])); - } - - if($hcard && !$hcard['url']) { - $hcard['url'] = array($url); - } - - return $hcard; -} - function html_sprintf() { $args = func_get_args(); diff --git a/plugins/MentionURL/MentionURLPlugin.php b/plugins/MentionURL/MentionURLPlugin.php new file mode 100644 index 0000000000..86135d8827 --- /dev/null +++ b/plugins/MentionURL/MentionURLPlugin.php @@ -0,0 +1,70 @@ +<?php + +if (!defined('GNUSOCIAL')) { exit(1); } + +require_once __DIR__ . '/lib/util.php'; + +/* + * This plugin lets you type @twitter.com/singpolyma + * so that you can be specific instead of relying on heuristics. + */ +class MentionURLPlugin extends Plugin +{ + public function onStartFindMentions($sender, $text, &$mentions) + { + preg_match_all('/(?:^|\s+)@([A-Za-z0-9_:\-\.\/%]+)\b/', + $text, + $atmatches, + PREG_OFFSET_CAPTURE); + + foreach ($atmatches[1] as $match) { + $url = $match[0]; + if(!common_valid_http_url($url)) { $url = 'http://' . $url; } + if(common_valid_http_url($url)) { + $mentioned = Mention_url_profile::fromUrl($url); + $text = mb_strlen($mentioned->nickname) <= mb_strlen($match[0]) ? $mentioned->nickname : $match[0]; + } + + if($mentioned instanceof Profile) { + $mentions[] = array('mentioned' => array($mentioned), + 'type' => 'mention', + 'text' => $text, + 'position' => $match[1], + 'length' => mb_strlen($match[0]), + 'url' => $mentioned->profileurl); + } + } + + return true; + } + + public function onStartGetProfileFromURI($uri, &$profile) + { + $mention_profile = Mention_url_profile::getKV('profileurl', $uri); + if($mention_profile instanceof Mention_url_profile) { + $profile = $mention_profile->getProfile(); + return !($profile instanceof Profile); + } + + return true; + } + + public function onCheckSchema() + { + $schema = Schema::get(); + $schema->ensureTable('mention_url_profile', Mention_url_profile::schemaDef()); + return true; + } + + public function onPluginVersion(array &$versions) + { + $versions[] = array('name' => 'MentionURL', + 'version' => GNUSOCIAL_VERSION, + 'author' => 'Stephen Paul Weber', + 'homepage' => 'http://gnu.io/', + 'description' => + // TRANS: Plugin description. + _m('Plugin to allow mentioning arbitrary URLs.')); + return true; + } +} diff --git a/plugins/MentionURL/classes/Mention_url_profile.php b/plugins/MentionURL/classes/Mention_url_profile.php new file mode 100644 index 0000000000..7f247c1966 --- /dev/null +++ b/plugins/MentionURL/classes/Mention_url_profile.php @@ -0,0 +1,118 @@ +<?php +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +/** + * Table Definition for mention_url_profile + */ + +class Mention_url_profile extends Managed_DataObject +{ + public $__table = 'mention_url_profile'; // table name + public $profile_id; // int(4) not_null + public $profileurl; // varchar(191) primary_key not_null not 255 because utf8mb4 takes more space + + public static function schemaDef() + { + return array( + 'fields' => array( + 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'matches exactly one profile id'), + 'profileurl' => array('type' => 'varchar', 'not null' => true, 'length' => 191, 'description' => 'URL of the profile'), + ), + 'primary key' => array('profileurl'), + 'foreign keys' => array( + 'mention_url_profile_profile_id_fkey' => array('profile', array('profile_id' => 'id')), + ), + ); + } + + public static function fromUrl($url, $depth=0) { + common_debug('MentionURL: trying to find a profile for ' . $url); + + $url = preg_replace('#https?://#', 'https://', $url); + try { + $profile = Profile::fromUri($url); + } catch(UnknownUriException $ex) {} + + if(!($profile instanceof Profile)) { + $profile = self::findProfileByProfileURL($url); + } + + $url = str_replace('https://', 'http://', $url); + if(!($profile instanceof Profile)) { + try { + $profile = Profile::fromUri($url); + } catch(UnknownUriException $ex) {} + } + + if(!($profile instanceof Profile)) { + $profile = self::findProfileByProfileURL($url); + } + + if(!($profile instanceof Profile)) { + $hcard = mention_url_representative_hcard($url); + if(!$hcard) return null; + + $mention_profile = new Mention_url_profile(); + $mention_profile->query('BEGIN'); + + $profile = new Profile(); + $profile->profileurl = $hcard['url'][0]; + $profile->fullname = $hcard['name'][0]; + preg_match('/\/([^\/]+)\/*$/', $profile->profileurl, $matches); + if(!$hcard['nickname']) $hcard['nickname'] = array($matches[1]); + $profile->nickname = $hcard['nickname'][0]; + $profile->created = common_sql_now(); + + $mention_profile->profile_id = $profile->insert(); + if(!$mention_profile->profile_id) { + $mention_profile->query('ROLLBACK'); + return null; + } + + $mention_profile->profileurl = $profile->profileurl; + if(!$mention_profile->insert()) { + $mention_profile->query('ROLLBACK'); + if($depth > 0) { + return null; + } else { + return self::fromUrl($url, $depth+1); + } + } else { + $mention_profile->query('COMMIT'); + } + } + + return $profile; + } + + protected static function findProfileByProfileURL($url) { + $profile = Profile::getKV('profileurl', $url); + if($profile instanceof Profile) { + $mention_profile = new Mention_url_profile(); + $mention_profile->profile_id = $profile->id; + $mention_profile->profileurl = $profile->profileurl; + $mention_profile->insert(); + } + + return $profile; + } + + public function getProfile() { + return Profile::getKV('id', $this->profile_id); + } +} diff --git a/plugins/MentionURL/lib/util.php b/plugins/MentionURL/lib/util.php new file mode 100644 index 0000000000..b2667806d1 --- /dev/null +++ b/plugins/MentionURL/lib/util.php @@ -0,0 +1,57 @@ +<?php + +function mention_url_representative_hcard($url, $fn=null, $mf2=null) { + if(!$mf2) { + $request = HTTPClient::start(); + + try { + $response = $request->get($url); + } catch(Exception $ex) { + return null; + } + + $url = $response->getEffectiveUrl(); + $mf2 = new Mf2\Parser($response->getBody(), $url); + $mf2 = $mf2->parse(); + } + + $hcard = null; + + if(!empty($mf2['items'])) { + $hcards = array(); + foreach($mf2['items'] as $item) { + if(!in_array('h-card', $item['type'])) { + continue; + } + + // We found a match, return it immediately + if(isset($item['properties']['url']) && in_array($url, $item['properties']['url'])) { + $hcard = $item['properties']; + break; + } + + // Let's keep all the hcards for later, to return one of them at least + $hcards[] = $item['properties']; + } + + // No match immediately for the url we expected, but there were h-cards found + if (count($hcards) > 0) { + $hcard = $hcards[0]; + } + } + + if(!$hcard && $fn) { + $hcard = array('name' => array($fn)); + } + + if(!$hcard && $response) { + preg_match('/<title>([^<]+)/', $response->getBody(), $match); + $hcard = array('name' => array($match[1])); + } + + if($hcard && !$hcard['url']) { + $hcard['url'] = array($url); + } + + return $hcard; +}