SubdomainService.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. <?php
  2. namespace App\Service\Typo3;
  3. use App\Entity\Access\Access;
  4. use App\Entity\Organization\Organization;
  5. use App\Entity\Organization\Subdomain;
  6. use App\Message\Command\MailerCommand;
  7. use App\Message\Command\Typo3\Typo3UpdateCommand;
  8. use App\Repository\Access\AccessRepository;
  9. use App\Repository\Organization\SubdomainRepository;
  10. use App\Service\Mailer\Model\SubdomainChangeModel;
  11. use App\Service\Organization\Utils as OrganizationUtils;
  12. use Doctrine\ORM\EntityManagerInterface;
  13. use Symfony\Bundle\SecurityBundle\Security;
  14. use Symfony\Component\Console\Exception\InvalidArgumentException;
  15. use Symfony\Component\Messenger\MessageBusInterface;
  16. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  17. /**
  18. * Service de gestion des sous-domaines des utilisateurs
  19. */
  20. class SubdomainService
  21. {
  22. // Max number of subdomains that an organization can own
  23. const MAX_SUBDOMAINS_NUMBER = 3;
  24. // Validation regex for subdomains
  25. const RX_VALIDATE_SUBDOMAIN = '/^[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$/';
  26. public function __construct(
  27. private readonly SubdomainRepository $subdomainRepository,
  28. private readonly EntityManagerInterface $em,
  29. private readonly MessageBusInterface $messageBus,
  30. private readonly OrganizationUtils $organizationUtils,
  31. private readonly BindFileService $bindFileService,
  32. private readonly AccessRepository $accessRepository,
  33. private readonly ParameterBagInterface $parameterBag
  34. ) {}
  35. /**
  36. * Is the organization allowed to register a new subdomain
  37. *
  38. * @param Organization $organization
  39. * @return bool
  40. */
  41. public function canRegisterNewSubdomain(Organization $organization): bool {
  42. return count($organization->getSubdomains()) < self::MAX_SUBDOMAINS_NUMBER;
  43. }
  44. /**
  45. * Is the input a valid value for a subdomain
  46. *
  47. * @see https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2
  48. * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
  49. * @see https://www.rfc-editor.org/rfc/rfc1123#section-2.1
  50. *
  51. * @param string $subdomainValue
  52. * @return bool
  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. * @see https://ressources.opentalent.fr/display/SPEC/Nom+de+sous+domaines+reserves+pour+2IOS
  61. *
  62. * @param string $subdomainValue
  63. * @return bool
  64. * @throws \Exception
  65. */
  66. public function isReservedSubdomain(string $subdomainValue): bool {
  67. // $reservedSubdomains = $this->configUtils->get('subdomains')['reserved'];
  68. dd($this->parameterBag->get('modules'));
  69. $reservedSubdomains = $this->parameterBag->get('opentalent')['reserved_subdomains'];
  70. $subRegexes = array_map(
  71. function (string $s) { return '(' . trim($s, '^$/\s') . ')'; },
  72. $reservedSubdomains
  73. );
  74. $regex = '/^' . strtolower(implode("|", $subRegexes)) . '$/';
  75. return preg_match($regex, $subdomainValue) !== 0;
  76. }
  77. /**
  78. * Register a new subdomain for the organization
  79. * Is $activate is true, makes this new subdomain the active one too.
  80. *
  81. * @param Organization $organization
  82. * @param string $subdomainValue
  83. * @param bool $activate
  84. * @return Subdomain
  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. // Vérifie que le sous-domaine n'est pas déjà utilisé
  101. if ($this->subdomainRepository->findBy(['subdomain' => $subdomainValue])) {
  102. throw new \RuntimeException('This subdomain is already registered');
  103. }
  104. $subdomain = new Subdomain();
  105. $subdomain->setSubdomain($subdomainValue);
  106. $subdomain->setOrganization($organization);
  107. $subdomain->setActive(false);
  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. return $subdomain;
  116. }
  117. /**
  118. * Makes the $subdomain the active one for the organization.
  119. *
  120. * @param Subdomain $subdomain
  121. * @return Subdomain
  122. */
  123. public function activateSubdomain(Subdomain $subdomain): Subdomain {
  124. if ($subdomain->isActive()) {
  125. throw new \RuntimeException('The subdomain is already active');
  126. }
  127. if (!$subdomain->getId()) {
  128. throw new \RuntimeException('Can not activate a non-persisted subdomain');
  129. }
  130. $subdomain = $this->setOrganizationActiveSubdomain($subdomain);
  131. $this->renameAdminUserToMatchSubdomain($subdomain);
  132. // Update the typo3 website (asynchronously with messenger)
  133. $this->updateTypo3Website($subdomain->getOrganization());
  134. // Send confirmation email
  135. $this->sendConfirmationEmail($subdomain);
  136. return $subdomain;
  137. }
  138. /**
  139. * The subdomain becomes the only active subdomain of its organization.
  140. * New state is persisted is database.
  141. *
  142. * @param Subdomain $subdomain
  143. * @return Subdomain
  144. */
  145. protected function setOrganizationActiveSubdomain(Subdomain $subdomain): Subdomain {
  146. foreach ($subdomain->getOrganization()->getSubdomains() as $other) {
  147. if ($other !== $subdomain && $other->isActive()) {
  148. $other->setActive(false);
  149. }
  150. }
  151. $subdomain->setActive(true);
  152. // TODO: comprendre pourquoi ce refresh est indispensable pour que l'organisation soit à jour
  153. $this->em->flush();
  154. $this->em->refresh($subdomain->getOrganization());
  155. return $subdomain;
  156. }
  157. /**
  158. * Rename the admin user of the organization to match the given subdomain
  159. *
  160. * @param Subdomain $subdomain
  161. * @return void
  162. */
  163. protected function renameAdminUserToMatchSubdomain(Subdomain $subdomain): void {
  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. * @param $organization
  172. * @return void
  173. */
  174. protected function updateTypo3Website(Organization $organization): void
  175. {
  176. $this->messageBus->dispatch(
  177. new Typo3UpdateCommand($organization->getId())
  178. );
  179. }
  180. /**
  181. * Build the data model for the confirmation email
  182. *
  183. * @param Subdomain $subdomain
  184. * @return SubdomainChangeModel
  185. */
  186. protected function getMailModel(Subdomain $subdomain): SubdomainChangeModel {
  187. $adminAccess = $this->accessRepository->findAdminAccess($subdomain->getOrganization());
  188. /** @phpstan-ignore-next-line */
  189. return (new SubdomainChangeModel())
  190. ->setOrganizationId($subdomain->getOrganization()->getId())
  191. ->setSubdomainId($subdomain->getId())
  192. ->setUrl($this->organizationUtils->getOrganizationWebsite($subdomain->getOrganization()))
  193. ->setSenderId($adminAccess->getId());
  194. }
  195. /**
  196. * Send the confirmation email to the organization after a new subdomain has been activated
  197. *
  198. * @param Subdomain $subdomain
  199. * @return void
  200. */
  201. protected function sendConfirmationEmail(Subdomain $subdomain): void {
  202. // TODO: revoir quel sender par défaut
  203. $model = $this->getMailModel($subdomain);
  204. // Envoi d'un email
  205. $this->messageBus->dispatch(
  206. new MailerCommand($model)
  207. );
  208. }
  209. }