SubdomainService.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. <?php
  2. namespace App\Service\Typo3;
  3. use App\Entity\Organization\Organization;
  4. use App\Entity\Organization\Subdomain;
  5. use App\Message\Message\Mailer;
  6. use App\Message\Message\Typo3\Typo3Update;
  7. use App\Repository\Access\AccessRepository;
  8. use App\Repository\Organization\SubdomainRepository;
  9. use App\Service\Mailer\Model\SubdomainChangeModel;
  10. use App\Service\Organization\Utils as OrganizationUtils;
  11. use Doctrine\ORM\EntityManagerInterface;
  12. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  13. use Symfony\Component\Messenger\MessageBusInterface;
  14. /**
  15. * Service de gestion des sous-domaines des utilisateurs.
  16. */
  17. class SubdomainService
  18. {
  19. // Max number of subdomains that an organization can own
  20. public const MAX_SUBDOMAINS_NUMBER = 3;
  21. // Validation regex for subdomains
  22. public const RX_VALIDATE_SUBDOMAIN = '/^[a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9])?$/';
  23. public function __construct(
  24. private readonly SubdomainRepository $subdomainRepository,
  25. private readonly EntityManagerInterface $em,
  26. private readonly MessageBusInterface $messageBus,
  27. private readonly OrganizationUtils $organizationUtils,
  28. private readonly BindFileService $bindFileService,
  29. private readonly AccessRepository $accessRepository,
  30. private readonly ParameterBagInterface $parameterBag,
  31. ) {
  32. }
  33. /**
  34. * Récupère le sous-domaine actif de l'organisation.
  35. */
  36. public function getSubdomain(string $subdomainValue): ?Subdomain
  37. {
  38. return $this->subdomainRepository->findOneBy(['subdomain' => $subdomainValue]);
  39. }
  40. /**
  41. * Is the organization allowed to register a new subdomain.
  42. */
  43. public function canRegisterNewSubdomain(Organization $organization): bool
  44. {
  45. return count($organization->getSubdomains()) < self::MAX_SUBDOMAINS_NUMBER;
  46. }
  47. /**
  48. * Is the input a valid value for a subdomain.
  49. *
  50. * @see https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2
  51. * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
  52. * @see https://www.rfc-editor.org/rfc/rfc1123#section-2.1
  53. */
  54. public function isValidSubdomain(string $subdomainValue): bool
  55. {
  56. return (bool) preg_match(self::RX_VALIDATE_SUBDOMAIN, $subdomainValue);
  57. }
  58. /**
  59. * Is the subdomain a reserved one.
  60. *
  61. * @see https://ressources.opentalent.fr/display/SPEC/Nom+de+sous+domaines+reserves+pour+2IOS
  62. *
  63. * @throws \Exception
  64. */
  65. public function isReservedSubdomain(string $subdomainValue): bool
  66. {
  67. $reservedSubdomains = $this->parameterBag->get('opentalent.subdomains')['reserved'];
  68. $subRegexes = array_map(
  69. function (string $s) { return '(\b'.trim($s, '^$/\s').'\b)'; },
  70. $reservedSubdomains
  71. );
  72. $regex = '/^'.strtolower(implode('|', $subRegexes)).'$/';
  73. return preg_match($regex, $subdomainValue) !== 0;
  74. }
  75. /**
  76. * Returns true if the subdomain has already been registered.
  77. */
  78. public function isRegistered(string $subdomainValue): bool
  79. {
  80. return count($this->subdomainRepository->findBy(['subdomain' => $subdomainValue])) !== 0;
  81. }
  82. /**
  83. * Register a new subdomain for the organization
  84. * Is $activate is true, makes this new subdomain the active one too.
  85. */
  86. public function addNewSubdomain(
  87. Organization $organization,
  88. string $subdomainValue,
  89. bool $activate = false,
  90. ): Subdomain {
  91. if (!$this->isValidSubdomain($subdomainValue)) {
  92. throw new \RuntimeException('Not a valid subdomain');
  93. }
  94. if (!$this->canRegisterNewSubdomain($organization)) {
  95. throw new \RuntimeException('This organization can not register new subdomains');
  96. }
  97. if ($this->isReservedSubdomain($subdomainValue)) {
  98. throw new \RuntimeException('This subdomain is not available');
  99. }
  100. if ($this->isRegistered($subdomainValue)) {
  101. throw new \RuntimeException('This subdomain is already registered');
  102. }
  103. $this->em->beginTransaction();
  104. try {
  105. $subdomain = new Subdomain();
  106. $subdomain->setSubdomain($subdomainValue);
  107. $subdomain->setOrganization($organization);
  108. $subdomain->setActive(false);
  109. $this->em->persist($subdomain);
  110. $this->em->flush();
  111. // Register into the BindFile (takes up to 5min to take effect)
  112. $this->bindFileService->registerSubdomain($subdomain->getSubdomain());
  113. if ($activate) {
  114. $subdomain = $this->activateSubdomain($subdomain);
  115. }
  116. $this->em->commit();
  117. } catch (\Throwable $e) {
  118. $this->em->rollback();
  119. throw $e;
  120. }
  121. return $subdomain;
  122. }
  123. /**
  124. * Makes the $subdomain the active one for the organization.
  125. */
  126. public function activateSubdomain(Subdomain $subdomain): Subdomain
  127. {
  128. $currentActiveSubdomain = $this->subdomainRepository->getActiveSubdomainOf($subdomain->getOrganization());
  129. if ($currentActiveSubdomain && $subdomain->getId() === $currentActiveSubdomain->getId()) {
  130. throw new \RuntimeException('The subdomain is already active');
  131. }
  132. if (!$subdomain->getId()) {
  133. throw new \RuntimeException('Can not activate a non-persisted subdomain');
  134. }
  135. $subdomain = $this->setOrganizationActiveSubdomain($subdomain);
  136. $this->renameAdminUserToMatchSubdomain($subdomain);
  137. // Update the typo3 website (asynchronously with messenger)
  138. $this->updateTypo3Website($subdomain->getOrganization());
  139. // Send confirmation email
  140. $this->sendConfirmationEmail($subdomain);
  141. return $subdomain;
  142. }
  143. /**
  144. * The subdomain becomes the only active subdomain of its organization.
  145. * New state is persisted is database.
  146. */
  147. protected function setOrganizationActiveSubdomain(Subdomain $subdomain): Subdomain
  148. {
  149. foreach ($subdomain->getOrganization()->getSubdomains() as $other) {
  150. if ($other !== $subdomain && $other->isActive()) {
  151. $other->setActive(false);
  152. }
  153. }
  154. $subdomain->setActive(true);
  155. // <--- Pour la rétrocompatibilité avec la v1; pourra être supprimé lorsque la migration sera achevée
  156. $parameters = $subdomain->getOrganization()->getParameters();
  157. $parameters->setSubDomain($subdomain->getSubdomain());
  158. $parameters->setOtherWebsite('https://'.$subdomain->getSubdomain().'.opentalent.fr');
  159. $this->em->persist($parameters);
  160. // --->
  161. // TODO: comprendre pourquoi ce refresh est indispensable pour que l'organisation soit à jour
  162. $this->em->flush();
  163. $this->em->refresh($subdomain->getOrganization());
  164. return $subdomain;
  165. }
  166. /**
  167. * Rename the admin user of the organization to match the given subdomain.
  168. */
  169. protected function renameAdminUserToMatchSubdomain(Subdomain $subdomain): void
  170. {
  171. $adminAccess = $this->accessRepository->findAdminAccess($subdomain->getOrganization());
  172. $adminAccess->getPerson()->setUsername('admin'.$subdomain->getSubdomain());
  173. $this->em->flush();
  174. }
  175. /**
  176. * Trigger an update of the typo3 organization's website.
  177. */
  178. protected function updateTypo3Website(Organization $organization): void
  179. {
  180. $this->messageBus->dispatch(
  181. new Typo3Update($organization->getId())
  182. );
  183. }
  184. /**
  185. * Build the data model for the confirmation email.
  186. */
  187. protected function getMailModel(Subdomain $subdomain): SubdomainChangeModel
  188. {
  189. $adminAccess = $this->accessRepository->findAdminAccess($subdomain->getOrganization());
  190. /* @phpstan-ignore-next-line */
  191. return (new SubdomainChangeModel())
  192. ->setOrganizationId($subdomain->getOrganization()->getId())
  193. ->setSubdomainId($subdomain->getId())
  194. ->setUrl($this->organizationUtils->getOrganizationWebsite($subdomain->getOrganization()))
  195. ->setSenderId($adminAccess->getId());
  196. }
  197. /**
  198. * Send the confirmation email to the organization after a new subdomain has been activated.
  199. */
  200. protected function sendConfirmationEmail(Subdomain $subdomain): void
  201. {
  202. // TODO: revoir quel sender par défaut
  203. $model = $this->getMailModel($subdomain);
  204. // Envoi d'un email
  205. $this->messageBus->dispatch(
  206. new Mailer($model)
  207. );
  208. }
  209. }