SubdomainService.php 8.2 KB

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