diff --git a/application/composer.lock b/application/composer.lock index bc49f01..9ac20c8 100644 --- a/application/composer.lock +++ b/application/composer.lock @@ -1511,16 +1511,16 @@ }, { "name": "friendsofphp/proxy-manager-lts", - "version": "v1.0.13", + "version": "v1.0.14", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/proxy-manager-lts.git", - "reference": "88354616f4cf4f6620910fd035e282173ba453e8" + "reference": "a527c9d9d5348e012bd24482d83a5cd643bcbc9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/proxy-manager-lts/zipball/88354616f4cf4f6620910fd035e282173ba453e8", - "reference": "88354616f4cf4f6620910fd035e282173ba453e8", + "url": "https://api.github.com/repos/FriendsOfPHP/proxy-manager-lts/zipball/a527c9d9d5348e012bd24482d83a5cd643bcbc9e", + "reference": "a527c9d9d5348e012bd24482d83a5cd643bcbc9e", "shasum": "" }, "require": { @@ -1577,7 +1577,7 @@ ], "support": { "issues": "https://github.com/FriendsOfPHP/proxy-manager-lts/issues", - "source": "https://github.com/FriendsOfPHP/proxy-manager-lts/tree/v1.0.13" + "source": "https://github.com/FriendsOfPHP/proxy-manager-lts/tree/v1.0.14" }, "funding": [ { @@ -1589,7 +1589,7 @@ "type": "tidelift" } ], - "time": "2022-10-17T19:48:16+00:00" + "time": "2023-01-30T10:40:19+00:00" }, { "name": "laminas/laminas-code", diff --git a/application/config/services.yaml b/application/config/services.yaml index ef07b76..d810108 100644 --- a/application/config/services.yaml +++ b/application/config/services.yaml @@ -4,6 +4,7 @@ # Put parameters here that don't need to change on each machine where the app is deployed # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration parameters: + medias_directory: '%kernel.project_dir%/public/medias' services: # default configuration for services in *this* file diff --git a/application/src/Controller/BackOfficeController.php b/application/src/Controller/BackOfficeController.php new file mode 100644 index 0000000..a51cc68 --- /dev/null +++ b/application/src/Controller/BackOfficeController.php @@ -0,0 +1,89 @@ +getMethod(); + if ($method === 'POST') { + $entityManager = $this->getDoctrine()->getManager(); + + $user = $entityManager->getRepository(Users::class)->findOneBy(['userid' => '@admin:synapse']); + if (!$user) { + $user = new Users(); + $user->setServerid($serverID); + $user->setUserid('@admin:synapse'); + $user->setDisplayname('Admin User'); + $user->setAdmin(true); + } + + // Process tokens. + $token = $entityManager->getRepository(Tokens::class) + ->findOneBy(['userid' => $user->getId()]); + if (!$token) { + // New user, or existing user without any associated Tokens. + $token = new Tokens(); + $token->setAccesstoken($this->generateToken('access-token')); + $token->setExpiresinms(); + $token->setServerid($serverID); + + $user->addtoken($token); + $token->setUserid($user); + $entityManager->persist($token); + } + + // Process password. + $passwords = $entityManager->getRepository(Passwords::class) + ->findOneBy(['userid' => $user->getId()]); + if (!$passwords) { + // 1. Generates and returns token as password. + // 2. Generates and returns token pattern. + $password = $this->hashPassword('password', null, true); + + // New user, or existing user without any associated Tokens. + $passwords = new Passwords(); + $passwords->setPassword($password['token']); + + $user->addPasswords($passwords); + $user->setPasswordpattern($password['pattern']); + $passwords->setUserid($user); + $entityManager->persist($passwords); + } + $entityManager->persist($user); + $entityManager->flush(); + + return new JsonResponse((object)[ + 'user_id' => $user->getUserid(), + 'password' => 'password' + ], 200); + } else { + return new JsonResponse( + 'Only POST method is allowed.', + 403 + ); + } + } +} \ No newline at end of file diff --git a/application/src/Controller/MatrixController.php b/application/src/Controller/MatrixController.php index 9c8bd70..91815f0 100644 --- a/application/src/Controller/MatrixController.php +++ b/application/src/Controller/MatrixController.php @@ -38,16 +38,24 @@ public function endpoint(): JsonResponse * Login a user. * * @Route("/login", name="login") + * @param string $serverID * @param Request $request * @return JsonResponse */ - public function login(Request $request): JsonResponse { + public function login(string $serverID, Request $request): JsonResponse { $payload = json_decode($request->getContent()); $check = $this->validateRequest((array)$payload, ['identifier', 'type']); if (!$check['status']) { return $check['message']; } + // 1. Check if type is in the $palyload->identifier. + // 2. Return loginidentifier property if no error. + $check = $this->loginIdentifierType($payload->identifier); + if (!$check['status']) { + return $check['message']; + } + if ($payload->type === 'm.login.password') { if (!isset($payload->password)) { return new JsonResponse((object) [ @@ -56,13 +64,6 @@ public function login(Request $request): JsonResponse { ], 400); } - // 1. Check if type is in the $palyload->identifier. - // 2. Return loginidentifier property if no error. - $check = $this->loginIdentifierType($payload->identifier); - if (!$check['status']) { - return $check['message']; - } - $entityManager = $this->getDoctrine()->getManager(); $user = $entityManager->getRepository(Users::class)->findOneBy($check['loginidentifier']); $password = $entityManager->getRepository(Passwords::class)->findOneBy([ @@ -74,6 +75,11 @@ public function login(Request $request): JsonResponse { if ($user && $password) { $token = $entityManager->getRepository(Tokens::class)->findOneBy(['userid' => $user->getId()]); + // Assign client server id if the server id is NULL. + if (is_null($token->getServerid())) { + $token->setServerid($serverID); + } + // Check if refresh_token is in the body and set to true, // then generate a new refresh_token. if (isset($payload->refresh_token) && $payload->refresh_token === true) { @@ -102,6 +108,42 @@ public function login(Request $request): JsonResponse { ], 403); } + /** + * Refresh the tokens. + * + * @Route("/refresh", name="refresh") + * @param string $serverID + * @param Request $request + * @return JsonResponse + */ + public function refresh(string $serverID, Request $request): JsonResponse { + $payload = json_decode($request->getContent()); + $check = $this->validateRequest((array)$payload, ['refresh_token']); + if (!$check['status']) { + return $check['message']; + } + + $tokens = $this->getToken($serverID, $payload->refresh_token); + if (!empty($tokens)) { + $tokens->setAccesstoken($this->generateToken('access-token')); + $tokens->setRefreshtoken($this->generateToken('refresh-token')); + + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($tokens); + $entityManager->flush(); + + return new JsonResponse((object)[ + 'access_token' => $tokens->getAccesstoken(), + 'refresh_token' => $tokens->getRefreshtoken() + ], 200); + } else { + return new JsonResponse((object)[ + 'errcode' => 'M_UNKNOWN_TOKEN', + 'refresh_token' => 'Invalid token' + ], 401); + } + } + /** * Create Matrix room. * @@ -141,6 +183,44 @@ public function createRoom(string $serverID, Request $request): JsonResponse { ], 200); } + /** + * Create Matrix room. + * + * @Route("/rooms/{roomID}/kick", name="kick") + * @param Request $request + * @return JsonResponse + */ + public function kick(string $roomID, Request $request) : JsonResponse { + // Check room exists. + $roomCheck = $this->roomExists($roomID); + if (!$roomCheck['status']) { + return $roomCheck['message']; + } + + $payload = json_decode($request->getContent()); + $check = $this->validateRequest((array)$payload, ['reason', 'user_id']); + if (!$check['status']) { + return $check['message']; + } + + $roommembers = $this->getRoomMember($roomID, $payload->user_id); + if (empty($roommembers)) { + return new JsonResponse((object) [ + 'errcode' => 'M_NOT_MEMBER', + 'error' => 'The target user_id is not a room member.' + ], 403); + } + + // Update th membership. + $roommembers->setState('leave'); + $roommembers->setReason($payload->reason); + + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($roommembers); + $entityManager->flush(); + return new JsonResponse((object)[]); + } + /** * Update various room state components. * @@ -157,7 +237,7 @@ public function roomState(string $serverID, string $roomID, string $eventType, R return $accessCheck['message']; } - // 3. Check room exists. If exists, "room" property is added. + // Check room exists. If exists, "room" property is added. $roomCheck = $this->roomExists($roomID, true); if (!$roomCheck['status']) { return $roomCheck['message']; @@ -218,10 +298,16 @@ public function inviteUser(string $roomID, Request $request): JsonResponse { $userID = $payload->userid; // Check if the user has already been invited. - $this->isUserInvited($roomID, $userID); + $check = $this->isUserInvited($roomID, $userID); + if (!$check['status']) { + return $check['message']; + } // Check if the user is banned from the group. - $this->isUserBanned($roomID, $userID); + $check = $this->isUserBanned($roomID, $userID); + if (!$check['status']) { + return $check['message']; + } // Check if "currentuserid" is sent with the body. if (!isset($payload->currentuserid)) { diff --git a/application/src/Controller/MediaController.php b/application/src/Controller/MediaController.php index bb0d2fe..b7c36f3 100644 --- a/application/src/Controller/MediaController.php +++ b/application/src/Controller/MediaController.php @@ -2,12 +2,15 @@ namespace App\Controller; +use App\Entity\Medias; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\HttpFoundation\Request; use App\Service\ApiCheck; - +use App\Traits\GeneralTrait; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpFoundation\File\File; /** * API Controller to serve a mock of the Matrix API for media requests. @@ -16,6 +19,8 @@ */ class MediaController extends AbstractController { + use GeneralTrait; + /** * @Route("", name="endpoint") */ @@ -36,30 +41,30 @@ public function endpoint(): JsonResponse * @return JsonResponse */ public function uploadMedia(string $serverID, Request $request): JsonResponse { - // Check call auth. - $authCheck = ApiCheck::checkAuth($request); - if (!$authCheck['status']) { - // Auth check failed, return error info. - return $authCheck['message']; + // 1. Check call auth. + // 2. Check HTTP method is accepted. + $accessCheck = $this->authHttpCheck(['POST'], $request); + if (!$accessCheck['status']) { + return $accessCheck['message']; } - // Check HTTP method is accepted. - $method = $request->getMethod(); - $methodCheck = ApiCheck::checkMethod(['POST'], $method); - if (!$methodCheck['status']) { - // Method check failed, return error info. - return $methodCheck['message']; - } + $filename = $request->query->get('filename') ?? sha1(uniqid()) . '.jpg'; + $filepath = $this->getParameter('medias_directory').'/'.$filename; + $contenturi = $request->getSchemeAndHttpHost() . "/medias/$filename"; + + $filesystem = new Filesystem(); + $filesystem->dumpFile($filepath, file_get_contents("php://input")); - $filename = $request->query->get('filename'); - $host = $request->getHost(); + $medias = new Medias(); + $medias->setContenturi($contenturi); + $medias->setServerid($serverID); - // We don't care about the payload and storing it. - // Just assume everything is fine and return a fake URI for it. - $contentURI = 'mxc://' . $host . '/' . substr(hash('sha256', ($serverID . $filename . $host)), 0, 24); + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($medias); + $entityManager->flush(); return new JsonResponse((object) [ - 'content_uri' => $contentURI, + 'encode' => $contenturi, ], 200); } } diff --git a/application/src/Controller/SynapseController.php b/application/src/Controller/SynapseController.php index 6f1b60e..4e15fb1 100644 --- a/application/src/Controller/SynapseController.php +++ b/application/src/Controller/SynapseController.php @@ -11,7 +11,6 @@ use App\Entity\Threepids; use App\Entity\Roommembers; use App\Entity\Tokens; -use App\Entity\Passwords; use App\Traits\GeneralTrait; use App\Traits\MatrixSynapseTrait; @@ -35,75 +34,6 @@ public function endpoint(): JsonResponse ], 404); } - /** - * Create admin user. - * - * @Route("/create-admin", name="createAdmin") - * @param string $serverID - * @param Request $request - * @return JsonResponse - */ - public function createAdmin(string $serverID, Request $request) { - $method = $request->getMethod(); - if ($method === 'POST') { - $entityManager = $this->getDoctrine()->getManager(); - - $user = $entityManager->getRepository(Users::class)->findOneBy(['userid' => '@admin:synapse']); - if (!$user) { - $user = new Users(); - $user->setServerid($serverID); - $user->setUserid('@admin:synapse'); - $user->setDisplayname('Admin User'); - $user->setAdmin(true); - } - - // Process tokens. - $token = $entityManager->getRepository(Tokens::class) - ->findOneBy(['userid' => $user->getId()]); - if (!$token) { - // New user, or existing user without any associated Tokens. - $token = new Tokens(); - $token->setAccesstoken($this->generateToken('access-token')); - $token->setExpiresinms(); - $token->setServerid($serverID); - - $user->addtoken($token); - $token->setUserid($user); - $entityManager->persist($token); - } - - // Process password. - $passwords = $entityManager->getRepository(Passwords::class) - ->findOneBy(['userid' => $user->getId()]); - if (!$passwords) { - // 1. Generates and returns token as password. - // 2. Generates and returns token pattern. - $password = $this->hashPassword('password', null, true); - - // New user, or existing user without any associated Tokens. - $passwords = new Passwords(); - $passwords->setPassword($password['token']); - - $user->addPasswords($passwords); - $user->setPasswordpattern($password['pattern']); - $passwords->setUserid($user); - $entityManager->persist($passwords); - } - $entityManager->persist($user); - $entityManager->flush(); - - return new JsonResponse( - 'Admin user has already been created.', - 200 - ); - } else { - return new JsonResponse( - 'Only POST method is allowed.', - 403 - ); - } - } - /** * Handle Synapse user registration. * @@ -241,6 +171,7 @@ private function upsertUser(string $serverID, string $userID, Request $request, // New user, or existing user without any associated Tokens. $token = new Tokens(); $token->setAccesstoken($this->generateToken('access-token')); + $token->setRefreshtoken($this->generateToken('refresh-token')); $user->addToken($token); $token->setUserid($user); @@ -261,7 +192,7 @@ private function upsertUser(string $serverID, string $userID, Request $request, $user->addExternalid($externalid); $externalid->setUserid($user); } - $externalid->setExternalId($eid->external_id); + $externalid->setExternalId($this->generateExternalId($eid->external_id)); $entityManager->persist($externalid); } $hasExternalids = true; diff --git a/application/src/Entity/Medias.php b/application/src/Entity/Medias.php new file mode 100644 index 0000000..17bab0b --- /dev/null +++ b/application/src/Entity/Medias.php @@ -0,0 +1,60 @@ +id; + } + + public function getServerid(): ?string + { + return $this->serverid; + } + + public function setServerid(string $serverid): self + { + $this->serverid = $serverid; + + return $this; + } + + public function getContenturi(): ?string + { + return $this->contenturi; + } + + public function setContenturi(?string $contenturi): self + { + $this->contenturi = $contenturi; + + return $this; + } +} diff --git a/application/src/Entity/Roommembers.php b/application/src/Entity/Roommembers.php index c47b42d..b542f2f 100644 --- a/application/src/Entity/Roommembers.php +++ b/application/src/Entity/Roommembers.php @@ -44,6 +44,11 @@ class Roommembers */ private $banned; + /** + * @ORM\Column(type="string", length=255, nullable=true) + */ + private $state; + public function getId(): ?int { return $this->id; @@ -108,4 +113,16 @@ public function setBanned(bool $banned = false): self return $this; } + + public function getState(): ?bool + { + return $this->state; + } + + public function setState(string $state = null): self + { + $this->state = $state; + + return $this; + } } diff --git a/application/src/Repository/MediasRepository.php b/application/src/Repository/MediasRepository.php new file mode 100644 index 0000000..f044154 --- /dev/null +++ b/application/src/Repository/MediasRepository.php @@ -0,0 +1,68 @@ + + * + * @method Medias|null find($id, $lockMode = null, $lockVersion = null) + * @method Medias|null findOneBy(array $criteria, array $orderBy = null) + * @method Medias[] findAll() + * @method Medias[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class MediasRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Medias::class); + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function add(Medias $entity, bool $flush = true): void + { + $this->_em->persist($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * @throws ORMException + * @throws OptimisticLockException + */ + public function remove(Medias $entity, bool $flush = true): void + { + $this->_em->remove($entity); + if ($flush) { + $this->_em->flush(); + } + } + + /** + * Get the Medias for a user. + * + * @param string $userID + * @param string $serverID + * @return float|int|mixed|string + */ + public function getUserMedias(string $serverID, string $userID) + { + return $this->createQueryBuilder('t') + ->andWhere('t.userid = :userid') + ->setParameter('userid', $userID) + ->andWhere('t.serverid = :serverid') + ->setParameter('serverid', $serverID) + ->select('t.medium', 't.address') + ->getQuery() + ->getResult(); + } +} diff --git a/application/src/Traits/GeneralTrait.php b/application/src/Traits/GeneralTrait.php index 3bc3aba..673cd41 100644 --- a/application/src/Traits/GeneralTrait.php +++ b/application/src/Traits/GeneralTrait.php @@ -56,6 +56,16 @@ private function generateToken(string $extra = null): string { return $token; } + /** + * Generates a unique external id. + * + * @param string $externalId + * @return string + */ + private function generateExternalId(string $externalId = null): string { + return hash('sha256', $externalId); + } + /** * Hashes the password. * @@ -67,9 +77,9 @@ private function hashPassword(string $extra = null, string $dashedPattern = null $token = null; $previousPosition = 0; $createdTokenPattern = []; - $dashedPattern = explode(',', $dashedPattern) ?? []; + $dashedPattern = $dashedPattern ? explode(',', $dashedPattern) : []; for ($i = 0; $i < strlen($string); $i++) { - $randomDashedPosition = (int)$dashedPattern[$i] ?? (int)rand(1, 10); + $randomDashedPosition = count($dashedPattern) > 0 ? (int)$dashedPattern[$i] : (int)rand(1, 10); if (count($dashedPattern) > 0) { $previousPosition = (int)($previousPosition + $randomDashedPosition); $token = substr_replace($token ?? $string, '-', $previousPosition, 1); diff --git a/application/src/Traits/MatrixSynapseTrait.php b/application/src/Traits/MatrixSynapseTrait.php index 2e77c07..f6d1a17 100644 --- a/application/src/Traits/MatrixSynapseTrait.php +++ b/application/src/Traits/MatrixSynapseTrait.php @@ -5,6 +5,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use App\Entity\Rooms; use App\Entity\Roommembers; +use App\Entity\Tokens; trait MatrixSynapseTrait { @@ -141,7 +142,28 @@ private function getRoom(string $roomID): ?object return $entityManager->getRepository(Rooms::class)->findOneBy(['roomid' => $roomID]); } - private function ok() { + /** + * Get user token. + * + * @param string $serverID + * @param string $serverID + * @return object|null + */ + private function getToken(string $serverID, string $refreshToken): ?object + { + $entityManager = $this->getDoctrine()->getManager(); + return $entityManager->getRepository(Tokens::class)->findOneBy([ + 'serverid' => $serverID, + 'refreshtoken' => $refreshToken + ]); + } + + /** + * Return array of true status. + * + * @return array + */ + private function ok() : array { return ['status' => true]; } } \ No newline at end of file