src/Controller/DefaultController.php line 51

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