getSubdomains()) < self::MAX_SUBDOMAINS_NUMBER; } /** * Is the input a valid value for a subdomain. * * @see https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5 * @see https://www.rfc-editor.org/rfc/rfc1123#section-2.1 */ public function isValidSubdomain(string $subdomainValue): bool { return (bool) preg_match(self::RX_VALIDATE_SUBDOMAIN, $subdomainValue); } /** * Is the subdomain a reserved one. * * @see https://ressources.opentalent.fr/display/SPEC/Nom+de+sous+domaines+reserves+pour+2IOS * * @throws \Exception */ public function isReservedSubdomain(string $subdomainValue): bool { $reservedSubdomains = $this->parameterBag->get('opentalent.subdomains')['reserved']; $subRegexes = array_map( function (string $s) { return '(\b'.trim($s, '^$/\s').'\b)'; }, $reservedSubdomains ); $regex = '/^'.strtolower(implode('|', $subRegexes)).'$/'; return preg_match($regex, $subdomainValue) !== 0; } /** * Returns true if the subdomain has already been registered. */ public function isRegistered(string $subdomainValue): bool { return count($this->subdomainRepository->findBy(['subdomain' => $subdomainValue])) !== 0; } /** * Register a new subdomain for the organization * Is $activate is true, makes this new subdomain the active one too. */ public function addNewSubdomain( Organization $organization, string $subdomainValue, bool $activate = false ): Subdomain { if (!$this->isValidSubdomain($subdomainValue)) { throw new \RuntimeException('Not a valid subdomain'); } if (!$this->canRegisterNewSubdomain($organization)) { throw new \RuntimeException('This organization can not register new subdomains'); } if ($this->isReservedSubdomain($subdomainValue)) { throw new \RuntimeException('This subdomain is not available'); } if ($this->isRegistered($subdomainValue)) { throw new \RuntimeException('This subdomain is already registered'); } $subdomain = new Subdomain(); $subdomain->setSubdomain($subdomainValue); $subdomain->setOrganization($organization); $subdomain->setActive(false); $this->em->beginTransaction(); try { // <--- Pour la rétrocompatibilité avec la v1; pourra être supprimé lorsque la migration sera achevée $parameters = $organization->getParameters(); $parameters->setSubDomain($subdomainValue); $parameters->setOtherWebsite('https://'.$subdomainValue.'.opentalent.fr'); $this->em->persist($parameters); // ---> $this->em->persist($subdomain); $this->em->flush(); // Register into the BindFile (takes up to 5min to take effect) $this->bindFileService->registerSubdomain($subdomain->getSubdomain()); if ($activate) { $subdomain = $this->activateSubdomain($subdomain); } $this->em->commit(); } catch (\Throwable $e) { $this->em->rollback(); throw $e; } return $subdomain; } /** * Makes the $subdomain the active one for the organization. */ public function activateSubdomain(Subdomain $subdomain): Subdomain { $currentActiveSubdomain = $this->subdomainRepository->getActiveSubdomainOf($subdomain->getOrganization()); if ($currentActiveSubdomain && $subdomain->getId() === $currentActiveSubdomain->getId()) { throw new \RuntimeException('The subdomain is already active'); } if (!$subdomain->getId()) { throw new \RuntimeException('Can not activate a non-persisted subdomain'); } $subdomain = $this->setOrganizationActiveSubdomain($subdomain); $this->renameAdminUserToMatchSubdomain($subdomain); // Update the typo3 website (asynchronously with messenger) $this->updateTypo3Website($subdomain->getOrganization()); // Send confirmation email $this->sendConfirmationEmail($subdomain); return $subdomain; } /** * The subdomain becomes the only active subdomain of its organization. * New state is persisted is database. */ protected function setOrganizationActiveSubdomain(Subdomain $subdomain): Subdomain { foreach ($subdomain->getOrganization()->getSubdomains() as $other) { if ($other !== $subdomain && $other->isActive()) { $other->setActive(false); } } $subdomain->setActive(true); // TODO: comprendre pourquoi ce refresh est indispensable pour que l'organisation soit à jour $this->em->flush(); $this->em->refresh($subdomain->getOrganization()); return $subdomain; } /** * Rename the admin user of the organization to match the given subdomain. */ protected function renameAdminUserToMatchSubdomain(Subdomain $subdomain): void { $adminAccess = $this->accessRepository->findAdminAccess($subdomain->getOrganization()); $adminAccess->getPerson()->setUsername('admin'.$subdomain->getSubdomain()); $this->em->flush(); } /** * Trigger an update of the typo3 organization's website. */ protected function updateTypo3Website(Organization $organization): void { $this->messageBus->dispatch( new Typo3UpdateCommand($organization->getId()) ); } /** * Build the data model for the confirmation email. */ protected function getMailModel(Subdomain $subdomain): SubdomainChangeModel { $adminAccess = $this->accessRepository->findAdminAccess($subdomain->getOrganization()); /* @phpstan-ignore-next-line */ return (new SubdomainChangeModel()) ->setOrganizationId($subdomain->getOrganization()->getId()) ->setSubdomainId($subdomain->getId()) ->setUrl($this->organizationUtils->getOrganizationWebsite($subdomain->getOrganization())) ->setSenderId($adminAccess->getId()); } /** * Send the confirmation email to the organization after a new subdomain has been activated. */ protected function sendConfirmationEmail(Subdomain $subdomain): void { // TODO: revoir quel sender par défaut $model = $this->getMailModel($subdomain); // Envoi d'un email $this->messageBus->dispatch( new MailerCommand($model) ); } }