From 89f7ee2cc58e48e3473a949c2d95e2dad1075171 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 17 Jul 2021 04:57:21 +0000 Subject: [PATCH 01/10] Prevent endless loop when updating contact by probe --- src/Model/Contact.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 12cba0c080..6cda3e72ad 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -2117,7 +2117,7 @@ class Contact } if (Strings::normaliseLink($ret['url']) != Strings::normaliseLink($contact['url'])) { - $cid = self::getIdForURL($ret['url']); + $cid = self::getIdForURL($ret['url'], 0, false); if (!empty($cid) && ($cid != $id)) { Logger::notice('URL of contact changed.', ['id' => $id, 'new_id' => $cid, 'old' => $contact['url'], 'new' => $ret['url']]); return self::updateFromProbeArray($cid, $ret); From 596bb9fa7c0673fdc1919fac78c35ff63c2fcc2e Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 17 Jul 2021 05:25:04 +0000 Subject: [PATCH 02/10] Fixed wrong parameter --- src/Model/Contact.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 6cda3e72ad..fe6fe55f32 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -1808,7 +1808,7 @@ class Contact // User contacts use are updated through the public contacts if (($uid != 0) && !in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) { - $pcid = self::getIdForURL($contact['url'], false); + $pcid = self::getIdForURL($contact['url'], 0, false); if (!empty($pcid)) { Logger::debug('Update the private contact via the public contact', ['id' => $cid, 'uid' => $uid, 'public' => $pcid]); self::updateAvatar($pcid, $avatar, $force, true); From d8bf9c4601988216eb95d7a24288a440434454b0 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 18 Jul 2021 16:42:55 +0000 Subject: [PATCH 03/10] Prevent loop also when fetching the outbox --- src/Model/APContact.php | 23 +++++++++++++---------- src/Protocol/ActivityPub/Transmitter.php | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Model/APContact.php b/src/Model/APContact.php index 1ae34a40a8..e9500f85b8 100644 --- a/src/Model/APContact.php +++ b/src/Model/APContact.php @@ -148,6 +148,19 @@ class APContact $url = $apcontact['url']; } + // Detect multiple fast repeating request to the same address + // See https://github.com/friendica/friendica/issues/9303 + $cachekey = 'apcontact:getByURL:' . $url; + $result = DI::cache()->get($cachekey); + if (!is_null($result)) { + Logger::notice('Multiple requests for the address', ['url' => $url, 'update' => $update, 'callstack' => System::callstack(20), 'result' => $result]); + if (!empty($fetched_contact)) { + return $fetched_contact; + } + } else { + DI::cache()->set($cachekey, System::callstack(20), Duration::FIVE_MINUTES); + } + $curlResult = HTTPSignature::fetchRaw($url); $failed = empty($curlResult) || empty($curlResult->getBody()) || (!$curlResult->isSuccess() && ($curlResult->getReturnCode() != 410)); @@ -171,16 +184,6 @@ class APContact return $fetched_contact; } - // Detect multiple fast repeating request to the same address - // See https://github.com/friendica/friendica/issues/9303 - $cachekey = 'apcontact:getByURL:' . $url; - $result = DI::cache()->get($cachekey); - if (!is_null($result)) { - Logger::notice('Multiple requests for the address', ['url' => $url, 'update' => $update, 'callstack' => System::callstack(20), 'result' => $result]); - } else { - DI::cache()->set($cachekey, System::callstack(20), Duration::FIVE_MINUTES); - } - $apcontact['url'] = $compacted['@id']; $apcontact['uuid'] = JsonLD::fetchElement($compacted, 'diaspora:guid', '@value'); $apcontact['type'] = str_replace('as:', '', JsonLD::fetchElement($compacted, '@type')); diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index a65f855422..c19dcf0021 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -235,7 +235,7 @@ class Transmitter */ public static function getOutbox($owner, $page = null, $requester = '') { - $public_contact = Contact::getIdForURL($owner['url']); + $public_contact = Contact::getIdForURL($owner['url'], 0, false); $condition = ['uid' => 0, 'contact-id' => $public_contact, 'private' => [Item::PUBLIC, Item::UNLISTED]]; From 424a85bb9424d5b826734147add52f4d196f13c9 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 18 Jul 2021 18:54:25 +0000 Subject: [PATCH 04/10] Fetch local data without HTTP requests --- src/Model/APContact.php | 55 +++++++++++++++++++++++++----------- src/Protocol/ActivityPub.php | 2 -- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/Model/APContact.php b/src/Model/APContact.php index e9500f85b8..a4c97f6232 100644 --- a/src/Model/APContact.php +++ b/src/Model/APContact.php @@ -31,11 +31,13 @@ use Friendica\DI; use Friendica\Network\Probe; use Friendica\Protocol\ActivityNamespace; use Friendica\Protocol\ActivityPub; +use Friendica\Protocol\ActivityPub\Transmitter; use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; use Friendica\Util\HTTPSignature; use Friendica\Util\JsonLD; use Friendica\Util\Network; +use Friendica\Util\Strings; class APContact { @@ -161,22 +163,31 @@ class APContact DI::cache()->set($cachekey, System::callstack(20), Duration::FIVE_MINUTES); } - $curlResult = HTTPSignature::fetchRaw($url); - $failed = empty($curlResult) || empty($curlResult->getBody()) || - (!$curlResult->isSuccess() && ($curlResult->getReturnCode() != 410)); - - if (!$failed) { - $data = json_decode($curlResult->getBody(), true); - $failed = empty($data) || !is_array($data); + if (Network::isLocalLink($url) && ($local_uid = User::getIdForURL($url))) { + $data = Transmitter::getProfile($local_uid); + $local_owner = User::getOwnerDataById($local_uid); } - if (!$failed && ($curlResult->getReturnCode() == 410)) { - $data = ['@context' => ActivityPub::CONTEXT, 'id' => $url, 'type' => 'Tombstone']; - } + if (empty($data)) { + $local_owner = []; - if ($failed) { - self::markForArchival($fetched_contact ?: []); - return $fetched_contact; + $curlResult = HTTPSignature::fetchRaw($url); + $failed = empty($curlResult) || empty($curlResult->getBody()) || + (!$curlResult->isSuccess() && ($curlResult->getReturnCode() != 410)); + + if (!$failed) { + $data = json_decode($curlResult->getBody(), true); + $failed = empty($data) || !is_array($data); + } + + if (!$failed && ($curlResult->getReturnCode() == 410)) { + $data = ['@context' => ActivityPub::CONTEXT, 'id' => $url, 'type' => 'Tombstone']; + } + + if ($failed) { + self::markForArchival($fetched_contact ?: []); + return $fetched_contact; + } } $compacted = JsonLD::compact($data); @@ -267,7 +278,11 @@ class APContact } if (!empty($apcontact['following'])) { - $following = ActivityPub::fetchContent($apcontact['following']); + if (!empty($local_owner)) { + $following = ActivityPub\Transmitter::getContacts($local_owner, [Contact::SHARING, Contact::FRIEND], 'following'); + } else { + $following = ActivityPub::fetchContent($apcontact['following']); + } if (!empty($following['totalItems'])) { // Mastodon seriously allows for this condition? // Jul 14 2021 - See https://mastodon.social/@BLUW for a negative following count @@ -279,7 +294,11 @@ class APContact } if (!empty($apcontact['followers'])) { - $followers = ActivityPub::fetchContent($apcontact['followers']); + if (!empty($local_owner)) { + $followers = ActivityPub\Transmitter::getContacts($local_owner, [Contact::FOLLOWER, Contact::FRIEND], 'followers'); + } else { + $followers = ActivityPub::fetchContent($apcontact['followers']); + } if (!empty($followers['totalItems'])) { // Mastodon seriously allows for this condition? // Jul 14 2021 - See https://mastodon.online/@goes11 for a negative followers count @@ -291,7 +310,11 @@ class APContact } if (!empty($apcontact['outbox'])) { - $outbox = ActivityPub::fetchContent($apcontact['outbox']); + if (!empty($local_owner)) { + $outbox = ActivityPub\Transmitter::getOutbox($local_owner); + } else { + $outbox = ActivityPub::fetchContent($apcontact['outbox']); + } if (!empty($outbox['totalItems'])) { $apcontact['statuses_count'] = $outbox['totalItems']; } diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 134f69f4a6..0f62ab5376 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -22,8 +22,6 @@ namespace Friendica\Protocol; use Friendica\Core\Protocol; -use Friendica\Database\DBA; -use Friendica\DI; use Friendica\Model\APContact; use Friendica\Model\User; use Friendica\Util\HTTPSignature; From aa6313dee6b46353dc5d6a6b0b74449842357daf Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 19 Jul 2021 04:15:57 +0000 Subject: [PATCH 05/10] Improved detection for a local contact --- src/Model/APContact.php | 18 ++++++++++++++++-- src/Model/Contact.php | 7 +++++++ src/Network/Probe.php | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Model/APContact.php b/src/Model/APContact.php index a4c97f6232..7bdde60c65 100644 --- a/src/Model/APContact.php +++ b/src/Model/APContact.php @@ -54,6 +54,20 @@ class APContact return []; } + if (Contact::isLocal($addr) && ($local_uid = User::getIdForURL($addr)) && ($local_owner = User::getOwnerDataById($local_uid))) { + $data = [ + 'addr' => $local_owner['addr'], + 'baseurl' => $local_owner['baseurl'], + 'url' => $local_owner['url'], + 'subscribe' => $local_owner['baseurl'] . '/follow?url={uri}']; + + if (!empty($local_owner['alias']) && ($local_owner['url'] != $local_owner['alias'])) { + $data['alias'] = $local_owner['alias']; + } + + return $data; + } + $data = ['addr' => $addr]; $template = 'https://' . $addr_parts[1] . '/.well-known/webfinger?resource=acct:' . urlencode($addr); $webfinger = Probe::webfinger(str_replace('{uri}', urlencode($addr), $template), 'application/jrd+json'); @@ -284,7 +298,7 @@ class APContact $following = ActivityPub::fetchContent($apcontact['following']); } if (!empty($following['totalItems'])) { - // Mastodon seriously allows for this condition? + // Mastodon seriously allows for this condition? // Jul 14 2021 - See https://mastodon.social/@BLUW for a negative following count if ($following['totalItems'] < 0) { $following['totalItems'] = 0; @@ -300,7 +314,7 @@ class APContact $followers = ActivityPub::fetchContent($apcontact['followers']); } if (!empty($followers['totalItems'])) { - // Mastodon seriously allows for this condition? + // Mastodon seriously allows for this condition? // Jul 14 2021 - See https://mastodon.online/@goes11 for a negative followers count if ($followers['totalItems'] < 0) { $followers['totalItems'] = 0; diff --git a/src/Model/Contact.php b/src/Model/Contact.php index fe6fe55f32..c68039643f 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -453,6 +453,13 @@ class Contact */ public static function isLocal($url) { + if (!parse_url($url, PHP_URL_SCHEME)) { + $addr_parts = explode('@', $url); + if (count($addr_parts) == 2) { + return $addr_parts[1] == DI::baseUrl()->getHostname(); + } + } + return Strings::compareLink(self::getBasepath($url, true), DI::baseUrl()); } diff --git a/src/Network/Probe.php b/src/Network/Probe.php index e44a8d326e..12a5b26925 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -333,7 +333,7 @@ class Probe public static function uri($uri, $network = '', $uid = -1) { // Local profiles aren't probed via network - if (empty($network) && strpos($uri, DI::baseUrl()->getHostname())) { + if (empty($network) && Contact::isLocal($uri)) { $data = self::localProbe($uri); if (!empty($data)) { return $data; From 01abea7c258ba45b348b93c89aabef5e653c21f4 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 19 Jul 2021 04:49:58 +0000 Subject: [PATCH 06/10] Don't probe non existing local contacts --- src/Network/Probe.php | 54 +++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/src/Network/Probe.php b/src/Network/Probe.php index 12a5b26925..9ee338e8fd 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -2201,39 +2201,33 @@ class Probe */ private static function localProbe(string $url) { - $uid = User::getIdForURL($url); - if (empty($uid)) { - return []; - } + if ($uid = User::getIdForURL($url)) { + $profile = User::getOwnerDataById($uid); + $approfile = ActivityPub\Transmitter::getProfile($uid); - $profile = User::getOwnerDataById($uid); - if (empty($profile)) { - return []; - } + if (empty($profile['gsid'])) { + $profile['gsid'] = GServer::getID($approfile['generator']['url']); + } - $approfile = ActivityPub\Transmitter::getProfile($uid); - if (empty($approfile)) { - return []; + $data = ['name' => $profile['name'], 'nick' => $profile['nick'], 'guid' => $approfile['diaspora:guid'] ?? '', + 'url' => $profile['url'], 'addr' => $profile['addr'], 'alias' => $profile['alias'], + 'photo' => Contact::getAvatarUrlForId($profile['id'], $profile['updated']), + 'header' => $profile['header'] ? Contact::getHeaderUrlForId($profile['id'], $profile['updated']) : '', + 'account-type' => $profile['contact-type'], 'community' => ($profile['contact-type'] == User::ACCOUNT_TYPE_COMMUNITY), + 'keywords' => $profile['keywords'], 'location' => $profile['location'], 'about' => $profile['about'], + 'hide' => !$profile['net-publish'], 'batch' => '', 'notify' => $profile['notify'], + 'poll' => $profile['poll'], 'request' => $profile['request'], 'confirm' => $profile['confirm'], + 'subscribe' => $approfile['generator']['url'] . '/follow?url={uri}', 'poco' => $profile['poco'], + 'following' => $approfile['following'], 'followers' => $approfile['followers'], + 'inbox' => $approfile['inbox'], 'outbox' => $approfile['outbox'], + 'sharedinbox' => $approfile['endpoints']['sharedInbox'], 'network' => Protocol::DFRN, + 'pubkey' => $profile['upubkey'], 'baseurl' => $approfile['generator']['url'], 'gsid' => $profile['gsid'], + 'manually-approve' => in_array($profile['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP])]; + } else { + // Default values for non existing targets + $data = ['name' => $url, 'nick' => $url, 'url' => $url, 'network' => Protocol::PHANTOM, + 'photo' => DI::baseUrl() . Contact::DEFAULT_AVATAR_PHOTO]; } - - if (empty($profile['gsid'])) { - $profile['gsid'] = GServer::getID($approfile['generator']['url']); - } - - $data = ['name' => $profile['name'], 'nick' => $profile['nick'], 'guid' => $approfile['diaspora:guid'] ?? '', - 'url' => $profile['url'], 'addr' => $profile['addr'], 'alias' => $profile['alias'], - 'photo' => Contact::getAvatarUrlForId($profile['id'], $profile['updated']), - 'header' => $profile['header'] ? Contact::getHeaderUrlForId($profile['id'], $profile['updated']) : '', - 'account-type' => $profile['contact-type'], 'community' => ($profile['contact-type'] == User::ACCOUNT_TYPE_COMMUNITY), - 'keywords' => $profile['keywords'], 'location' => $profile['location'], 'about' => $profile['about'], - 'hide' => !$profile['net-publish'], 'batch' => '', 'notify' => $profile['notify'], - 'poll' => $profile['poll'], 'request' => $profile['request'], 'confirm' => $profile['confirm'], - 'subscribe' => $approfile['generator']['url'] . '/follow?url={uri}', 'poco' => $profile['poco'], - 'following' => $approfile['following'], 'followers' => $approfile['followers'], - 'inbox' => $approfile['inbox'], 'outbox' => $approfile['outbox'], - 'sharedinbox' => $approfile['endpoints']['sharedInbox'], 'network' => Protocol::DFRN, - 'pubkey' => $profile['upubkey'], 'baseurl' => $approfile['generator']['url'], 'gsid' => $profile['gsid'], - 'manually-approve' => in_array($profile['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP])]; return self::rearrangeData($data); } } From 2647514603852fe5fb9f47f0bf153dd20c124ce6 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 19 Jul 2021 06:14:14 +0000 Subject: [PATCH 07/10] Detection of local requests --- src/Model/Photo.php | 24 +++++++++++++----------- src/Network/HTTPRequest.php | 12 ++++++++++++ src/Util/Images.php | 13 +++++++++++-- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/Model/Photo.php b/src/Model/Photo.php index 61fc2df4ea..30e6668987 100644 --- a/src/Model/Photo.php +++ b/src/Model/Photo.php @@ -804,30 +804,33 @@ class Photo } /** - * Returns the GUID from picture links + * Fetch the guid and scale from picture links * * @param string $name Picture link - * @return string GUID - * @throws \Exception + * @return array */ - public static function getGUID($name) + public static function getResourceData(string $name):array { $base = DI::baseUrl()->get(); $guid = str_replace([Strings::normaliseLink($base), '/photo/'], '', Strings::normaliseLink($name)); + if (parse_url($guid, PHP_URL_SCHEME)) { + return []; + } + $guid = self::stripExtension($guid); if (substr($guid, -2, 1) != "-") { - return ''; + return []; } $scale = intval(substr($guid, -1, 1)); if (!is_numeric($scale)) { - return ''; + return []; } $guid = substr($guid, 0, -2); - return $guid; + return ['guid' => $guid, 'scale' => $scale]; } /** @@ -839,13 +842,12 @@ class Photo */ public static function isLocal($name) { - $guid = self::getGUID($name); - - if (empty($guid)) { + $data = self::getResourceData($name); + if (empty($data)) { return false; } - return DBA::exists('photo', ['resource-id' => $guid]); + return DBA::exists('photo', ['resource-id' => $data['guid'], 'scale' => $data['scale']]); } /** diff --git a/src/Network/HTTPRequest.php b/src/Network/HTTPRequest.php index bd31ac1e14..622828b434 100644 --- a/src/Network/HTTPRequest.php +++ b/src/Network/HTTPRequest.php @@ -74,6 +74,10 @@ class HTTPRequest implements IHTTPRequest { $stamp1 = microtime(true); + if (Network::isLocalLink($url)) { + $this->logger->info('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); + } + if (strlen($url) > 1000) { $this->logger->debug('URL is longer than 1000 characters.', ['url' => $url, 'callstack' => System::callstack(20)]); $this->profiler->saveTimestamp($stamp1, 'network'); @@ -226,6 +230,10 @@ class HTTPRequest implements IHTTPRequest { $stamp1 = microtime(true); + if (Network::isLocalLink($url)) { + $this->logger->info('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); + } + if (Network::isUrlBlocked($url)) { $this->logger->info('Domain is blocked.' . ['url' => $url]); $this->profiler->saveTimestamp($stamp1, 'network'); @@ -328,6 +336,10 @@ class HTTPRequest implements IHTTPRequest */ public function finalUrl(string $url, int $depth = 1, bool $fetchbody = false) { + if (Network::isLocalLink($url)) { + $this->logger->info('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); + } + if (Network::isUrlBlocked($url)) { $this->logger->info('Domain is blocked.', ['url' => $url]); return $url; diff --git a/src/Util/Images.php b/src/Util/Images.php index 7b11ea3f6b..3b07aee2fc 100644 --- a/src/Util/Images.php +++ b/src/Util/Images.php @@ -22,8 +22,8 @@ namespace Friendica\Util; use Friendica\Core\Logger; -use Friendica\Core\System; use Friendica\DI; +use Friendica\Model\Photo; /** * Image utilities @@ -184,7 +184,16 @@ class Images return $data; } - $img_str = DI::httpRequest()->fetch($url, 4); + if (Network::isLocalLink($url) && ($data = Photo::getResourceData($url))) { + $photo = Photo::getPhoto($data['guid'], $data['scale']); + if (!empty($photo)) { + $img_str = Photo::getImageDataForPhoto($photo); + } + } + + if (empty($img_str)) { + $img_str = DI::httpRequest()->fetch($url, 4); + } if (!$img_str) { return []; From fa00a4ee32a693459c0d000576afada7523e79d1 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 19 Jul 2021 06:19:13 +0000 Subject: [PATCH 08/10] Simplyfied picture fetching --- src/Util/Images.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Util/Images.php b/src/Util/Images.php index 3b07aee2fc..dd4848606b 100644 --- a/src/Util/Images.php +++ b/src/Util/Images.php @@ -185,7 +185,7 @@ class Images } if (Network::isLocalLink($url) && ($data = Photo::getResourceData($url))) { - $photo = Photo::getPhoto($data['guid'], $data['scale']); + $photo = Photo::selectFirst([], ['resource-id' => $data['guid'], 'scale' => $data['scale']]); if (!empty($photo)) { $img_str = Photo::getImageDataForPhoto($photo); } From f5a7b0141b6ea9199844d2b021bace30130df578 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 19 Jul 2021 06:55:23 +0000 Subject: [PATCH 09/10] Added todo --- src/Util/Images.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Util/Images.php b/src/Util/Images.php index dd4848606b..bf84ee6c22 100644 --- a/src/Util/Images.php +++ b/src/Util/Images.php @@ -189,6 +189,7 @@ class Images if (!empty($photo)) { $img_str = Photo::getImageDataForPhoto($photo); } + // @todo Possibly add a check for locally stored files } if (empty($img_str)) { From 64026ed979efa38a3f092e1546fbbfd953cf66c0 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 19 Jul 2021 12:07:02 +0000 Subject: [PATCH 10/10] Simplify code --- src/Model/Contact.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Model/Contact.php b/src/Model/Contact.php index c68039643f..20135a49e5 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -455,9 +455,7 @@ class Contact { if (!parse_url($url, PHP_URL_SCHEME)) { $addr_parts = explode('@', $url); - if (count($addr_parts) == 2) { - return $addr_parts[1] == DI::baseUrl()->getHostname(); - } + return (count($addr_parts) == 2) && ($addr_parts[1] == DI::baseUrl()->getHostname()); } return Strings::compareLink(self::getBasepath($url, true), DI::baseUrl());