<?php
namespace App\EventSubscriber\Framework;
use App\Controller\DoAdditionalUserChecksControllerInterface;
use App\Entity\DirectLoginAuthKey;
use App\Entity\NextStepInfoForAfterSubscription;
use App\Entity\User;
use App\Service\DirectLoginAuthService;
use App\Service\FeatureFlagService;
use App\Service\FeatureLimitationsService;
use App\Service\FrontendSpaService;
use App\Service\JobseekerProfileService;
use App\Service\Membership\MembershipService;
use App\Service\RouterHelperService;
use App\Service\SessionService;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use JanusHercules\JoboffererRegistration\Domain\Service\FlowLogicService;
use JanusHercules\JoboffererRegistration\Presentation\Interface\DoFlowCheckControllerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class ControllerEventSubscriber implements EventSubscriberInterface
{
protected $routerHelperService;
protected $router;
protected $requestStack;
protected $tokenStorage;
protected $validator;
protected $translator;
protected $entityManager;
protected $membershipService;
protected $featureLimitationsService;
protected $logger;
protected $sessionService;
protected $featureFlagService;
protected $directLoginAuthService;
protected $flowLogicService;
protected $jobseekerProfileService;
private FrontendSpaService $frontendSpaService;
public function __construct(
RouterHelperService $routerHelperService,
RouterInterface $router,
RequestStack $requestStack,
TokenStorageInterface $tokenStorage,
ValidatorInterface $validator,
TranslatorInterface $translator,
EntityManagerInterface $entityManager,
MembershipService $membershipService,
FeatureLimitationsService $featureLimitationsService,
LoggerInterface $logger,
SessionService $sessionService,
FeatureFlagService $featureFlagService,
DirectLoginAuthService $directLoginAuthService,
FrontendSpaService $frontendSpaService,
FlowLogicService $flowLogicService,
JobseekerProfileService $jobseekerProfileService
) {
$this->routerHelperService = $routerHelperService;
$this->router = $router;
$this->requestStack = $requestStack;
$this->tokenStorage = $tokenStorage;
$this->validator = $validator;
$this->translator = $translator;
$this->entityManager = $entityManager;
$this->membershipService = $membershipService;
$this->featureLimitationsService = $featureLimitationsService;
$this->logger = $logger;
$this->sessionService = $sessionService;
$this->featureFlagService = $featureFlagService;
$this->directLoginAuthService = $directLoginAuthService;
$this->frontendSpaService = $frontendSpaService;
$this->flowLogicService = $flowLogicService;
$this->jobseekerProfileService = $jobseekerProfileService;
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER => 'onKernelController'
];
}
/**
* @throws Exception
*/
public function onKernelController(ControllerEvent $event)
{
$controller = $event->getController();
/*
* $controller passed can be either a class or a Closure.
* This is not usual in Symfony but it may happen.
* If it is a class, it comes in array format
*/
if (!is_array($controller)) {
return;
}
/** @var AbstractController $theController */
$theController = $controller[0];
/** @var Request $theRequest */
$theRequest = $event->getRequest();
// Special rule. See https://trello.com/c/fXTtOM2c/1071-umsetzung-subdom%C3%A4ne-020-epos-gmbh-auf-recruit-sperren
if (str_starts_with($theRequest->getHttpHost(), '020-epos-gmbh.')) {
$response = new Response(
'This subdomain does not exist.', Response::HTTP_NOT_FOUND
);
$event->setController(function () use ($response) {
return $response;
});
return;
}
$matchedRoute = [];
$routeCanBeMatched = false;
try { // Matching the route won't work for POST request, so ignore these
$matchedRoute = $this->router->match(strtok($theRequest->getRequestUri(), '?'))['_route'];
$routeCanBeMatched = true;
// $this->logger->debug('ControllerEventSubscriber matched route: ' . $matchedRoute . ' for URI ' . $theRequest->getRequestUri());
} catch (Exception $e) {
$routeCanBeMatched = false;
}
/*
* These are the routes we never want to protect
*/
if ($routeCanBeMatched && in_array(
$matchedRoute,
[
'account.conversations.unread_messages_count.api',
'activitytracking.update_last_seen',
'cookiepreferences.accept_all',
'cookiepreferences.accept_core',
'frontend_spa.index'
]
)
) {
return;
}
$token = $this->tokenStorage->getToken();
/** @var User $user */
$user = null;
if (!is_null($token)) {
$user = $token->getUser();
}
$directLoginAuthKeyId = $theRequest->get(DirectLoginAuthService::KEY_ID_QUERY_PARAMETER_NAME);
if (!is_null($directLoginAuthKeyId)) {
try {
if (!is_null($directLoginAuthKey = $this->entityManager->getRepository(DirectLoginAuthKey::class)->find($directLoginAuthKeyId))) {
$userToLogIn = $user = $this->directLoginAuthService->verifyAndGetDirectLoginAuthUser($directLoginAuthKey);
$sessionToken = new UsernamePasswordToken($userToLogIn, null, 'main', $userToLogIn->getRoles());
$this->tokenStorage->setToken($sessionToken);
// We need to ensure that end users never see URIs which include the directLoginAuthKeyId parameter value;
// they could forward it to another party ("hey, look at the job I've found on Joboo!"), giving that
// party unintended access to their user account.
$response = new RedirectResponse(
$this->routerHelperService->removeQueryStringParamsInRequest($theRequest, [DirectLoginAuthService::KEY_ID_QUERY_PARAMETER_NAME])
);
$event->setController(function () use ($response) {
return $response;
});
return;
}
} catch (Exception $e) {
$this->logger->warning("Tried to do a direct login based on auth key id '{$directLoginAuthKey}', but it failed with an exception: '{$e->getMessage()}'.");
}
}
if (is_object($user) && $user instanceof User) {
if (
$user->hasSystemGeneratedPassword()
&& !$user->isLocked()
&& $user->hasAtLeastBasePlusProfile()
&& $routeCanBeMatched && !in_array(
$matchedRoute,
[
'account.profiles.editor.jobseeker',
'account.profiles.editor.jobofferer',
'account.setownpassword.form',
'account.recurrent_jobs.new',
'account.recurrent_jobs.editor',
'account.recurrent_jobs.editor_plus',
'account.recurrent_jobs.deactivation',
'account.wanted_jobs.new',
'account.wanted_jobs.editor',
'account.wanted_jobs.deactivation',
'account.subscription.payment_problem',
'account.subscription.manage',
'account.subscription.jobofferer.index',
'account.subscription.upgrade',
'account.subscription.jobofferer.choose_and_pay_form',
'account.subscription.choose_and_pay_error_grabber',
'account.subscription.success'
]
)
) {
$this->requestStack->getSession()->set('showSetOwnPasswordNotice', true);
} else {
$this->requestStack->getSession()->set('showSetOwnPasswordNotice', false);
}
}
if ($theController instanceof DoFlowCheckControllerInterface) {
if (is_object($user) && $user instanceof User) {
if ($user->isLocked()) {
$url = $this->routerHelperService->generate('errors.own_user_locked');
$response = new RedirectResponse($url);
$event->setController(function () use ($response) {
return $response;
});
return;
}
if ($user->isJobofferer() && $user->hasJoboffererProfile()) {
$joboffererProfile = $user->getDefaultJoboffererProfile();
$redirectDataArray = $this->flowLogicService->getRedirectDataArray(
$user,
$joboffererProfile,
$routeCanBeMatched,
$matchedRoute
);
if ($redirectDataArray['url'] !== null) {
if ($redirectDataArray['message'] !== null) {
$this->requestStack->getSession()->getFlashBag()->add(
'warning',
$this->translator->trans($redirectDataArray['message'])
);
}
$url = $this->routerHelperService->generate($redirectDataArray['url'], $redirectDataArray['urlParameters']);
$response = new RedirectResponse($url);
$event->setController(function () use ($response) {
return $response;
});
}
return;
}
}
}
if ($theController instanceof DoAdditionalUserChecksControllerInterface) {
if (is_object($user) && $user instanceof User) {
if ($user->isLocked()) {
$url = $this->routerHelperService->generate('errors.own_user_locked');
$response = new RedirectResponse($url);
$event->setController(function () use ($response) {
return $response;
});
return;
}
// Crawler Managers that are not also admins must not reach any pages outside the Crawler Manager context
if (!$this->featureLimitationsService->userCanUseWebsiteOutsideOfExternalJobPostingCrawlerManager($user)
&& $routeCanBeMatched && !in_array(
$matchedRoute,
[
'fos_user_security_logout',
'recurrent_jobs.share',
'recurrent_jobs.view_or_forward',
'recurrent_jobs.forward_to_external_job_post',
'recurrent_jobs.remove_trailing_slash',
'frontend_spa.index',
'frontend_spa.index_dev',
'frontend_spa.backend_css',
'frontend_spa_api.v1.external-job-posting-crawler-management.get-available-crawling-options',
'frontend_spa_api.v1.external-job-posting-crawler-management.create-and-submit-content-parser-task',
'frontend_spa_api.v1.external-job-posting-crawler-management.get-latest-content-parser-task',
'frontend_spa_api.v1.external-job-posting-crawler-management.get-content-parser-task'
]
)
) {
$url = "{$this->frontendSpaService->getSpaUrl(FrontendSpaService::ROUTE_EXTERNAL_JOB_POSTING_CRAWLER_MANAGEMENT_CONTENT_PARSER_TASK_MANAGER)}";
$response = new RedirectResponse($url);
$event->setController(function () use ($response) {
return $response;
});
return;
}
if ($user->isJobseeker() && !$user->hasJobseekerProfile()) {
$profile = $this->jobseekerProfileService->getOrCreateAndGetDefaultProfile($user);
$profile->setMobilenumberPublic(true);
}
if ($user->isJobseeker() && $user->hasJobseekerProfile()) {
// In order to allow the jobseeker to open his subscription page we also need to allow the
// jobofferer subscription route, because that is where the navigation always points at.
if ($routeCanBeMatched && in_array(
$matchedRoute,
[
'account.subscription.jobofferer.index',
'account.subscription.jobseeker.index'
]
)
) {
return;
}
$jobseekerProfile = $user->getDefaultJobseekerProfile();
if ($jobseekerProfile->needsToBeCompletedToBasic()) {
$url = $this->routerHelperService->generate('account.profiles.editor.jobseeker', ['id' => $jobseekerProfile->getId()]);
$this->requestStack->getSession()->getFlashBag()->add(
'warning',
$this->translator->trans('profiles.not_available_without_valid_default_profile')
);
$response = new RedirectResponse($url);
$event->setController(function () use ($response) {
return $response;
});
return;
}
}
// Admittingly, this case should be near-impossible, because we send all users trough the profile index
// action where an empty profile is created, but who knows.
if (($user->isJobseeker() && !$user->hasJobseekerProfile())
|| ($user->isJobofferer() && !$user->hasJoboffererProfile())
) {
$url = $this->routerHelperService->generate('account.index');
$this->requestStack->getSession()->getFlashBag()->add(
'warning',
$this->translator->trans('profiles.not_available_without_valid_default_profile')
);
$response = new RedirectResponse($url);
$event->setController(function () use ($response) {
return $response;
});
return;
}
// The jobofferer is subscribed, but there is a problem with their payment.
// We check this before the general subscription check below, so that
// users see the payment problem page even if their subscription is no longer
// active by now
if (
$user->isJobofferer()
&& $this->membershipService->userHasPaymentProblem($user)
&& $routeCanBeMatched && !in_array(
$matchedRoute,
[
'account.recurrent_jobs.index_router',
'account.recurrent_jobs.index',
'account.recurrent_jobs.deactivation',
'account.recurrent_jobs.deactivation.api',
'account.subscription.payment_problem',
'account.subscription.manage',
'contact',
'homepage',
'content'
]
)
) {
$url = $this->routerHelperService->generate('account.subscription.payment_problem');
$response = new RedirectResponse($url);
$event->setController(function () use ($response) {
return $response;
});
return;
}
// Force non-paying jobofferers or jobofferers not linked to an external partner into subscription if
// they want to use any pages except recurrent job creation or subscription pages or non-business-relevant pages
if (
$user->isJobofferer()
&& !$this->featureLimitationsService->userHasAccessToNonfreeJoboffererFeatures($user)
&& !$this->membershipService->userHasPaymentProblem($user)
&& $routeCanBeMatched && !in_array(
$matchedRoute,
[
'account.recurrent_jobs.index_router',
'account.recurrent_jobs.index',
'account.recurrent_jobs.new_router',
'account.recurrent_jobs.new',
'account.recurrent_jobs.editor',
'account.recurrent_jobs.editor_plus',
'account.recurrent_jobs.deactivation',
'account.recurrent_jobs.publish',
'account.recurrent_jobs.duplication',
'account.recurrent_jobs.delete.api',
'account.recurrent_jobs.reactivation',
'account.recurrent_jobs.reactivation.api',
'account.recurrent_jobs.deactivation.api',
'account.recurrent_jobs.deactivation.admin.api',
'account.recurrent_jobs.self_categorization_for_afa',
'account.recurrent_jobs.redirect_to_choose_and_pay',
'account.subscription.jobofferer.index',
'account.subscription.upgrade',
'account.subscription.calculate_upgrade_costs.api',
'account.subscription.jobofferer.choose_and_pay_form',
'account.subscription.jobofferer.choose_and_pay_form_trial_february_2023',
'account.subscription.resubscribe',
'account.subscription.choose_and_pay_error_grabber',
'account.subscription.unable_to_subscribe',
'account.subscription.choose_and_pay.initiate_conversion_tracking',
'account.subscription.choose_and_pay.handle_conversion_tracking',
'wanted_jobs_search.form',
'wanted_jobs_search.results',
'wanted_jobs_search.results.for_specific_recurrent_job',
'wanted_jobs_search.profile_selection_for_multiple_message.add.api',
'wanted_jobs_search.profile_selection_for_multiple_message.get.api',
'wanted_jobs_search.profile_selection_for_multiple_message.delete.api',
'account.favorites.jobofferer.add_jobseeker.api',
'account.favorites.jobofferer.remove_jobseeker.api',
'account.wanted_jobs.new',
'account.wanted_jobs.new_router',
'account.wanted_jobs.editor',
'account.wanted_jobs.index',
'account.wanted_jobs.index_router',
'account.wanted_jobs.deactivation',
'account.wanted_jobs.reactivation',
'account.wanted_jobs.duplication',
'account.wanted_jobs.publish',
'account.wanted_jobs.delete.api',
'account.conversations.index_router',
'account.conversations.show_router',
'account.conversations.new_message_router',
'account.conversations.return_from_new_review',
'account.conversations.index_jobofferer',
'account.conversations.remove_jobofferer.api',
'account.conversations.jobofferer.remove_message.api',
'wanted_jobs.share',
'contact',
'homepage',
'content',
'janus_hercules.membership.presentation.resume_membership.api',
'janus_hercules.membership.presentation.cancel_membership_questionnaire.api',
'janus_hercules.membership.presentation.cancel_membership.api'
]
)
) {
if ($this->featureFlagService->isFeatureEnabledForUser(FeatureFlagService::FEATURE_REDIRECT_ON_SUBSCRIPTION_SUCCESS, $user)) {
if (in_array(
$matchedRoute,
[
'account.conversations.show_jobofferer',
'account.conversations.multiple_jobofferer_form',
'wanted_jobs.share'
])
) {
$targetUrl = $theRequest->get('after_subscription_target_url');
$targetUrlChecksum = $theRequest->get('after_subscription_target_url_checksum');
if (!is_null($targetUrl) && !is_null($targetUrlChecksum)) {
try {
$nextStepInfoForAfterSubscription = new NextStepInfoForAfterSubscription($targetUrl, $targetUrlChecksum, $theRequest->request->all(), null);
$this->sessionService->setNextStepInfoForAfterSubscription($this->requestStack->getSession(), $nextStepInfoForAfterSubscription);
} catch (Exception $e) {
$this->logger->warning('Wanted to define a NextStepInfoForAfterSubscription, but got ' . $e->getMessage());
}
}
}
}
$url = $this->routerHelperService->generate('account.subscription.jobofferer.choose_and_pay_form');
if ($this->membershipService->userHasPausedJoboffererMembership($user)) {
if ($matchedRoute == 'account.subscription.manage') {
return;
}
$url = $this->routerHelperService->generate('account.subscription.jobofferer.index', [
'showPauseNotice' => '1',
]);
}
if ($matchedRoute == 'account.recurrent_jobs.reactivation') {
$this->requestStack->getSession()->getFlashBag()->add(
'warning',
$this->translator->trans('recurrent_jobs.index_page.activation_warning_message')
);
}
$response = new RedirectResponse($url);
$event->setController(function () use ($response) {
return $response;
});
return;
}
// Allow Navigators to only access certain pages
if ($user->isJobofferer()
&& !$user->hasAtLeastOneAdminRole()
&& $this->featureFlagService->isFeatureEnabledForUser(FeatureFlagService::FEATURE_NAVIGATE_WANTED_JOBS, $user)
&& $routeCanBeMatched && !in_array(
$matchedRoute,
[
'account.subscription.unable_to_subscribe',
'wanted_jobs.share',
'recurrent_jobs.share',
'navigator.recurrent_jobs',
'contact',
'homepage',
'content'
]
)
) {
$url = $this->routerHelperService->generate('account.subscription.unable_to_subscribe');
$response = new RedirectResponse($url);
$event->setController(function () use ($response) {
return $response;
});
return;
}
}
}
}
}