diff --git a/src/BaseEntity.php b/src/BaseEntity.php new file mode 100644 index 0000000000..66acab1bb0 --- /dev/null +++ b/src/BaseEntity.php @@ -0,0 +1,56 @@ +. + * + */ + +namespace Friendica; + +use Friendica\Network\HTTPException; + +/** + * The Entity classes directly inheriting from this abstract class are meant to represent a single business entity. + * Their properties may or may not correspond with the database fields of the table we use to represent it. + * Each model method must correspond to a business action being performed on this entity. + * Only these methods will be allowed to alter the model data. + * + * To persist such a model, the associated Repository must be instantiated and the "save" method must be called + * and passed the entity as a parameter. + * + * Ideally, the constructor should only be called in the associated Factory which will instantiate entities depending + * on the provided data. + * + * Since these objects aren't meant to be using any dependency, including logging, unit tests can and must be + * written for each and all of their methods + */ +abstract class BaseEntity extends BaseDataTransferObject +{ + /** + * @param string $name + * @return mixed + * @throws HTTPException\InternalServerErrorException + */ + public function __get(string $name) + { + if (!property_exists($this, $name)) { + throw new HTTPException\InternalServerErrorException('Unknown property ' . $name . ' in Entity ' . static::class); + } + + return $this->$name; + } +} diff --git a/src/Security/TwoFactor/Collection/TrustedBrowsers.php b/src/Security/TwoFactor/Collection/TrustedBrowsers.php new file mode 100644 index 0000000000..659e16a5b5 --- /dev/null +++ b/src/Security/TwoFactor/Collection/TrustedBrowsers.php @@ -0,0 +1,10 @@ +cookie_hash = $cookie_hash; + $this->uid = $uid; + $this->user_agent = $user_agent; + $this->created = $created; + $this->last_used = $last_used; + } + + public function recordUse() + { + $this->last_used = DateTimeFormat::utcNow(); + } +} diff --git a/src/Security/TwoFactor/Repository/TrustedBrowser.php b/src/Security/TwoFactor/Repository/TrustedBrowser.php new file mode 100644 index 0000000000..6d708d3673 --- /dev/null +++ b/src/Security/TwoFactor/Repository/TrustedBrowser.php @@ -0,0 +1,98 @@ +db = $database; + $this->logger = $logger; + $this->factory = $factory ?? new \Friendica\Security\TwoFactor\Factory\TrustedBrowser($logger); + } + + /** + * @param string $cookie_hash + * @return Model\TrustedBrowser|null + * @throws \Exception + */ + public function selectOneByHash(string $cookie_hash): Model\TrustedBrowser + { + $fields = $this->db->selectFirst(self::$table_name, [], ['cookie_hash' => $cookie_hash]); + if (!$this->db->isResult($fields)) { + throw new NotFoundException(''); + } + + return $this->factory->createFromTableRow($fields); + } + + public function selectAllByUid(int $uid): TrustedBrowsers + { + $rows = $this->db->selectToArray(self::$table_name, [], ['uid' => $uid]); + + $trustedBrowsers = []; + foreach ($rows as $fields) { + $trustedBrowsers[] = $this->factory->createFromTableRow($fields); + } + + return new TrustedBrowsers($trustedBrowsers); + } + + /** + * @param Model\TrustedBrowser $trustedBrowser + * @return bool + * @throws \Exception + */ + public function save(Model\TrustedBrowser $trustedBrowser): bool + { + return $this->db->insert(self::$table_name, $trustedBrowser->toArray(), $this->db::INSERT_UPDATE); + } + + /** + * @param Model\TrustedBrowser $trustedBrowser + * @return bool + * @throws \Exception + */ + public function remove(Model\TrustedBrowser $trustedBrowser): bool + { + return $this->db->delete(self::$table_name, ['cookie_hash' => $trustedBrowser->cookie_hash]); + } + + /** + * @param int $local_user + * @param string $cookie_hash + * @return bool + * @throws \Exception + */ + public function removeForUser(int $local_user, string $cookie_hash): bool + { + return $this->db->delete(self::$table_name, ['cookie_hash' => $cookie_hash,'uid' => $local_user]); + } + + /** + * @param int $local_user + * @return bool + * @throws \Exception + */ + public function removeAllForUser(int $local_user): bool + { + return $this->db->delete(self::$table_name, ['uid' => $local_user]); + } +} diff --git a/tests/src/Security/TwoFactor/Factory/TrustedBrowserTest.php b/tests/src/Security/TwoFactor/Factory/TrustedBrowserTest.php new file mode 100644 index 0000000000..5b2b6111c9 --- /dev/null +++ b/tests/src/Security/TwoFactor/Factory/TrustedBrowserTest.php @@ -0,0 +1,62 @@ + Strings::getRandomHex(), + 'uid' => 42, + 'user_agent' => 'PHPUnit', + 'created' => DateTimeFormat::utcNow(), + 'last_used' => null, + ]; + + $trustedBrowser = $factory->createFromTableRow($row); + + $this->assertEquals($row, $trustedBrowser->toArray()); + } + + public function testCreateFromTableRowMissingData() + { + $this->expectException(\TypeError::class); + + $factory = new TrustedBrowser(new VoidLogger()); + + $row = [ + 'cookie_hash' => null, + 'uid' => null, + 'user_agent' => null, + 'created' => null, + 'last_used' => null, + ]; + + $trustedBrowser = $factory->createFromTableRow($row); + + $this->assertEquals($row, $trustedBrowser->toArray()); + } + + public function testCreateForUserWithUserAgent() + { + $factory = new TrustedBrowser(new VoidLogger()); + + $uid = 42; + $userAgent = 'PHPUnit'; + + $trustedBrowser = $factory->createForUserWithUserAgent($uid, $userAgent); + + $this->assertNotEmpty($trustedBrowser->cookie_hash); + $this->assertEquals($uid, $trustedBrowser->uid); + $this->assertEquals($userAgent, $trustedBrowser->user_agent); + $this->assertNotEmpty($trustedBrowser->created); + } +} diff --git a/tests/src/Security/TwoFactor/Model/TrustedBrowserTest.php b/tests/src/Security/TwoFactor/Model/TrustedBrowserTest.php new file mode 100644 index 0000000000..d895273744 --- /dev/null +++ b/tests/src/Security/TwoFactor/Model/TrustedBrowserTest.php @@ -0,0 +1,46 @@ +assertEquals($hash, $trustedBrowser->cookie_hash); + $this->assertEquals(42, $trustedBrowser->uid); + $this->assertEquals('PHPUnit', $trustedBrowser->user_agent); + $this->assertNotEmpty($trustedBrowser->created); + } + + public function testRecordUse() + { + $hash = Strings::getRandomHex(); + $past = DateTimeFormat::utc('now - 5 minutes'); + + $trustedBrowser = new TrustedBrowser( + $hash, + 42, + 'PHPUnit', + $past, + $past + ); + + $trustedBrowser->recordUse(); + + $this->assertEquals($past, $trustedBrowser->created); + $this->assertGreaterThan($past, $trustedBrowser->last_used); + } +}