src/Controller/DefaultController.php line 52

Open in your IDE?
  1. <?php
  2. namespace MedBrief\MSR\Controller;
  3. use DateTime;
  4. use Doctrine\ORM\EntityManagerInterface;
  5. use Exception;
  6. use MedBrief\MSR\Controller\BaseController as Controller;
  7. use MedBrief\MSR\Entity\Analytics\AnalyticsReport;
  8. use MedBrief\MSR\Entity\Invitation;
  9. use MedBrief\MSR\Entity\Project;
  10. use MedBrief\MSR\Entity\RoleInvitation;
  11. use MedBrief\MSR\Entity\User;
  12. use MedBrief\MSR\Form\ChangePasswordFormType;
  13. use MedBrief\MSR\Form\Filter\MatterFilterType;
  14. use MedBrief\MSR\Form\Order\MatterOrderType;
  15. use MedBrief\MSR\Repository\ProjectRepository;
  16. use MedBrief\MSR\Repository\RoleInvitationRepository;
  17. use MedBrief\MSR\Repository\SystemNotificationRepository;
  18. use MedBrief\MSR\Service\Analytics\AnalyticsService;
  19. use MedBrief\MSR\Service\Analytics\Model\AnalyticsEmbeddedReport;
  20. use MedBrief\MSR\Service\DeviceDetectorService;
  21. use MedBrief\MSR\Service\EntityHelper\ProjectHelper;
  22. use MedBrief\MSR\Service\EntityHelper\UserHelper;
  23. use MedBrief\MSR\Service\LicenceRenewal\RenewalHelperService;
  24. use MedBrief\MSR\Service\ProjectMatch\MatchExpertUser;
  25. use MedBrief\MSR\Service\Role\RoleParserService;
  26. use MedBrief\MSR\Traits\PaginatorAwareTrait;
  27. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
  28. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
  29. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  30. use Symfony\Component\HttpFoundation\Cookie;
  31. use Symfony\Component\HttpFoundation\JsonResponse;
  32. use Symfony\Component\HttpFoundation\RedirectResponse;
  33. use Symfony\Component\HttpFoundation\Request;
  34. use Symfony\Component\HttpFoundation\Response;
  35. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  36. use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
  37. use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken;
  38. use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
  39. class DefaultController extends Controller
  40. {
  41. use PaginatorAwareTrait;
  42. /**
  43. * When the user navigates to the MedBrief home page, if they are logged in, then we
  44. * log them out. We then redirect to login. This is per Steve England's
  45. * request that for security purposes, when the landing page is hit, we log
  46. * users out.
  47. */
  48. public function indexAction(): JsonResponse|RedirectResponse
  49. {
  50. // if the user is currently logged in
  51. if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  52. // Forward to the dashboard. We used to log the user out but this caused more problems than it solved.
  53. return $this->redirectWithAjaxSupport($this->generateUrl('infology_medbrief_dashboard'));
  54. }
  55. // if we have a redirect page specified in the parameters
  56. $redirectUrl = $this->getParameter('root_page_redirect_url');
  57. if ($redirectUrl) {
  58. // redirect the user to it
  59. return $this->redirectWithAjaxSupport($redirectUrl);
  60. }
  61. // otherwise just sent them to login
  62. return $this->redirectWithAjaxSupport($this->generateUrl('login'));
  63. }
  64. /**
  65. * @param EntityManagerInterface $entityManager
  66. * @param AnalyticsService $analyticsService
  67. * @param UserHelper $userHelper
  68. * @param ProjectRepository $projectRepository
  69. * @param RoleInvitationRepository $roleInvitationRepository
  70. * @param SystemNotificationRepository $systemNotificationRepository
  71. * @param RoleParserService $roleParser
  72. * @param RenewalHelperService $renewalHelperService
  73. * @param Request $request
  74. * @param ProjectHelper $projectHelper
  75. * @param MatchExpertUser $matchExpertUserService
  76. *
  77. * @throws Exception
  78. *
  79. */
  80. public function dashboardAction(
  81. AnalyticsService $analyticsService,
  82. UserHelper $userHelper,
  83. ProjectRepository $projectRepository,
  84. RoleInvitationRepository $roleInvitationRepository,
  85. SystemNotificationRepository $systemNotificationRepository,
  86. RoleParserService $roleParser,
  87. RenewalHelperService $renewalHelperService,
  88. ProjectHelper $projectHelper,
  89. MatchExpertUser $matchExpertUserService,
  90. Request $request
  91. ): JsonResponse|RedirectResponse|Response {
  92. // Redirect to pending invitations page if User has pending role invitations.
  93. if ($this->userHasPendingInvitations()) {
  94. return $this->redirectWithAjaxSupport($this->pendingInvitationsUrlWithFlash());
  95. }
  96. /** @var User $user */
  97. $user = $this->getUser();
  98. $userHelper->setUser($user);
  99. // If the user has 2FA enabled and is rate limited, redirect them to the login page.
  100. if ($user->hasMultiMfaEnabled() && $request->getSession()->has('rateLimited')) {
  101. $this->addFlash('danger', 'You have entered the incorrect authentication code too many times. Please try again in fifteen minutes.');
  102. return $this->redirectToRoute('logout');
  103. }
  104. // If the user has 2FA enabled and their IP Address has been blacklisted, redirect them to the login page.
  105. if ($user->hasMultiMfaEnabled() && $request->getSession()->has('ipBlacklisted')) {
  106. $this->addFlash('danger', 'You have entered the incorrect authentication code too many times from this IP Address. Please try again in fifteen minutes.');
  107. return $this->redirectToRoute('logout');
  108. }
  109. $analyticsEmbeddedReport = $analyticsService->generateAnalyticsEmbeddedReport($user, AnalyticsReport::REPORT_TYPE_DASHBOARD);
  110. // We use this to determine if we should display the 'Explore Analytics' button.
  111. $analyticsEmbeddedReportMain = $analyticsService->generateAnalyticsEmbeddedReport($user, AnalyticsReport::REPORT_TYPE_MAIN);
  112. $recentlyViewedProjects = $this->getRecentlyViewedProjects($projectRepository, $userHelper, $projectHelper, $matchExpertUserService, false)['projects'];
  113. $favouriteProjects = $this->getFavouriteProjects($projectHelper, $matchExpertUserService, false)['projects'];
  114. $recentlyAcceptedProjects = $this->getRecentlyAcceptedProjects($roleInvitationRepository, $roleParser, $projectHelper, $matchExpertUserService, false)['projects'];
  115. $recentlyViewedProjectCount = count($recentlyViewedProjects);
  116. $favouritesCount = count($favouriteProjects);
  117. $recentlyAcceptedCount = count($recentlyAcceptedProjects);
  118. $renewalsCount = $projectRepository->countRenewalsDueWithin30DaysForUser($userHelper);
  119. // Create a combined array of all unique Projects fetched in this controller action.
  120. $uniqueProjects = array_unique(array_merge(
  121. $recentlyViewedProjects,
  122. $favouriteProjects,
  123. $recentlyAcceptedProjects,
  124. ));
  125. $grossTotalProjectsCount = count($uniqueProjects);
  126. // All Projects that have a renewal date
  127. $eligibleProjects = $this->getEligibleRenewalProjects($uniqueProjects, $renewalHelperService);
  128. // Get the latest active system notification
  129. $systemNotification = $systemNotificationRepository->findLatestSystemNotification();
  130. /**
  131. * Create a form we will use to allow the user to Filter this list
  132. */
  133. $filterForm = $this->createForm(MatterFilterType::class, null, [
  134. 'userHelper' => $userHelper,
  135. 'em' => $this->getDoctrine()->getManager(),
  136. ]);
  137. // Create and user a form to order this list
  138. $orderForm = $this->createForm(MatterOrderType::class);
  139. $isExpert = $user->isExpertOnly() || $user->isExpertViewerOnly();
  140. // Order A applies to all user roles, excluding the two expert user roles
  141. // Order B applies to the two expert user roles only
  142. $tabOrder = $isExpert ? 'order-b' : 'order-a';
  143. return $this->renderTurbo('Default/dashboard.html.twig', [
  144. 'eligibleProjectsToDisplayRenewalDate' => $eligibleProjects,
  145. 'filterForm' => $filterForm->createView(),
  146. 'orderForm' => $orderForm->createView(),
  147. 'systemNotification' => $systemNotification,
  148. 'analyticsEmbeddedReport' => $analyticsEmbeddedReport,
  149. 'analyticsEmbeddedReportMain' => $analyticsEmbeddedReportMain,
  150. 'recentlyViewedCount' => $recentlyViewedProjectCount,
  151. 'recentlyAcceptedCount' => $recentlyAcceptedCount,
  152. 'favouritesCount' => $favouritesCount,
  153. 'renewalsCount' => $renewalsCount,
  154. 'grossTotalProjectsCount' => $grossTotalProjectsCount,
  155. 'tabOrder' => $tabOrder,
  156. 'isExpert' => $isExpert,
  157. ]);
  158. }
  159. /**
  160. * List Matters most recently viewed
  161. *
  162. * @Template("Default/Partials/viewedTab.html.twig")
  163. *
  164. * @param RoleInvitationRepository $roleInvitationRepository
  165. * @param RoleParserService $roleParser
  166. * @param UserHelper $userHelper
  167. * @param ProjectRepository $projectRepository
  168. * @param RenewalHelperService $renewalHelperService
  169. * @param ProjectHelper $projectHelper
  170. * @param MatchExpertUser $matchExpertUserService
  171. *
  172. * @throws Exception
  173. *
  174. */
  175. public function dashboardViewedAjaxAction(
  176. UserHelper $userHelper,
  177. ProjectRepository $projectRepository,
  178. RenewalHelperService $renewalHelperService,
  179. MatchExpertUser $matchExpertUserService,
  180. ProjectHelper $projectHelper,
  181. ): JsonResponse|RedirectResponse|array {
  182. // Redirect to pending invitations page if User has pending role invitations.
  183. if ($this->userHasPendingInvitations()) {
  184. return $this->redirectWithAjaxSupport($this->pendingInvitationsUrlWithFlash());
  185. }
  186. $userHelper->setUser($this->getUser());
  187. $recentlyViewed = $this->getRecentlyViewedProjects($projectRepository, $userHelper, $projectHelper, $matchExpertUserService);
  188. $recentlyViewedProjects = $recentlyViewed['projects'];
  189. $userType = $recentlyViewed['userType'];
  190. // Return if no qualifying Projects
  191. if ($recentlyViewedProjects === []) {
  192. return [
  193. 'blurb' => 'You do not have any recently viewed matters.',
  194. ];
  195. }
  196. // All Projects that have a renewal date
  197. $eligibleProjects = $this->getEligibleRenewalProjects($recentlyViewedProjects, $renewalHelperService);
  198. return [
  199. 'projects' => $recentlyViewedProjects,
  200. 'eligibleProjectsToDisplayRenewalDate' => $eligibleProjects,
  201. 'blurb' => 'Most recently viewed matters (up to 20).',
  202. 'userType' => $userType,
  203. ];
  204. }
  205. /**
  206. * List the User's favourite Matters
  207. *
  208. * @Template("Default/Partials/favouritesTab.html.twig")
  209. *
  210. * @param Request $request
  211. * @param RenewalHelperService $renewalHelperService
  212. * @param ProjectHelper $projectHelper
  213. * @param MatchExpertUser $matchExpertUserService
  214. *
  215. * @throws Exception
  216. */
  217. public function dashboardFavouritesAjaxAction(Request $request, RenewalHelperService $renewalHelperService, ProjectHelper $projectHelper, MatchExpertUser $matchExpertUserService): JsonResponse|RedirectResponse|array
  218. {
  219. // Redirect to pending invitations page if User has pending role invitations.
  220. if ($this->userHasPendingInvitations()) {
  221. return $this->redirectWithAjaxSupport($this->pendingInvitationsUrlWithFlash());
  222. }
  223. $favouriteProjectsArray = $this->getFavouriteProjects($projectHelper, $matchExpertUserService);
  224. $favouriteProjects = $favouriteProjectsArray['projects'];
  225. $userType = $favouriteProjectsArray['userType'];
  226. // Return if no qualifying Projects
  227. if ($favouriteProjects === []) {
  228. return [
  229. 'blurb' => 'You do not have any matters added as a favourite.',
  230. ];
  231. }
  232. // All favourite Projects that have a renewal date
  233. $eligibleProjects = $this->getEligibleRenewalProjects($favouriteProjects, $renewalHelperService);
  234. $projectCount = count($favouriteProjects);
  235. $blurbText = $projectCount === 1 ? '1 Matter added as a favourite.' : sprintf('%d %s', $projectCount, 'Matters added as a favourite.');
  236. // Paginate with a specific page and limit
  237. $pagination = $this->paginator->paginate(
  238. $favouriteProjects, // the array to paginate
  239. $request->query->getInt('page', 1), // current page number, default is 1
  240. 10 // items per page
  241. );
  242. return [
  243. 'pagination' => $pagination,
  244. 'totalProjectCount' => $projectCount,
  245. 'eligibleProjectsToDisplayRenewalDate' => $eligibleProjects,
  246. 'blurb' => $blurbText,
  247. 'userType' => $userType,
  248. ];
  249. }
  250. /**
  251. * List Matters accepted in the past 30 days
  252. *
  253. * @Template("Default/Partials/invitationsTab.html.twig")
  254. *
  255. * @param Request $request
  256. * @param RoleInvitationRepository $roleInvitationRepository
  257. * @param RoleParserService $roleParser
  258. * @param RenewalHelperService $renewalHelperService
  259. * @param ProjectHelper $projectHelper
  260. * @param MatchExpertUser $matchExpertUserService
  261. *
  262. * @throws Exception
  263. */
  264. public function dashboardInvitationsAjaxAction(
  265. Request $request,
  266. RoleInvitationRepository $roleInvitationRepository,
  267. RoleParserService $roleParser,
  268. RenewalHelperService $renewalHelperService,
  269. MatchExpertUser $matchExpertUserService,
  270. ProjectHelper $projectHelper
  271. ): JsonResponse|RedirectResponse|array {
  272. // Redirect to pending invitations page if User has pending role invitations.
  273. if ($this->userHasPendingInvitations()) {
  274. return $this->redirectWithAjaxSupport($this->pendingInvitationsUrlWithFlash());
  275. }
  276. $recentlyAcceptedArray = $this->getRecentlyAcceptedProjects($roleInvitationRepository, $roleParser, $projectHelper, $matchExpertUserService);
  277. $recentlyAccepted = $recentlyAcceptedArray['projects'];
  278. $userType = $recentlyAcceptedArray['userType'];
  279. // Return if no qualifying Projects
  280. if ($recentlyAccepted === []) {
  281. return [
  282. 'blurb' => 'You have not accepted any invitations in the last 30 days.',
  283. ];
  284. }
  285. // All recently accepted Projects that have a renewal date
  286. $eligibleProjects = $this->getEligibleRenewalProjects($recentlyAccepted, $renewalHelperService);
  287. $projectCount = count($recentlyAccepted);
  288. $blurbText = $projectCount === 1 ? '1 Invitation accepted in the last 30 days.' : sprintf('%d %s', $projectCount, 'Invitations accepted in the last 30 days.');
  289. // Paginate with a specific page and limit
  290. $pagination = $this->paginator->paginate(
  291. $recentlyAccepted, // the array to paginate
  292. $request->query->getInt('page', 1), // current page number, default is 1
  293. 10 // items per page
  294. );
  295. return [
  296. 'pagination' => $pagination,
  297. 'totalProjectCount' => $projectCount,
  298. 'eligibleProjectsToDisplayRenewalDate' => $eligibleProjects,
  299. 'blurb' => $blurbText,
  300. 'userType' => $userType,
  301. ];
  302. }
  303. /**
  304. * List Matters due for renewal
  305. *
  306. * @Template("Default/Partials/renewalsTab.html.twig")
  307. *
  308. * @param Request $request
  309. * @param ProjectRepository $projectRepository
  310. * @param UserHelper $userHelper
  311. * @param ProjectHelper $projectHelper
  312. * @param MatchExpertUser $matchExpertUserService
  313. *
  314. * @return array
  315. */
  316. public function dashboardRenewalsAjaxAction(Request $request, ProjectRepository $projectRepository, UserHelper $userHelper, ProjectHelper $projectHelper, MatchExpertUser $matchExpertUserService): RedirectResponse|array
  317. {
  318. // Redirect to pending invitations page if User has pending role invitations.
  319. if ($this->userHasPendingInvitations()) {
  320. return $this->redirect($this->pendingInvitationsUrlWithFlash());
  321. }
  322. $userHelper->setUser($this->getUser());
  323. // All Projects that are due for renewal within the next 30 days
  324. $renewalProjects = $projectRepository->findRenewalsDueWithin30DaysForUser($userHelper);
  325. $projectCount = count($renewalProjects);
  326. $blurbText = $projectCount === 1 ? '1 Matter due for renewal within the next 30 days.' : sprintf('%d %s', $projectCount, 'Matters due for renewal within the next 30 days.');
  327. // Return if no qualifying Projects
  328. if (empty($renewalProjects)) {
  329. return [
  330. 'blurb' => 'There are no matters due for renewal in the next 30 days.',
  331. ];
  332. }
  333. // Paginate with a specific page and limit
  334. $pagination = $this->paginator->paginate(
  335. $renewalProjects, // the array to paginate
  336. $request->query->getInt('page', 1), // current page number, default is 1
  337. 10 // items per page
  338. );
  339. // Determine user type for displaying unread message counts (firm/expert)
  340. $userType = $matchExpertUserService->getUserType($this->getUser());
  341. // This sets the unread message counts on each Project entity
  342. $projectHelper->setUnreadMessageCount(iterator_to_array($pagination), $userType, $this->getUser());
  343. return [
  344. 'pagination' => $pagination,
  345. 'totalProjectCount' => $projectCount,
  346. 'eligibleProjectsToDisplayRenewalDate' => $renewalProjects,
  347. 'blurb' => $blurbText,
  348. 'userType' => $userType,
  349. ];
  350. }
  351. /**
  352. * @Template("Default/analytics.html.twig")
  353. *
  354. * @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
  355. *
  356. * @param AnalyticsService $analyticsService
  357. */
  358. public function analyticsAction(AnalyticsService $analyticsService): RedirectResponse|array
  359. {
  360. $analyticsEmbeddedReport = $analyticsService->generateAnalyticsEmbeddedReport($this->getUser(), AnalyticsReport::REPORT_TYPE_MAIN);
  361. if (!$analyticsEmbeddedReport instanceof AnalyticsEmbeddedReport) {
  362. $this->addFlash('warning', 'No Analytics Report for this case');
  363. return $this->redirectToRoute('infology_medbrief_dashboard');
  364. }
  365. return [
  366. 'analyticsEmbeddedReport' => $analyticsEmbeddedReport,
  367. ];
  368. }
  369. /**
  370. * This action is the click through from the email that is sent to a user asking
  371. * them to finalise their registration in order to enable their account
  372. *
  373. * @Template("Default/enableAccount.html.twig")
  374. *
  375. * @param Request $request
  376. * @param TokenStorageInterface $tokenStorage
  377. * @param EventDispatcherInterface $eventDispatcher
  378. * @param EntityManagerInterface $entityManager
  379. * @param UserPasswordEncoderInterface $userPasswordEncoder
  380. * @param UserHelper $userHelper
  381. *
  382. * @throws Exception
  383. */
  384. public function enableAccountAction(
  385. Request $request,
  386. TokenStorageInterface $tokenStorage,
  387. EventDispatcherInterface $eventDispatcher,
  388. EntityManagerInterface $entityManager,
  389. UserPasswordEncoderInterface $userPasswordEncoder,
  390. UserHelper $userHelper
  391. ): RedirectResponse|array {
  392. // if the user is logged in
  393. if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  394. // log them out
  395. $tokenStorage->setToken();
  396. $request->getSession()->invalidate();
  397. // redirect them back here
  398. return $this->redirect($request->getRequestUri());
  399. }
  400. // grab the invitation code from the URL
  401. $invitationCode = $request->query->get('i');
  402. // if none was provided, we have a problem
  403. if (empty($invitationCode)) {
  404. throw $this->createNotFoundException('Invitation could not be found.');
  405. }
  406. // try to find a matching invitation
  407. $invitation = $this->getDoctrine()->getRepository(Invitation::class)->findOneByCode($invitationCode);
  408. // if we cant find one, we have a problem
  409. if (!$invitation) {
  410. throw $this->createNotFoundException('Unable to find Invitation.');
  411. }
  412. // try to find a user who matches this invitation
  413. $matchingUser = $entityManager->getRepository(User::class)
  414. ->findOneByEmail($invitation->getEmail())
  415. ;
  416. if (!$matchingUser) {
  417. throw $this->createNotFoundException('Unable to find User.');
  418. }
  419. // if the user is already enabled, then we assume this invitation has been processed already
  420. if ($matchingUser->isEnabled()) {
  421. return $this->redirectToRoute('infology_medbrief_invitation_already_processed');
  422. }
  423. // if we get here then we have found a user who is not yet enabled and may now complete their registration
  424. $form = $this->createForm(ChangePasswordFormType::class, $matchingUser, [
  425. 'action' => $this->generateUrl('infology_medbrief_user_complete_registration', ['id' => $matchingUser->getId()]),
  426. ]);
  427. $form->handleRequest($request);
  428. if ($form->isSubmitted() && $form->isValid()) {
  429. // Accept the Medbrief policies
  430. $userHelper->acceptDeclineMedbriefPolicies($matchingUser);
  431. $encodedPassword = $userPasswordEncoder->encodePassword(
  432. $matchingUser,
  433. $form->get('plainPassword')->getData()
  434. );
  435. // Set the password
  436. $matchingUser->setPassword($encodedPassword);
  437. // set the user enabled now
  438. $matchingUser->setEnabled(true);
  439. $entityManager->flush();
  440. // log the user in
  441. $token = new PostAuthenticationGuardToken($matchingUser, 'main', $matchingUser->getRoles());
  442. $tokenStorage->setToken($token); //now the user is logged in
  443. //now dispatch the login event
  444. $eventDispatcher->dispatch(
  445. new InteractiveLoginEvent($request, $token),
  446. 'security.interactive_login'
  447. );
  448. // Determine if th user requires 2FA to be enabled (if they don't already) and store it in the session
  449. // to prevent navigating to any part of the site, except the 2FA enable page, or logout page.
  450. // @see MedBrief\MSR\EventSubscriber\Enforce2FASubscriber
  451. /** @var User $user */
  452. $user = $this->getUser();
  453. $userHelper->setUser($user);
  454. if ($userHelper->twoFactoryAuthEnabledUser() && !$user->hasMultiMfaEnabled()) {
  455. $request->getSession()->set('enforce-two-factor-authentication-detail', [
  456. '2faRequired' => true,
  457. 'targetPath' => null,
  458. ]);
  459. return $this->redirectToRoute('mfa_enable_force');
  460. }
  461. // If the user has 2FA enabled and is rate limited, redirect them to the login page.
  462. if ($user->hasMultiMfaEnabled() && $request->getSession()->has('rateLimited')) {
  463. $this->addFlash('danger', 'You have entered the incorrect authentication code too many times. Please try again in five minutes.');
  464. return $this->redirectToRoute('logout');
  465. }
  466. // If the user has 2FA enabled and their IP Address has been blacklisted, redirect them to the login page.
  467. if ($user->hasMultiMfaEnabled() && $request->getSession()->has('ipBlacklisted')) {
  468. $this->addFlash('danger', 'You have entered the incorrect authentication code too many times from this IP address. Please try again in fifteen minutes.');
  469. return $this->redirectToRoute('logout');
  470. }
  471. return $this->redirectToRoute('infology_medbrief_dashboard');
  472. }
  473. return [
  474. 'user' => $matchingUser,
  475. 'form' => $form->createView(),
  476. ];
  477. }
  478. /**
  479. * Is the user logged in?
  480. *
  481. *
  482. *
  483. * @param Request $request
  484. *
  485. * @return JsonResponse 1/0
  486. */
  487. public function isUserLoggedInAction(Request $request)
  488. {
  489. // if the user is currently logged in
  490. if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  491. return new JsonResponse(['status' => 1], 200);
  492. }
  493. return new JsonResponse(['status' => 0], 200);
  494. }
  495. /**
  496. * Refresh the session upon user wishing to remain logged in.
  497. *
  498. *
  499. *
  500. * @param Request $request
  501. *
  502. * @return JsonResponse
  503. */
  504. public function refreshSessionAction(Request $request)
  505. {
  506. // if the user is currently logged in
  507. if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  508. // Extend the session
  509. try {
  510. $cookie_lifetime = ini_get('session.cookie_lifetime');
  511. if ($cookie_lifetime === false || $cookie_lifetime === '') {
  512. throw new Exception('session.cookie_lifetime not found!');
  513. }
  514. $lifeTime = (int) $cookie_lifetime;
  515. } catch (Exception) {
  516. $lifeTime = $request->getSession()->getMetadataBag()->getLifetime();
  517. }
  518. $request->getSession()->getMetadataBag()->stampNew($lifeTime);
  519. $now = new DateTime();
  520. $expire = $request->getSession()->getMetadataBag()->getCreated() + $request->getSession()->getMetadataBag()->getLifetime();
  521. $response = new JsonResponse([
  522. 'sessionExpire' => $expire,
  523. ]);
  524. $sessionName = $this->getParameter('session.name');
  525. $sessionCookieValue = $request->cookies->get($sessionName);
  526. $sessionCookie = Cookie::create($sessionName, $sessionCookieValue, $expire);
  527. $response->headers->setCookie($sessionCookie);
  528. return $response;
  529. }
  530. return new JsonResponse([], 404);
  531. }
  532. /**
  533. * Refresh the session upon user wishing to remain logged in.
  534. *
  535. *
  536. *
  537. * @param Request $request
  538. *
  539. * @return JsonResponse
  540. */
  541. public function refreshUserSessionAction(Request $request)
  542. {
  543. // if the user is currently logged in
  544. if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  545. // Expiration time
  546. $expire = time() + (20 * 60 * 1000);
  547. return new JsonResponse([
  548. 'sessionExpire' => $expire,
  549. ]);
  550. }
  551. return new JsonResponse(['status' => 0], Response::HTTP_FORBIDDEN);
  552. }
  553. /**
  554. * Redirect the user to the login page with a message
  555. *
  556. * Used by the session timeout feature to logout inactive users while preserving
  557. * the return URL so they can resume where they left off after logging back in.
  558. *
  559. * @param Request $request
  560. */
  561. public function userInactivityLoginRedirectAction(Request $request): RedirectResponse
  562. {
  563. $this->addFlash('warning', 'You have been automatically logged out due to inactivity');
  564. // Get the return URL from the query parameter (passed from JavaScript)
  565. $returnUrl = $request->query->get('returnUrl');
  566. // Note: Uses 'logout' route (not legacy 'fos_user_security_logout' which no longer exists)
  567. $response = $this->redirectToRoute('logout');
  568. // Store return URL in a cookie that survives logout (session is cleared on logout)
  569. // The login authenticator will read this cookie and redirect after successful login
  570. if ($returnUrl) {
  571. $response->headers->setCookie(
  572. new Cookie(
  573. 'inactivity_return_url',
  574. $returnUrl,
  575. time() + 3600, // 1 hour expiry
  576. '/',
  577. null,
  578. $request->isSecure(),
  579. true, // httpOnly
  580. false,
  581. Cookie::SAMESITE_LAX
  582. )
  583. );
  584. }
  585. return $response;
  586. }
  587. /**
  588. * Helper function which determines if the user is using a deprecated browser.
  589. *
  590. *
  591. *
  592. * @param Request $request
  593. * @param DeviceDetectorService $deviceDetector
  594. *
  595. * @return bool
  596. */
  597. protected function isDeprecatedBrowser(Request $request, DeviceDetectorService $deviceDetector)
  598. {
  599. $dci = $deviceDetector->getClientInfoFromRequest($request);
  600. if (is_array($dci)) {
  601. // Check if the browser matches IE10 or lower (which we won't support in future).
  602. return $dci['type'] === 'browser' && $dci['short_name'] === 'IE' && floor($dci['version']) <= 10;
  603. }
  604. // If we didn't get an array of results, this is probably an automated action
  605. return true;
  606. }
  607. /**
  608. * Helper function to determine whether the User has any pending Role Invitations
  609. */
  610. private function userHasPendingInvitations(): bool
  611. {
  612. return $this->getUser()->hasUnsuppressedRoleInvitationsPendingApproval();
  613. }
  614. /**
  615. * Generate the Url to the role invitation listing page and
  616. * add a flash message to current session
  617. */
  618. private function pendingInvitationsUrlWithFlash(): string
  619. {
  620. $this->addFlash('info', 'You have pending invitations. Before you continue, please accept or decline all of your invitations.');
  621. return $this->generateUrl('infology_medbrief_role_invitation_list_pending');
  622. }
  623. /**
  624. * Get all Projects the User has recently viewed
  625. *
  626. * @param ProjectRepository $projectRepository
  627. * @param UserHelper $userHelper
  628. * @param ProjectHelper $projectHelper
  629. * @param int $limit
  630. * @param bool $calculateUnreadMessages
  631. * @param MatchExpertUser $matchExpertUserService
  632. *
  633. * @return array ['projects' => Project[], 'userType' => string|null]
  634. */
  635. private function getRecentlyViewedProjects(
  636. ProjectRepository $projectRepository,
  637. UserHelper $userHelper,
  638. ProjectHelper $projectHelper,
  639. MatchExpertUser $matchExpertUserService,
  640. bool $calculateUnreadMessages = true,
  641. int $limit = 20
  642. ): array {
  643. $projects = $projectRepository->findRecentlyAccessedByUser($userHelper, $limit);
  644. // if calculating unread messages, set them on each Project entity
  645. if ($calculateUnreadMessages) {
  646. // Determine user type for displaying unread message counts (firm/expert)
  647. $userType = $matchExpertUserService->getUserType($this->getUser());
  648. // This sets the unread message counts on each Project entity
  649. $projectHelper->setUnreadMessageCount(iterator_to_array($projects), $userType, $userHelper->getUser());
  650. return ['projects' => $projects, 'userType' => $userType];
  651. }
  652. return ['projects' => $projects, 'userType' => null];
  653. }
  654. /**
  655. * Get all Projects the User has recently accepted
  656. *
  657. * @param RoleInvitationRepository $roleInvitationRepository
  658. * @param RoleParserService $roleParser
  659. * @param ProjectHelper $projectHelper
  660. * @param bool $calculateUnreadMessages
  661. * @param MatchExpertUser $matchExpertUserService
  662. *
  663. * @return array ['projects' => Project[], 'userType' => string|null]
  664. */
  665. private function getRecentlyAcceptedProjects(RoleInvitationRepository $roleInvitationRepository, RoleParserService $roleParser, ProjectHelper $projectHelper, MatchExpertUser $matchExpertUserService, bool $calculateUnreadMessages = true): array
  666. {
  667. /**
  668. * Not Accessed
  669. */
  670. $recentlyAccepted = $roleInvitationRepository->findLatestProjectRoleInvitationsByUser($this->getUser());
  671. // Map Role Invitations to obtain Projects via the RoleParserService
  672. $recentlyAccepted = array_map(fn (RoleInvitation $roleInvitation)
  673. => $roleParser->parseRole($roleInvitation)->getSubject(), $recentlyAccepted);
  674. // Filter out Projects with any type of deleted or archived status
  675. $recentlyAccepted = array_filter($recentlyAccepted, fn ($project): bool => !$project->isCloseInProgressOrComplete());
  676. // if calculating unread messages, set them on each Project entity
  677. if ($calculateUnreadMessages) {
  678. // Determine user type for displaying unread message counts (firm/expert)
  679. $userType = $matchExpertUserService->getUserType($this->getUser());
  680. // This sets the unread message counts on each Project entity
  681. $projectHelper->setUnreadMessageCount(iterator_to_array($recentlyAccepted), $userType, $this->getUser());
  682. return ['projects' => $recentlyAccepted, 'userType' => $userType];
  683. }
  684. return ['projects' => $recentlyAccepted, 'userType' => null];
  685. }
  686. /**
  687. * Get all the User's favourite Projects
  688. *
  689. * @param ProjectHelper $projectHelper
  690. * @param bool $calculateUnreadMessages
  691. * @param MatchExpertUser $matchExpertUserService
  692. *
  693. * @return array ['projects' => Project[], 'userType' => string|null]
  694. */
  695. private function getFavouriteProjects(ProjectHelper $projectHelper, MatchExpertUser $matchExpertUserService, bool $calculateUnreadMessages = true): array
  696. {
  697. $favouriteProjects = $this->getUser()->getFavouriteProjects()->toArray();
  698. // Filter out deleted and archived Projects
  699. $favouriteProjects = array_filter($favouriteProjects, fn ($project): bool => !$project->isDeleteStatusComplete() && !$project->isArchiveStatusComplete());
  700. // Converts favourite projects to project_id's and removes ones where access is no longer granted
  701. foreach ($favouriteProjects as $key => $project) {
  702. if (!$this->isGranted('READ', $project)) {
  703. unset($favouriteProjects[$key]);
  704. }
  705. }
  706. // if calculating unread messages, set them on each Project entity
  707. if ($calculateUnreadMessages) {
  708. // Determine user type for displaying unread message counts (firm/expert)
  709. $userType = $matchExpertUserService->getUserType($this->getUser());
  710. // This sets the unread message counts on each Project entity
  711. $projectHelper->setUnreadMessageCount(iterator_to_array($favouriteProjects), $userType, $this->getUser());
  712. return ['projects' => $favouriteProjects, 'userType' => $userType];
  713. }
  714. // TODO: create a UserProject Entity to store the date when the Project was favourited
  715. return ['projects' => $favouriteProjects, 'userType' => null];
  716. }
  717. /**
  718. * @param Project[] $projects
  719. * @param RenewalHelperService $renewalHelperService
  720. *
  721. * @return Projects[]|[]
  722. */
  723. private function getEligibleRenewalProjects(array $projects, RenewalHelperService $renewalHelperService): array
  724. {
  725. $eligibleProjectsToDisplayRenewalDate = array_filter($projects, fn ($project): ?bool => $renewalHelperService->isMatterEligibleForRenewal($project));
  726. return $this->sortProjectsByDateOrder($eligibleProjectsToDisplayRenewalDate, 'descending');
  727. }
  728. /**
  729. * Sort an array of Projects by their Updated date in specified order.
  730. *
  731. *
  732. *
  733. * @param array $projects
  734. * @param string $order
  735. *
  736. * @return array $projects
  737. */
  738. private function sortProjectsByDateOrder(array $projects, string $order = 'descending'): array
  739. {
  740. // Use usort to sort the array using the custom comparison function
  741. if ($order === 'descending') {
  742. usort($projects, self::compareByDateDescending(...));
  743. } else {
  744. usort($projects, self::compareByDateAscending(...));
  745. }
  746. return $projects;
  747. }
  748. /**
  749. * Callback function for the comparison of two Projects by Updated date Descending.
  750. *
  751. * @param Project $project1
  752. * @param Project $project2
  753. */
  754. private function compareByDateDescending($project1, $project2): bool
  755. {
  756. $date1 = $project1->getUpdated();
  757. $date2 = $project2->getUpdated();
  758. return $date2 <=> $date1;
  759. }
  760. /**
  761. * Callback function for the comparison of two Projects by Updated date Ascending.
  762. *
  763. * @param Project $project1
  764. * @param Project $project2
  765. */
  766. private function compareByDateAscending($project1, $project2): bool
  767. {
  768. $date1 = $project1->getUpdated();
  769. $date2 = $project2->getUpdated();
  770. return $date1 <=> $date2;
  771. }
  772. }