src/App/EventSubscriber/Framework/ControllerEventSubscriber.php line 102

Open in your IDE?
  1. <?php
  2. namespace App\EventSubscriber\Framework;
  3. use App\Controller\DoAdditionalUserChecksControllerInterface;
  4. use App\Entity\DirectLoginAuthKey;
  5. use App\Entity\NextStepInfoForAfterSubscription;
  6. use App\Entity\User;
  7. use App\Service\DirectLoginAuthService;
  8. use App\Service\FeatureFlagService;
  9. use App\Service\FeatureLimitationsService;
  10. use App\Service\FrontendSpaService;
  11. use App\Service\JobseekerProfileService;
  12. use App\Service\Membership\MembershipService;
  13. use App\Service\RouterHelperService;
  14. use App\Service\SessionService;
  15. use Doctrine\ORM\EntityManagerInterface;
  16. use Exception;
  17. use JanusHercules\JoboffererRegistration\Domain\Service\FlowLogicService;
  18. use JanusHercules\JoboffererRegistration\Presentation\Interface\DoFlowCheckControllerInterface;
  19. use Psr\Log\LoggerInterface;
  20. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  21. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  22. use Symfony\Component\HttpFoundation\RedirectResponse;
  23. use Symfony\Component\HttpFoundation\Request;
  24. use Symfony\Component\HttpFoundation\RequestStack;
  25. use Symfony\Component\HttpFoundation\Response;
  26. use Symfony\Component\HttpKernel\Event\ControllerEvent;
  27. use Symfony\Component\HttpKernel\KernelEvents;
  28. use Symfony\Component\Routing\RouterInterface;
  29. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  30. use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
  31. use Symfony\Component\Validator\Validator\ValidatorInterface;
  32. use Symfony\Contracts\Translation\TranslatorInterface;
  33. class ControllerEventSubscriber implements EventSubscriberInterface
  34. {
  35. protected $routerHelperService;
  36. protected $router;
  37. protected $requestStack;
  38. protected $tokenStorage;
  39. protected $validator;
  40. protected $translator;
  41. protected $entityManager;
  42. protected $membershipService;
  43. protected $featureLimitationsService;
  44. protected $logger;
  45. protected $sessionService;
  46. protected $featureFlagService;
  47. protected $directLoginAuthService;
  48. protected $flowLogicService;
  49. protected $jobseekerProfileService;
  50. private FrontendSpaService $frontendSpaService;
  51. public function __construct(
  52. RouterHelperService $routerHelperService,
  53. RouterInterface $router,
  54. RequestStack $requestStack,
  55. TokenStorageInterface $tokenStorage,
  56. ValidatorInterface $validator,
  57. TranslatorInterface $translator,
  58. EntityManagerInterface $entityManager,
  59. MembershipService $membershipService,
  60. FeatureLimitationsService $featureLimitationsService,
  61. LoggerInterface $logger,
  62. SessionService $sessionService,
  63. FeatureFlagService $featureFlagService,
  64. DirectLoginAuthService $directLoginAuthService,
  65. FrontendSpaService $frontendSpaService,
  66. FlowLogicService $flowLogicService,
  67. JobseekerProfileService $jobseekerProfileService
  68. ) {
  69. $this->routerHelperService = $routerHelperService;
  70. $this->router = $router;
  71. $this->requestStack = $requestStack;
  72. $this->tokenStorage = $tokenStorage;
  73. $this->validator = $validator;
  74. $this->translator = $translator;
  75. $this->entityManager = $entityManager;
  76. $this->membershipService = $membershipService;
  77. $this->featureLimitationsService = $featureLimitationsService;
  78. $this->logger = $logger;
  79. $this->sessionService = $sessionService;
  80. $this->featureFlagService = $featureFlagService;
  81. $this->directLoginAuthService = $directLoginAuthService;
  82. $this->frontendSpaService = $frontendSpaService;
  83. $this->flowLogicService = $flowLogicService;
  84. $this->jobseekerProfileService = $jobseekerProfileService;
  85. }
  86. public static function getSubscribedEvents(): array
  87. {
  88. return [
  89. KernelEvents::CONTROLLER => 'onKernelController'
  90. ];
  91. }
  92. /**
  93. * @throws Exception
  94. */
  95. public function onKernelController(ControllerEvent $event)
  96. {
  97. $controller = $event->getController();
  98. /*
  99. * $controller passed can be either a class or a Closure.
  100. * This is not usual in Symfony but it may happen.
  101. * If it is a class, it comes in array format
  102. */
  103. if (!is_array($controller)) {
  104. return;
  105. }
  106. /** @var AbstractController $theController */
  107. $theController = $controller[0];
  108. /** @var Request $theRequest */
  109. $theRequest = $event->getRequest();
  110. // Special rule. See https://trello.com/c/fXTtOM2c/1071-umsetzung-subdom%C3%A4ne-020-epos-gmbh-auf-recruit-sperren
  111. if (str_starts_with($theRequest->getHttpHost(), '020-epos-gmbh.')) {
  112. $response = new Response(
  113. 'This subdomain does not exist.', Response::HTTP_NOT_FOUND
  114. );
  115. $event->setController(function () use ($response) {
  116. return $response;
  117. });
  118. return;
  119. }
  120. $matchedRoute = [];
  121. $routeCanBeMatched = false;
  122. try { // Matching the route won't work for POST request, so ignore these
  123. $matchedRoute = $this->router->match(strtok($theRequest->getRequestUri(), '?'))['_route'];
  124. $routeCanBeMatched = true;
  125. // $this->logger->debug('ControllerEventSubscriber matched route: ' . $matchedRoute . ' for URI ' . $theRequest->getRequestUri());
  126. } catch (Exception $e) {
  127. $routeCanBeMatched = false;
  128. }
  129. /*
  130. * These are the routes we never want to protect
  131. */
  132. if ($routeCanBeMatched && in_array(
  133. $matchedRoute,
  134. [
  135. 'account.conversations.unread_messages_count.api',
  136. 'activitytracking.update_last_seen',
  137. 'cookiepreferences.accept_all',
  138. 'cookiepreferences.accept_core',
  139. 'frontend_spa.index'
  140. ]
  141. )
  142. ) {
  143. return;
  144. }
  145. $token = $this->tokenStorage->getToken();
  146. /** @var User $user */
  147. $user = null;
  148. if (!is_null($token)) {
  149. $user = $token->getUser();
  150. }
  151. $directLoginAuthKeyId = $theRequest->get(DirectLoginAuthService::KEY_ID_QUERY_PARAMETER_NAME);
  152. if (!is_null($directLoginAuthKeyId)) {
  153. try {
  154. if (!is_null($directLoginAuthKey = $this->entityManager->getRepository(DirectLoginAuthKey::class)->find($directLoginAuthKeyId))) {
  155. $userToLogIn = $user = $this->directLoginAuthService->verifyAndGetDirectLoginAuthUser($directLoginAuthKey);
  156. $sessionToken = new UsernamePasswordToken($userToLogIn, null, 'main', $userToLogIn->getRoles());
  157. $this->tokenStorage->setToken($sessionToken);
  158. // We need to ensure that end users never see URIs which include the directLoginAuthKeyId parameter value;
  159. // they could forward it to another party ("hey, look at the job I've found on Joboo!"), giving that
  160. // party unintended access to their user account.
  161. $response = new RedirectResponse(
  162. $this->routerHelperService->removeQueryStringParamsInRequest($theRequest, [DirectLoginAuthService::KEY_ID_QUERY_PARAMETER_NAME])
  163. );
  164. $event->setController(function () use ($response) {
  165. return $response;
  166. });
  167. return;
  168. }
  169. } catch (Exception $e) {
  170. $this->logger->warning("Tried to do a direct login based on auth key id '{$directLoginAuthKey}', but it failed with an exception: '{$e->getMessage()}'.");
  171. }
  172. }
  173. if (is_object($user) && $user instanceof User) {
  174. if (
  175. $user->hasSystemGeneratedPassword()
  176. && !$user->isLocked()
  177. && $user->hasAtLeastBasePlusProfile()
  178. && $routeCanBeMatched && !in_array(
  179. $matchedRoute,
  180. [
  181. 'account.profiles.editor.jobseeker',
  182. 'account.profiles.editor.jobofferer',
  183. 'account.setownpassword.form',
  184. 'account.recurrent_jobs.new',
  185. 'account.recurrent_jobs.editor',
  186. 'account.recurrent_jobs.editor_plus',
  187. 'account.recurrent_jobs.deactivation',
  188. 'account.wanted_jobs.new',
  189. 'account.wanted_jobs.editor',
  190. 'account.wanted_jobs.deactivation',
  191. 'account.subscription.payment_problem',
  192. 'account.subscription.manage',
  193. 'account.subscription.jobofferer.index',
  194. 'account.subscription.upgrade',
  195. 'account.subscription.jobofferer.choose_and_pay_form',
  196. 'account.subscription.choose_and_pay_error_grabber',
  197. 'account.subscription.success'
  198. ]
  199. )
  200. ) {
  201. $this->requestStack->getSession()->set('showSetOwnPasswordNotice', true);
  202. } else {
  203. $this->requestStack->getSession()->set('showSetOwnPasswordNotice', false);
  204. }
  205. }
  206. if ($theController instanceof DoFlowCheckControllerInterface) {
  207. if (is_object($user) && $user instanceof User) {
  208. if ($user->isLocked()) {
  209. $url = $this->routerHelperService->generate('errors.own_user_locked');
  210. $response = new RedirectResponse($url);
  211. $event->setController(function () use ($response) {
  212. return $response;
  213. });
  214. return;
  215. }
  216. if ($user->isJobofferer() && $user->hasJoboffererProfile()) {
  217. $joboffererProfile = $user->getDefaultJoboffererProfile();
  218. $redirectDataArray = $this->flowLogicService->getRedirectDataArray(
  219. $user,
  220. $joboffererProfile,
  221. $routeCanBeMatched,
  222. $matchedRoute
  223. );
  224. if ($redirectDataArray['url'] !== null) {
  225. if ($redirectDataArray['message'] !== null) {
  226. $this->requestStack->getSession()->getFlashBag()->add(
  227. 'warning',
  228. $this->translator->trans($redirectDataArray['message'])
  229. );
  230. }
  231. $url = $this->routerHelperService->generate($redirectDataArray['url'], $redirectDataArray['urlParameters']);
  232. $response = new RedirectResponse($url);
  233. $event->setController(function () use ($response) {
  234. return $response;
  235. });
  236. }
  237. return;
  238. }
  239. }
  240. }
  241. if ($theController instanceof DoAdditionalUserChecksControllerInterface) {
  242. if (is_object($user) && $user instanceof User) {
  243. if ($user->isLocked()) {
  244. $url = $this->routerHelperService->generate('errors.own_user_locked');
  245. $response = new RedirectResponse($url);
  246. $event->setController(function () use ($response) {
  247. return $response;
  248. });
  249. return;
  250. }
  251. // Crawler Managers that are not also admins must not reach any pages outside the Crawler Manager context
  252. if (!$this->featureLimitationsService->userCanUseWebsiteOutsideOfExternalJobPostingCrawlerManager($user)
  253. && $routeCanBeMatched && !in_array(
  254. $matchedRoute,
  255. [
  256. 'fos_user_security_logout',
  257. 'recurrent_jobs.share',
  258. 'recurrent_jobs.view_or_forward',
  259. 'recurrent_jobs.forward_to_external_job_post',
  260. 'recurrent_jobs.remove_trailing_slash',
  261. 'frontend_spa.index',
  262. 'frontend_spa.index_dev',
  263. 'frontend_spa.backend_css',
  264. 'frontend_spa_api.v1.external-job-posting-crawler-management.get-available-crawling-options',
  265. 'frontend_spa_api.v1.external-job-posting-crawler-management.create-and-submit-content-parser-task',
  266. 'frontend_spa_api.v1.external-job-posting-crawler-management.get-latest-content-parser-task',
  267. 'frontend_spa_api.v1.external-job-posting-crawler-management.get-content-parser-task'
  268. ]
  269. )
  270. ) {
  271. $url = "{$this->frontendSpaService->getSpaUrl(FrontendSpaService::ROUTE_EXTERNAL_JOB_POSTING_CRAWLER_MANAGEMENT_CONTENT_PARSER_TASK_MANAGER)}";
  272. $response = new RedirectResponse($url);
  273. $event->setController(function () use ($response) {
  274. return $response;
  275. });
  276. return;
  277. }
  278. if ($user->isJobseeker() && !$user->hasJobseekerProfile()) {
  279. $profile = $this->jobseekerProfileService->getOrCreateAndGetDefaultProfile($user);
  280. $profile->setMobilenumberPublic(true);
  281. }
  282. if ($user->isJobseeker() && $user->hasJobseekerProfile()) {
  283. // In order to allow the jobseeker to open his subscription page we also need to allow the
  284. // jobofferer subscription route, because that is where the navigation always points at.
  285. if ($routeCanBeMatched && in_array(
  286. $matchedRoute,
  287. [
  288. 'account.subscription.jobofferer.index',
  289. 'account.subscription.jobseeker.index'
  290. ]
  291. )
  292. ) {
  293. return;
  294. }
  295. $jobseekerProfile = $user->getDefaultJobseekerProfile();
  296. if ($jobseekerProfile->needsToBeCompletedToBasic()) {
  297. $url = $this->routerHelperService->generate('account.profiles.editor.jobseeker', ['id' => $jobseekerProfile->getId()]);
  298. $this->requestStack->getSession()->getFlashBag()->add(
  299. 'warning',
  300. $this->translator->trans('profiles.not_available_without_valid_default_profile')
  301. );
  302. $response = new RedirectResponse($url);
  303. $event->setController(function () use ($response) {
  304. return $response;
  305. });
  306. return;
  307. }
  308. }
  309. // Admittingly, this case should be near-impossible, because we send all users trough the profile index
  310. // action where an empty profile is created, but who knows.
  311. if (($user->isJobseeker() && !$user->hasJobseekerProfile())
  312. || ($user->isJobofferer() && !$user->hasJoboffererProfile())
  313. ) {
  314. $url = $this->routerHelperService->generate('account.index');
  315. $this->requestStack->getSession()->getFlashBag()->add(
  316. 'warning',
  317. $this->translator->trans('profiles.not_available_without_valid_default_profile')
  318. );
  319. $response = new RedirectResponse($url);
  320. $event->setController(function () use ($response) {
  321. return $response;
  322. });
  323. return;
  324. }
  325. // The jobofferer is subscribed, but there is a problem with their payment.
  326. // We check this before the general subscription check below, so that
  327. // users see the payment problem page even if their subscription is no longer
  328. // active by now
  329. if (
  330. $user->isJobofferer()
  331. && $this->membershipService->userHasPaymentProblem($user)
  332. && $routeCanBeMatched && !in_array(
  333. $matchedRoute,
  334. [
  335. 'account.recurrent_jobs.index_router',
  336. 'account.recurrent_jobs.index',
  337. 'account.recurrent_jobs.deactivation',
  338. 'account.recurrent_jobs.deactivation.api',
  339. 'account.subscription.payment_problem',
  340. 'account.subscription.manage',
  341. 'contact',
  342. 'homepage',
  343. 'content'
  344. ]
  345. )
  346. ) {
  347. $url = $this->routerHelperService->generate('account.subscription.payment_problem');
  348. $response = new RedirectResponse($url);
  349. $event->setController(function () use ($response) {
  350. return $response;
  351. });
  352. return;
  353. }
  354. // Force non-paying jobofferers or jobofferers not linked to an external partner into subscription if
  355. // they want to use any pages except recurrent job creation or subscription pages or non-business-relevant pages
  356. if (
  357. $user->isJobofferer()
  358. && !$this->featureLimitationsService->userHasAccessToNonfreeJoboffererFeatures($user)
  359. && !$this->membershipService->userHasPaymentProblem($user)
  360. && $routeCanBeMatched && !in_array(
  361. $matchedRoute,
  362. [
  363. 'account.recurrent_jobs.index_router',
  364. 'account.recurrent_jobs.index',
  365. 'account.recurrent_jobs.new_router',
  366. 'account.recurrent_jobs.new',
  367. 'account.recurrent_jobs.editor',
  368. 'account.recurrent_jobs.editor_plus',
  369. 'account.recurrent_jobs.deactivation',
  370. 'account.recurrent_jobs.publish',
  371. 'account.recurrent_jobs.duplication',
  372. 'account.recurrent_jobs.delete.api',
  373. 'account.recurrent_jobs.reactivation',
  374. 'account.recurrent_jobs.reactivation.api',
  375. 'account.recurrent_jobs.deactivation.api',
  376. 'account.recurrent_jobs.deactivation.admin.api',
  377. 'account.recurrent_jobs.self_categorization_for_afa',
  378. 'account.recurrent_jobs.redirect_to_choose_and_pay',
  379. 'account.subscription.jobofferer.index',
  380. 'account.subscription.upgrade',
  381. 'account.subscription.calculate_upgrade_costs.api',
  382. 'account.subscription.jobofferer.choose_and_pay_form',
  383. 'account.subscription.jobofferer.choose_and_pay_form_trial_february_2023',
  384. 'account.subscription.resubscribe',
  385. 'account.subscription.choose_and_pay_error_grabber',
  386. 'account.subscription.unable_to_subscribe',
  387. 'account.subscription.choose_and_pay.initiate_conversion_tracking',
  388. 'account.subscription.choose_and_pay.handle_conversion_tracking',
  389. 'wanted_jobs_search.form',
  390. 'wanted_jobs_search.results',
  391. 'wanted_jobs_search.results.for_specific_recurrent_job',
  392. 'wanted_jobs_search.profile_selection_for_multiple_message.add.api',
  393. 'wanted_jobs_search.profile_selection_for_multiple_message.get.api',
  394. 'wanted_jobs_search.profile_selection_for_multiple_message.delete.api',
  395. 'account.favorites.jobofferer.add_jobseeker.api',
  396. 'account.favorites.jobofferer.remove_jobseeker.api',
  397. 'account.wanted_jobs.new',
  398. 'account.wanted_jobs.new_router',
  399. 'account.wanted_jobs.editor',
  400. 'account.wanted_jobs.index',
  401. 'account.wanted_jobs.index_router',
  402. 'account.wanted_jobs.deactivation',
  403. 'account.wanted_jobs.reactivation',
  404. 'account.wanted_jobs.duplication',
  405. 'account.wanted_jobs.publish',
  406. 'account.wanted_jobs.delete.api',
  407. 'account.conversations.index_router',
  408. 'account.conversations.show_router',
  409. 'account.conversations.new_message_router',
  410. 'account.conversations.return_from_new_review',
  411. 'account.conversations.index_jobofferer',
  412. 'account.conversations.remove_jobofferer.api',
  413. 'account.conversations.jobofferer.remove_message.api',
  414. 'wanted_jobs.share',
  415. 'contact',
  416. 'homepage',
  417. 'content',
  418. 'janus_hercules.membership.presentation.resume_membership.api',
  419. 'janus_hercules.membership.presentation.cancel_membership_questionnaire.api',
  420. 'janus_hercules.membership.presentation.cancel_membership.api'
  421. ]
  422. )
  423. ) {
  424. if ($this->featureFlagService->isFeatureEnabledForUser(FeatureFlagService::FEATURE_REDIRECT_ON_SUBSCRIPTION_SUCCESS, $user)) {
  425. if (in_array(
  426. $matchedRoute,
  427. [
  428. 'account.conversations.show_jobofferer',
  429. 'account.conversations.multiple_jobofferer_form',
  430. 'wanted_jobs.share'
  431. ])
  432. ) {
  433. $targetUrl = $theRequest->get('after_subscription_target_url');
  434. $targetUrlChecksum = $theRequest->get('after_subscription_target_url_checksum');
  435. if (!is_null($targetUrl) && !is_null($targetUrlChecksum)) {
  436. try {
  437. $nextStepInfoForAfterSubscription = new NextStepInfoForAfterSubscription($targetUrl, $targetUrlChecksum, $theRequest->request->all(), null);
  438. $this->sessionService->setNextStepInfoForAfterSubscription($this->requestStack->getSession(), $nextStepInfoForAfterSubscription);
  439. } catch (Exception $e) {
  440. $this->logger->warning('Wanted to define a NextStepInfoForAfterSubscription, but got ' . $e->getMessage());
  441. }
  442. }
  443. }
  444. }
  445. $url = $this->routerHelperService->generate('account.subscription.jobofferer.choose_and_pay_form');
  446. if ($this->membershipService->userHasPausedJoboffererMembership($user)) {
  447. if ($matchedRoute == 'account.subscription.manage') {
  448. return;
  449. }
  450. $url = $this->routerHelperService->generate('account.subscription.jobofferer.index', [
  451. 'showPauseNotice' => '1',
  452. ]);
  453. }
  454. if ($matchedRoute == 'account.recurrent_jobs.reactivation') {
  455. $this->requestStack->getSession()->getFlashBag()->add(
  456. 'warning',
  457. $this->translator->trans('recurrent_jobs.index_page.activation_warning_message')
  458. );
  459. }
  460. $response = new RedirectResponse($url);
  461. $event->setController(function () use ($response) {
  462. return $response;
  463. });
  464. return;
  465. }
  466. // Allow Navigators to only access certain pages
  467. if ($user->isJobofferer()
  468. && !$user->hasAtLeastOneAdminRole()
  469. && $this->featureFlagService->isFeatureEnabledForUser(FeatureFlagService::FEATURE_NAVIGATE_WANTED_JOBS, $user)
  470. && $routeCanBeMatched && !in_array(
  471. $matchedRoute,
  472. [
  473. 'account.subscription.unable_to_subscribe',
  474. 'wanted_jobs.share',
  475. 'recurrent_jobs.share',
  476. 'navigator.recurrent_jobs',
  477. 'contact',
  478. 'homepage',
  479. 'content'
  480. ]
  481. )
  482. ) {
  483. $url = $this->routerHelperService->generate('account.subscription.unable_to_subscribe');
  484. $response = new RedirectResponse($url);
  485. $event->setController(function () use ($response) {
  486. return $response;
  487. });
  488. return;
  489. }
  490. }
  491. }
  492. }
  493. }