From deaa6b1d205b944ba077cb77feed47e804167a2a Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 25 Oct 2024 13:37:21 -0400 Subject: [PATCH] Add Captcha Management System --- src/Socialbox/Enums/Status/CaptchaStatus.php | 9 + src/Socialbox/Managers/CaptchaManager.php | 188 ++++++++++++++++++ .../Objects/Database/CaptchaRecord.php | 80 ++++++++ 3 files changed, 277 insertions(+) create mode 100644 src/Socialbox/Enums/Status/CaptchaStatus.php create mode 100644 src/Socialbox/Managers/CaptchaManager.php create mode 100644 src/Socialbox/Objects/Database/CaptchaRecord.php diff --git a/src/Socialbox/Enums/Status/CaptchaStatus.php b/src/Socialbox/Enums/Status/CaptchaStatus.php new file mode 100644 index 0000000..4400a07 --- /dev/null +++ b/src/Socialbox/Enums/Status/CaptchaStatus.php @@ -0,0 +1,9 @@ +getUuid(); + } + + $answer = Utilities::randomString(6, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'); + + if(!self::captchaExists($peer_uuid)) + { + $statement = Database::getConnection()->prepare("INSERT INTO captcha_images (peer_uuid, answer) VALUES (?, ?)"); + $statement->bindParam(1, $peer_uuid); + $statement->bindParam(2, $answer); + + try + { + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to create a captcha in the database', $e); + } + + return $answer; + } + + $statement = Database::getConnection()->prepare("UPDATE captcha_images SET answer=?, status='UNSOLVED', created=NOW() WHERE peer_uuid=?"); + $statement->bindParam(1, $answer); + $statement->bindParam(2, $peer_uuid); + + try + { + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update a captcha in the database', $e); + } + + return $answer; + } + + /** + * Answers a captcha for the given peer UUID. + * + * @param string|RegisteredPeerRecord $peer_uuid The UUID of the peer to answer the captcha for. + * @param string $answer The answer to the captcha. + * @return bool True if the answer is correct, false otherwise. + * @throws DatabaseOperationException If the operation fails. + */ + public static function answerCaptcha(string|RegisteredPeerRecord $peer_uuid, string $answer): bool + { + if($peer_uuid instanceof RegisteredPeerRecord) + { + $peer_uuid = $peer_uuid->getUuid(); + } + + // Return false if the captcha does not exist + if(!self::captchaExists($peer_uuid)) + { + return false; + } + + $captcha = self::getCaptcha($peer_uuid); + + // Return false if the captcha has already been solved + if($captcha->getStatus() === CaptchaStatus::SOLVED) + { + return false; + } + + // Return false if the captcha is older than 5 minutes + if ($captcha->getCreated() instanceof DateTimeInterface && $captcha->getCreated()->diff(new DateTime())->i > 5) + { + return false; + } + + // Verify the answer + if($captcha->getAnswer() !== $answer) + { + return false; + } + + $statement = Database::getConnection()->prepare("UPDATE captcha_images SET status='SOLVED', answered=NOW() WHERE peer_uuid=?"); + $statement->bindParam(1, $peer_uuid); + + try + { + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update a captcha in the database', $e); + } + + return true; + } + + /** + * Retrieves the captcha record for the given peer UUID. + * + * @param string|RegisteredPeerRecord $peer_uuid The UUID of the peer to retrieve the captcha for. + * @return CaptchaRecord The captcha record. + * @throws DatabaseOperationException If the operation fails. + */ + public static function getCaptcha(string|RegisteredPeerRecord $peer_uuid): CaptchaRecord + { + // If the peer_uuid is a RegisteredPeerRecord, get the UUID + if($peer_uuid instanceof RegisteredPeerRecord) + { + $peer_uuid = $peer_uuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare("SELECT * FROM captcha_images WHERE peer_uuid=? LIMIT 1"); + $statement->bindParam(1, $peer_uuid); + $statement->execute(); + $result = $statement->fetch(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to get a captcha from the database', $e); + } + + if($result === false) + { + throw new DatabaseOperationException('The requested captcha does not exist'); + } + + return CaptchaRecord::fromArray($result); + } + + /** + * Checks if a captcha exists for the given peer UUID. + * + * @param string|RegisteredPeerRecord $peer_uuid The UUID of the peer to check for a captcha. + * @return bool True if a captcha exists, false otherwise. + * @throws DatabaseOperationException If the operation fails. + */ + private static function captchaExists(string|RegisteredPeerRecord $peer_uuid): bool + { + // If the peer_uuid is a RegisteredPeerRecord, get the UUID + if($peer_uuid instanceof RegisteredPeerRecord) + { + $peer_uuid = $peer_uuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare("SELECT COUNT(*) FROM captcha_images WHERE peer_uuid=?"); + $statement->bindParam(1, $peer_uuid); + $statement->execute(); + $result = $statement->fetchColumn(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to check if a captcha exists in the database', $e); + } + + return $result > 0; + } +} \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/CaptchaRecord.php b/src/Socialbox/Objects/Database/CaptchaRecord.php new file mode 100644 index 0000000..4427310 --- /dev/null +++ b/src/Socialbox/Objects/Database/CaptchaRecord.php @@ -0,0 +1,80 @@ +uuid = (string)$data['uuid']; + $this->peerUuid = (string)$data['peer_uuid']; + $this->status = CaptchaStatus::tryFrom((string)$data['status']); + $this->answer = isset($data['answer']) ? (string)$data['answer'] : null; + $this->answered = isset($data['answered']) ? new DateTime((string)$data['answered']) : null; + $this->created = new DateTime((string)$data['created']); + } + + public function getUuid(): string + { + return $this->uuid; + } + + public function getPeerUuid(): string + { + return $this->peerUuid; + } + + public function getStatus(): CaptchaStatus + { + return $this->status; + } + + public function getAnswer(): ?string + { + return $this->answer; + } + + public function getAnswered(): ?DateTime + { + return $this->answered; + } + + public function getCreated(): DateTime + { + return $this->created; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): object + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'peer_uuid' => $this->peerUuid, + 'status' => $this->status->value, + 'answer' => $this->answer, + 'answered' => $this->answered?->format('Y-m-d H:i:s'), + 'created' => $this->created->format('Y-m-d H:i:s') + ]; + } +} \ No newline at end of file