diff --git a/src/Core/Session.php b/src/Core/Session.php index a944cf8f5..d39db4861 100644 --- a/src/Core/Session.php +++ b/src/Core/Session.php @@ -31,9 +31,6 @@ use Friendica\Util\Strings; */ class Session { - public static $exists = false; - public static $expire = 180000; - /** * Returns the user id of locally logged in user or false. * diff --git a/src/Core/Session/Capability/IHandleUserSessions.php b/src/Core/Session/Capability/IHandleUserSessions.php new file mode 100644 index 000000000..9cd8de345 --- /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/AbstractSessionHandler.php b/src/Core/Session/Handler/AbstractSessionHandler.php new file mode 100644 index 000000000..a16242be5 --- /dev/null +++ b/src/Core/Session/Handler/AbstractSessionHandler.php @@ -0,0 +1,28 @@ +. + * + */ + +namespace Friendica\Core\Session\Handler; + +abstract class AbstractSessionHandler implements \SessionHandlerInterface +{ + /** @var int Duration of the Session */ + public const EXPIRE = 180000; +} diff --git a/src/Core/Session/Handler/Cache.php b/src/Core/Session/Handler/Cache.php index 671fd7ebe..4fcc51c17 100644 --- a/src/Core/Session/Handler/Cache.php +++ b/src/Core/Session/Handler/Cache.php @@ -23,14 +23,12 @@ namespace Friendica\Core\Session\Handler; use Friendica\Core\Cache\Capability\ICanCache; use Friendica\Core\Cache\Exception\CachePersistenceException; -use Friendica\Core\Session; use Psr\Log\LoggerInterface; -use SessionHandlerInterface; /** * SessionHandler using Friendica Cache */ -class Cache implements SessionHandlerInterface +class Cache extends AbstractSessionHandler { /** @var ICanCache */ private $cache; @@ -57,11 +55,10 @@ class Cache implements SessionHandlerInterface try { $data = $this->cache->get('session:' . $id); if (!empty($data)) { - Session::$exists = true; 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 ''; } @@ -91,7 +88,7 @@ class Cache implements SessionHandlerInterface } try { - return $this->cache->set('session:' . $id, $data, Session::$expire); + return $this->cache->set('session:' . $id, $data, static::EXPIRE); } catch (CachePersistenceException $exception) { $this->logger->warning('Cannot write session', ['id' => $id, 'exception' => $exception]); return false; diff --git a/src/Core/Session/Handler/Database.php b/src/Core/Session/Handler/Database.php index 46311dda2..41ccb6b33 100644 --- a/src/Core/Session/Handler/Database.php +++ b/src/Core/Session/Handler/Database.php @@ -21,15 +21,13 @@ namespace Friendica\Core\Session\Handler; -use Friendica\Core\Session; use Friendica\Database\Database as DBA; use Psr\Log\LoggerInterface; -use SessionHandlerInterface; /** * SessionHandler using database */ -class Database implements SessionHandlerInterface +class Database extends AbstractSessionHandler { /** @var DBA */ private $dba; @@ -37,6 +35,8 @@ class Database implements SessionHandlerInterface private $logger; /** @var array The $_SERVER variable */ private $server; + /** @var bool global check, if the current Session exists */ + private $sessionExists = false; /** * DatabaseSessionHandler constructor. @@ -66,11 +66,11 @@ class Database implements SessionHandlerInterface try { $session = $this->dba->selectFirst('session', ['data'], ['sid' => $id]); if ($this->dba->isResult($session)) { - Session::$exists = true; + $this->sessionExists = true; 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 ''; } @@ -101,11 +101,11 @@ class Database implements SessionHandlerInterface return $this->destroy($id); } - $expire = time() + Session::$expire; + $expire = time() + static::EXPIRE; $default_expire = time() + 300; try { - if (Session::$exists) { + if ($this->sessionExists) { $fields = ['data' => $data, 'expire' => $expire]; $condition = ["`sid` = ? AND (`data` != ? OR `expire` != ?)", $id, $data, $expire]; $this->dba->update('session', $fields, $condition); @@ -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 000000000..1b0d14121 --- /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 000000000..a45b64e68 --- /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 a28eb707a..b107e8c34 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 d8a5b387f..0f960f160 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 5aba529db..f7f98bb67 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 000000000..41c503352 --- /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 is 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()); + } +}