From 940619325d9b2109d90b40347607302ebd944bd9 Mon Sep 17 00:00:00 2001 From: Philipp Date: Tue, 18 Oct 2022 22:20:04 +0200 Subject: [PATCH] Add SessionUsers class including tests --- .../Capability/IHandleUserSessions.php | 81 +++++++ src/Core/Session/Handler/Cache.php | 2 +- src/Core/Session/Handler/Database.php | 8 +- src/Core/Session/Model/UserSession.php | 121 ++++++++++ src/Core/Session/Type/ArraySession.php | 81 +++++++ src/DI.php | 6 + src/Model/Contact.php | 26 +++ static/dependencies.config.php | 4 + tests/src/Core/Session/UserSessionTest.php | 218 ++++++++++++++++++ 9 files changed, 542 insertions(+), 5 deletions(-) create mode 100644 src/Core/Session/Capability/IHandleUserSessions.php create mode 100644 src/Core/Session/Model/UserSession.php create mode 100644 src/Core/Session/Type/ArraySession.php create mode 100644 tests/src/Core/Session/UserSessionTest.php diff --git a/src/Core/Session/Capability/IHandleUserSessions.php b/src/Core/Session/Capability/IHandleUserSessions.php new file mode 100644 index 0000000000..9cd8de3455 --- /dev/null +++ b/src/Core/Session/Capability/IHandleUserSessions.php @@ -0,0 +1,81 @@ +. + * + */ + +namespace Friendica\Core\Session\Capability; + +/** + * Handles user infos based on session infos + */ +interface IHandleUserSessions +{ + /** + * Returns the user id of locally logged-in user or false. + * + * @return int|bool user id or false + */ + public function getLocalUserId(); + + /** + * Returns the public contact id of logged-in user or false. + * + * @return int|bool public contact id or false + */ + public function getPublicContactId(); + + /** + * Returns public contact id of authenticated site visitor or false + * + * @return int|bool visitor_id or false + */ + public function getRemoteUserId(); + + /** + * Return the user contact ID of a visitor for the given user ID they are visiting + * + * @param int $uid User ID + * + * @return int + */ + public function getRemoteContactID(int $uid): int; + + /** + * Returns User ID for given contact ID of the visitor + * + * @param int $cid Contact ID + * + * @return int User ID for given contact ID of the visitor + */ + public function getUserIDForVisitorContactID(int $cid): int; + + /** + * Returns if the current visitor is authenticated + * + * @return bool "true" when visitor is either a local or remote user + */ + public function isAuthenticated(): bool; + + /** + * Set the session variable that contains the contact IDs for the visitor's contact URL + * + * @param string $url Contact URL + */ + public function setVisitorsContacts(); +} diff --git a/src/Core/Session/Handler/Cache.php b/src/Core/Session/Handler/Cache.php index 671fd7ebe1..519bad2228 100644 --- a/src/Core/Session/Handler/Cache.php +++ b/src/Core/Session/Handler/Cache.php @@ -61,7 +61,7 @@ class Cache implements SessionHandlerInterface return $data; } } catch (CachePersistenceException $exception) { - $this->logger->warning('Cannot read session.'. ['id' => $id, 'exception' => $exception]); + $this->logger->warning('Cannot read session.', ['id' => $id, 'exception' => $exception]); return ''; } diff --git a/src/Core/Session/Handler/Database.php b/src/Core/Session/Handler/Database.php index 46311dda2a..714471f9fe 100644 --- a/src/Core/Session/Handler/Database.php +++ b/src/Core/Session/Handler/Database.php @@ -70,7 +70,7 @@ class Database implements SessionHandlerInterface return $session['data']; } } catch (\Exception $exception) { - $this->logger->warning('Cannot read session.'. ['id' => $id, 'exception' => $exception]); + $this->logger->warning('Cannot read session.', ['id' => $id, 'exception' => $exception]); return ''; } @@ -114,7 +114,7 @@ class Database implements SessionHandlerInterface $this->dba->insert('session', $fields); } } catch (\Exception $exception) { - $this->logger->warning('Cannot write session.'. ['id' => $id, 'exception' => $exception]); + $this->logger->warning('Cannot write session.', ['id' => $id, 'exception' => $exception]); return false; } @@ -131,7 +131,7 @@ class Database implements SessionHandlerInterface try { return $this->dba->delete('session', ['sid' => $id]); } catch (\Exception $exception) { - $this->logger->warning('Cannot destroy session.'. ['id' => $id, 'exception' => $exception]); + $this->logger->warning('Cannot destroy session.', ['id' => $id, 'exception' => $exception]); return false; } } @@ -141,7 +141,7 @@ class Database implements SessionHandlerInterface try { return $this->dba->delete('session', ["`expire` < ?", time()]); } catch (\Exception $exception) { - $this->logger->warning('Cannot use garbage collector.'. ['exception' => $exception]); + $this->logger->warning('Cannot use garbage collector.', ['exception' => $exception]); return false; } } diff --git a/src/Core/Session/Model/UserSession.php b/src/Core/Session/Model/UserSession.php new file mode 100644 index 0000000000..1b0d141216 --- /dev/null +++ b/src/Core/Session/Model/UserSession.php @@ -0,0 +1,121 @@ +. + * + */ + +namespace Friendica\Core\Session\Model; + +use Friendica\Core\Session\Capability\IHandleSessions; +use Friendica\Core\Session\Capability\IHandleUserSessions; +use Friendica\Model\Contact; + +class UserSession implements IHandleUserSessions +{ + /** @var IHandleSessions */ + private $session; + /** @var int|bool saves the public Contact ID for later usage */ + protected $publicContactId = false; + + public function __construct(IHandleSessions $session) + { + $this->session = $session; + } + + /** {@inheritDoc} */ + public function getLocalUserId() + { + if (!empty($this->session->get('authenticated')) && !empty($this->session->get('uid'))) { + return intval($this->session->get('uid')); + } + + return false; + } + + /** {@inheritDoc} */ + public function getPublicContactId() + { + if (empty($this->publicContactId) && !empty($this->session->get('authenticated'))) { + if (!empty($this->session->get('my_address'))) { + // Local user + $this->publicContactId = Contact::getIdForURL($this->session->get('my_address'), 0, false); + } elseif (!empty($this->session->get('visitor_home'))) { + // Remote user + $this->publicContactId = Contact::getIdForURL($this->session->get('visitor_home'), 0, false); + } + } elseif (empty($this->session->get('authenticated'))) { + $this->publicContactId = false; + } + + return $this->publicContactId; + } + + /** {@inheritDoc} */ + public function getRemoteUserId() + { + if (empty($this->session->get('authenticated'))) { + return false; + } + + if (!empty($this->session->get('visitor_id'))) { + return (int)$this->session->get('visitor_id'); + } + + return false; + } + + /** {@inheritDoc} */ + public function getRemoteContactID(int $uid): int + { + if (!empty($this->session->get('remote')[$uid])) { + $remote = $this->session->get('remote')[$uid]; + } else { + $remote = 0; + } + + $local_user = !empty($this->session->get('authenticated')) ? $this->session->get('uid') : 0; + + if (empty($remote) && ($local_user != $uid) && !empty($my_address = $this->session->get('my_address'))) { + $remote = Contact::getIdForURL($my_address, $uid, false); + } + + return $remote; + } + + /** {@inheritDoc} */ + public function getUserIDForVisitorContactID(int $cid): int + { + if (empty($this->session->get('remote'))) { + return false; + } + + return array_search($cid, $this->session->get('remote')); + } + + /** {@inheritDoc} */ + public function isAuthenticated(): bool + { + return $this->session->get('authenticated', false); + } + + /** {@inheritDoc} */ + public function setVisitorsContacts() + { + $this->session->set('remote', Contact::getVisitorByUrl($this->session->get('my_url'))); + } +} diff --git a/src/Core/Session/Type/ArraySession.php b/src/Core/Session/Type/ArraySession.php new file mode 100644 index 0000000000..a45b64e684 --- /dev/null +++ b/src/Core/Session/Type/ArraySession.php @@ -0,0 +1,81 @@ +. + * + */ + +namespace Friendica\Core\Session\Type; + +use Friendica\Core\Session\Capability\IHandleSessions; + +class ArraySession implements IHandleSessions +{ + /** @var array */ + protected $data = []; + + public function __construct(array $data = []) + { + $this->data = $data; + } + + public function start(): IHandleSessions + { + return $this; + } + + public function exists(string $name): bool + { + return !empty($this->data[$name]); + } + + public function get(string $name, $defaults = null) + { + return $this->data[$name] ?? $defaults; + } + + public function pop(string $name, $defaults = null) + { + $value = $defaults; + if ($this->exists($name)) { + $value = $this->get($name); + $this->remove($name); + } + + return $value; + } + + public function set(string $name, $value) + { + $this->data[$name] = $value; + } + + public function setMultiple(array $values) + { + $this->data = array_merge($values, $this->data); + } + + public function remove(string $name) + { + unset($this->data[$name]); + } + + public function clear() + { + $this->data = []; + } +} diff --git a/src/DI.php b/src/DI.php index a28eb707a4..b107e8c348 100644 --- a/src/DI.php +++ b/src/DI.php @@ -22,6 +22,7 @@ namespace Friendica; use Dice\Dice; +use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Navigation\SystemMessages; use Psr\Log\LoggerInterface; @@ -219,6 +220,11 @@ abstract class DI return self::$dice->create(Core\Session\Capability\IHandleSessions::class); } + public static function userSession(): IHandleUserSessions + { + return self::$dice->create(Core\Session\Capability\IHandleUserSessions::class); + } + /** * @return \Friendica\Core\Storage\Repository\StorageManager */ diff --git a/src/Model/Contact.php b/src/Model/Contact.php index d8a5b387fd..0f960f1609 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -261,6 +261,32 @@ class Contact return DBA::selectFirst('contact', $fields, ['uri-id' => $uri_id], ['order' => ['uid']]); } + /** + * Fetch all remote contacts for a given contact url + * + * @param string $url The URL of the contact + * @param array $fields The wanted fields + * + * @return array all remote contacts + * + * @throws \Exception + */ + public static function getVisitorByUrl(string $url, array $fields = ['id', 'uid']): array + { + $remote = []; + + $remote_contacts = DBA::select('contact', ['id', 'uid'], ['nurl' => Strings::normaliseLink($url), 'rel' => [Contact::FOLLOWER, Contact::FRIEND], 'self' => false]); + while ($contact = DBA::fetch($remote_contacts)) { + if (($contact['uid'] == 0) || Contact\User::isBlocked($contact['id'], $contact['uid'])) { + continue; + } + $remote[$contact['uid']] = $contact['id']; + } + DBA::close($remote_contacts); + + return $remote; + } + /** * Fetches a contact by a given url * diff --git a/static/dependencies.config.php b/static/dependencies.config.php index 5aba529db2..f7f98bb676 100644 --- a/static/dependencies.config.php +++ b/static/dependencies.config.php @@ -41,6 +41,7 @@ use Friendica\Core\PConfig; use Friendica\Core\L10n; use Friendica\Core\Lock; use Friendica\Core\Session\Capability\IHandleSessions; +use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Core\Storage\Repository\StorageManager; use Friendica\Database\Database; use Friendica\Database\Definition\DbaDefinition; @@ -224,6 +225,9 @@ return [ ['start', [], Dice::CHAIN_CALL], ], ], + IHandleUserSessions::class => [ + 'instanceOf' => \Friendica\Core\Session\Model\UserSession::class, + ], Cookie::class => [ 'constructParams' => [ $_COOKIE diff --git a/tests/src/Core/Session/UserSessionTest.php b/tests/src/Core/Session/UserSessionTest.php new file mode 100644 index 0000000000..5325923871 --- /dev/null +++ b/tests/src/Core/Session/UserSessionTest.php @@ -0,0 +1,218 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Session; + +use Friendica\Core\Session\Model\UserSession; +use Friendica\Core\Session\Type\ArraySession; +use Friendica\Test\MockedTest; + +class UserSessionTest extends MockedTest +{ + public function dataLocalUserId() + { + return [ + 'standard' => [ + 'data' => [ + 'authenticated' => true, + 'uid' => 21, + ], + 'expected' => 21, + ], + 'not_auth' => [ + 'data' => [ + 'authenticated' => false, + 'uid' => 21, + ], + 'expected' => false, + ], + 'no_uid' => [ + 'data' => [ + 'authenticated' => true, + ], + 'expected' => false, + ], + 'no_auth' => [ + 'data' => [ + 'uid' => 21, + ], + 'expected' => false, + ], + 'invalid' => [ + 'data' => [ + 'authenticated' => false, + 'uid' => 'test', + ], + 'expected' => false, + ], + ]; + } + + /** + * @dataProvider dataLocalUserId + */ + public function testGetLocalUserId(array $data, $expected) + { + $userSession = new UserSession(new ArraySession($data)); + $this->assertEquals($expected, $userSession->getLocalUserId()); + } + + public function testPublicContactId() + { + $this->markTestSkipped('Needs Contact::getIdForURL testable first'); + } + + public function dataGetRemoteUserId() + { + return [ + 'standard' => [ + 'data' => [ + 'authenticated' => true, + 'visitor_id' => 21, + ], + 'expected' => 21, + ], + 'not_auth' => [ + 'data' => [ + 'authenticated' => false, + 'visitor_id' => 21, + ], + 'expected' => false, + ], + 'no_visitor_id' => [ + 'data' => [ + 'authenticated' => true, + ], + 'expected' => false, + ], + 'no_auth' => [ + 'data' => [ + 'visitor_id' => 21, + ], + 'expected' => false, + ], + 'invalid' => [ + 'data' => [ + 'authenticated' => false, + 'visitor_id' => 'test', + ], + 'expected' => false, + ], + ]; + } + + /** + * @dataProvider dataGetRemoteUserId + */ + public function testGetRemoteUserId(array $data, $expected) + { + $userSession = new UserSession(new ArraySession($data)); + $this->assertEquals($expected, $userSession->getRemoteUserId()); + } + + /// @fixme Add more data when Contact::getIdForUrl ist a dynamic class + public function dataGetRemoteContactId() + { + return [ + 'remote_exists' => [ + 'uid' => 1, + 'data' => [ + 'remote' => ['1' => '21'], + ], + 'expected' => 21, + ], + ]; + } + + /** + * @dataProvider dataGetRemoteContactId + */ + public function testGetRemoteContactId(int $uid, array $data, $expected) + { + $userSession = new UserSession(new ArraySession($data)); + $this->assertEquals($expected, $userSession->getRemoteContactID($uid)); + } + + public function dataGetUserIdForVisitorContactID() + { + return [ + 'standard' => [ + 'cid' => 21, + 'data' => [ + 'remote' => ['3' => '21'], + ], + 'expected' => 3, + ], + 'missing' => [ + 'cid' => 2, + 'data' => [ + 'remote' => ['3' => '21'], + ], + 'expected' => false, + ], + 'empty' => [ + 'cid' => 21, + 'data' => [ + ], + 'expected' => false, + ], + ]; + } + + /** @dataProvider dataGetUserIdForVisitorContactID */ + public function testGetUserIdForVisitorContactID(int $cid, array $data, $expected) + { + $userSession = new UserSession(new ArraySession($data)); + $this->assertEquals($expected, $userSession->getUserIDForVisitorContactID($cid)); + } + + public function dataAuthenticated() + { + return [ + 'authenticated' => [ + 'data' => [ + 'authenticated' => true, + ], + 'expected' => true, + ], + 'not_authenticated' => [ + 'data' => [ + 'authenticated' => false, + ], + 'expected' => false, + ], + 'missing' => [ + 'data' => [ + ], + 'expected' => false, + ], + ]; + } + + /** + * @dataProvider dataAuthenticated + */ + public function testIsAuthenticated(array $data, $expected) + { + $userSession = new UserSession(new ArraySession($data)); + $this->assertEquals($expected, $userSession->isAuthenticated()); + } +}