$modelFactory */ public function __construct( protected ManagerRegistry $doctrine, protected MauticFactory $factory, protected ModelFactory $modelFactory, UserHelper $userHelper, protected CoreParametersHelper $coreParametersHelper, protected EventDispatcherInterface $dispatcher, protected Translator $translator, private FlashBag $flashBag, private ?RequestStack $requestStack, protected ?CorePermissions $security ) { $this->user = $userHelper->getUser(); } protected function getCurrentRequest(): Request { $request = null !== $this->requestStack ? $this->requestStack->getCurrentRequest() : null; if (null === $request) { throw new \RuntimeException('Request is not set.'); } return $request; } /** * Check if a security level is granted. */ protected function accessGranted($level): bool { return in_array($level, $this->getPermissions()); } /** * Override this method in your controller * for easy access to the permissions. */ protected function getPermissions(): array { return []; } /** * Get a model instance from the service container. * * @param string $modelNameKey * * @return AbstractCommonModel */ protected function getModel($modelNameKey) { return $this->modelFactory->getModel($modelNameKey); } /** * Forwards the request to another controller and include the POST. * * @param string $controller The controller name (a string like BlogBundle:Post:index) * @param array $request An array of request parameters * @param array $path An array of path parameters * @param array $query An array of query parameters * * @return Response A Response instance */ public function forwardWithPost($controller, array $request = [], array $path = [], array $query = []) { $path['_controller'] = $controller; $subRequest = $this->requestStack->getCurrentRequest()->duplicate($query, $request, $path); return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST); } /** * Determines if ajax content should be returned or direct content (page refresh). * * @param array $args */ public function delegateView($args): Response { $request = $this->getCurrentRequest(); $bundle = $request->query->get('bundle'); $bundle = $bundle ? strtolower(InputHelper::alphanum($bundle)) : ''; // Used for error handling defined('MAUTIC_DELEGATE_VIEW') || define('MAUTIC_DELEGATE_VIEW', 1); if (!is_array($args)) { $args = [ 'contentTemplate' => $args, 'passthroughVars' => [ 'mauticContent' => $bundle, ], ]; } if (!isset($args['viewParameters']['currentRoute']) && isset($args['passthroughVars']['route'])) { $args['viewParameters']['currentRoute'] = $args['passthroughVars']['route']; } if (!isset($args['passthroughVars']['inBuilder']) && $inBuilder = $request->get('inBuilder')) { $args['passthroughVars']['inBuilder'] = (bool) $inBuilder; } if (!isset($args['viewParameters']['mauticContent'])) { if (isset($args['passthroughVars']['mauticContent'])) { $mauticContent = $args['passthroughVars']['mauticContent']; } else { $mauticContent = $bundle; } $args['viewParameters']['mauticContent'] = $mauticContent; } if ($request->isXmlHttpRequest() && !$request->get('ignoreAjax', false)) { return $this->ajaxAction($request, $args); } $parameters = $args['viewParameters'] ?? []; $template = $args['contentTemplate']; $code = $args['responseCode'] ?? 200; $response = new Response('', $code); if ($this->dispatcher->hasListeners(CoreEvents::VIEW_INJECT_CUSTOM_TEMPLATE)) { $event = $this->dispatcher->dispatch( new CustomTemplateEvent($request, $template, $parameters), CoreEvents::VIEW_INJECT_CUSTOM_TEMPLATE ); $template = $event->getTemplate(); $parameters = $event->getVars(); } $parameters['mauticTemplate'] = $template; $parameters['mauticTemplateVars'] = $parameters; return $this->render($template, $parameters, $response); } /** * Determines if a redirect response should be returned or a Json response directing the ajax call to force a page * refresh. * * @return JsonResponse|RedirectResponse */ public function delegateRedirect($url) { $request = $this->getCurrentRequest(); if ($request->isXmlHttpRequest()) { return new JsonResponse(['redirect' => $url]); } else { return $this->redirect($url); } } /** * Redirects URLs with trailing slashes in order to prevent 404s. * * @return RedirectResponse */ public function removeTrailingSlashAction(Request $request, TrailingSlashHelper $trailingSlashHelper) { return $this->redirect($trailingSlashHelper->getSafeRedirectUrl($request), 301); } /** * Redirects /s and /s/ to /s/dashboard. */ public function redirectSecureRootAction() { return $this->redirectToRoute('mautic_dashboard_index', [], 301); } /** * Redirects controller if not ajax or retrieves html output for ajax request. * * @param array $args [returnUrl, viewParameters, contentTemplate, passthroughVars, flashes, forwardController] * * @return Response */ public function postActionRedirect(array $args = []) { $request = $this->getCurrentRequest(); $returnUrl = array_key_exists('returnUrl', $args) ? $args['returnUrl'] : $this->generateUrl('mautic_dashboard_index'); $flashes = array_key_exists('flashes', $args) ? $args['flashes'] : []; // forward the controller by default $args['forwardController'] = (array_key_exists('forwardController', $args)) ? $args['forwardController'] : true; if (!empty($flashes)) { foreach ($flashes as $flash) { $this->addFlashMessage( $flash['msg'], !empty($flash['msgVars']) ? $flash['msgVars'] : [], !empty($flash['type']) ? $flash['type'] : 'notice', !empty($flash['domain']) ? $flash['domain'] : 'flashes' ); } } if (isset($args['passthroughVars']['closeModal'])) { $args['passthroughVars']['updateMainContent'] = true; } if (!$request->isXmlHttpRequest() || !empty($args['ignoreAjax'])) { $code = $args['responseCode'] ?? 302; return $this->redirect($returnUrl, $code); } // load by ajax return $this->ajaxAction($request, $args); } /** * Generates html for ajax request. * * @param array $args [parameters, contentTemplate, passthroughVars, forwardController] */ public function ajaxAction(Request $request, $args = []): Response { defined('MAUTIC_AJAX_VIEW') || define('MAUTIC_AJAX_VIEW', 1); $parameters = array_key_exists('viewParameters', $args) ? $args['viewParameters'] : []; $contentTemplate = array_key_exists('contentTemplate', $args) ? $args['contentTemplate'] : ''; $passthrough = array_key_exists('passthroughVars', $args) ? $args['passthroughVars'] : []; $forward = array_key_exists('forwardController', $args) ? $args['forwardController'] : false; $code = array_key_exists('responseCode', $args) ? $args['responseCode'] : 200; /* * Return json response if this is a modal */ if (!empty($passthrough['closeModal']) && empty($passthrough['updateModalContent']) && empty($passthrough['updateMainContent'])) { return new JsonResponse($passthrough); } // set the route to the returnUrl if (empty($passthrough['route']) && !empty($args['returnUrl'])) { $passthrough['route'] = $args['returnUrl']; } if (!empty($passthrough['route'])) { // Add the ajax route to the request so that the desired route is fed to plugins rather than the current request $baseUrl = $request->getBaseUrl(); $routePath = str_replace($baseUrl, '', $passthrough['route']); $ajaxRouteName = false; try { $routeParams = $this->get('router')->match($routePath); $ajaxRouteName = $routeParams['_route']; $request->attributes->set('ajaxRoute', [ '_route' => $ajaxRouteName, '_route_params' => $routeParams, ] ); } catch (\Exception) { // do nothing } // breadcrumbs may fail as it will retrieve the crumb path for currently loaded URI so we must override $request->query->set('overrideRouteUri', $passthrough['route']); if ($ajaxRouteName) { if (isset($routeParams['objectAction'])) { // action urls share same route name so tack on the action to differentiate $ajaxRouteName .= "|{$routeParams['objectAction']}"; } $request->query->set('overrideRouteName', $ajaxRouteName); } } // Ajax call so respond with json $newContent = ''; if ($contentTemplate) { if ($forward) { // the content is from another controller action so we must retrieve the response from it instead of // directly parsing the template $query = ['ignoreAjax' => true, 'request' => $request, 'subrequest' => true]; $newContentResponse = $this->forward($contentTemplate, $parameters, $query); if ($newContentResponse instanceof RedirectResponse) { $passthrough['redirect'] = $newContentResponse->getTargetUrl(); $passthrough['route'] = false; } else { $newContent = $newContentResponse->getContent(); } } else { $GLOBALS['MAUTIC_AJAX_DIRECT_RENDER'] = 1; // for error handling $newContent = $this->renderView($contentTemplate, $parameters); unset($GLOBALS['MAUTIC_AJAX_DIRECT_RENDER']); } } // there was a redirect within the controller leading to a double call of this function so just return the content // to prevent newContent from being json if ($request->get('ignoreAjax', false)) { return new Response($newContent, $code); } // render flashes $passthrough['flashes'] = $this->getFlashContent(); if (!defined('MAUTIC_INSTALLER')) { // Prevent error in case installer is loaded via dev environment $passthrough['notifications'] = $this->getNotificationContent(); } $tmpl = $parameters['tmpl'] ?? $request->get('tmpl', 'index'); if ('index' == $tmpl) { $updatedContent = []; if (!empty($newContent)) { $updatedContent['newContent'] = $newContent; } $dataArray = array_merge( $passthrough, $updatedContent ); } else { // just retrieve the content $dataArray = array_merge( $passthrough, ['newContent' => $newContent] ); } if ($newContent instanceof Response) { $response = $newContent; } else { $response = new JsonResponse($dataArray, $code); } return $response; } /** * Get's the content of error page. * * @return Response */ public function renderException(\Exception $e) { $request = $this->getCurrentRequest(); $parameters = ['exception' => $e]; $query = ['ignoreAjax' => true, 'subrequest' => true]; return $this->forwardWithPost( 'Mautic\CoreBundle\Controller\ExceptionController::showAction', $request->request->all(), $parameters, array_merge($query, $request->query->all()) ); } /** * Executes an action defined in route. * * @param string $objectAction * @param int $objectId * @param int $objectSubId * @param string $objectModel * * @return Response */ public function executeAction(Request $request, $objectAction, $objectId = 0, $objectSubId = 0, $objectModel = '') { if (method_exists($this, $objectAction.'Action')) { return $this->forward( static::class.'::'.$objectAction.'Action', array_merge( [ 'objectId' => $objectId, 'objectModel' => $objectModel, ], $request->attributes->all(), ), $request->query->all() ); } return $this->notFound(); } /** * Generates access denied message. * * @param bool $batch Flag if a batch action is being performed * @param string $msg Message that is logged * * @return JsonResponse|RedirectResponse|array * * @throws AccessDeniedHttpException */ public function accessDenied($batch = false, $msg = 'mautic.core.url.error.401') { $request = $this->getCurrentRequest(); $anonymous = $this->security->isAnonymous(); if ($anonymous || !$batch) { throw new AccessDeniedHttpException($this->translator->trans($msg, ['%url%' => $request->getRequestUri()])); } if ($batch) { return [ 'type' => 'error', 'msg' => $this->translator->trans('mautic.core.error.accessdenied', [], 'flashes'), ]; } } /** * Generate 404 not found message. * * @param string $msg * * @return Response */ public function notFound($msg = 'mautic.core.url.error.404') { $request = $this->getCurrentRequest(); $page_404 = $this->coreParametersHelper->get('404_page'); if (!empty($page_404)) { $pageModel = $this->getModel('page'); \assert($pageModel instanceof PageModel); $page = $pageModel->getEntity($page_404); if (!empty($page) && $page->getIsPublished() && !empty($page->getCustomHtml())) { $slug = $pageModel->generateSlug($page); return $this->redirectToRoute('mautic_page_public', ['slug' => $slug]); } } return $this->renderException( new NotFoundHttpException( $this->translator->trans($msg, [ '%url%' => $request->getRequestUri(), ] ) ) ); } /** * Returns a json encoded access denied error for modal windows. * * @param string $msg */ public function modalAccessDenied($msg = 'mautic.core.error.accessdenied'): JsonResponse { return new JsonResponse([ 'error' => $this->translator->trans($msg, [], 'flashes'), ]); } /** * Updates list filters, order, limit. * * @param string|null $name */ protected function setListFilters($name = null) { $request = $this->getCurrentRequest(); $session = $request->getSession(); if (empty($name)) { $name = InputHelper::clean($request->query->get('name')); } $name = 'mautic.'.$name; if (false === $request->query->has('orderby') && false === $session->has("$name.orderbydir")) { $session->set("$name.orderbydir", $this->getDefaultOrderDirection()); } if ($request->query->has('orderby')) { $orderBy = InputHelper::clean($request->query->get('orderby'), true); $dir = $session->get("$name.orderbydir", 'ASC'); $dir = $orderBy === $session->get("$name.orderby") || false == $session->has("$name.orderby") ? (('ASC' == $dir) ? 'DESC' : 'ASC') : $dir; $session->set("$name.orderby", $orderBy); $session->set("$name.orderbydir", $dir); } if ($request->query->has('limit')) { $limit = (int) $request->query->get('limit'); $session->set("$name.limit", $limit); } if ($request->query->has('filterby')) { $filter = InputHelper::clean($request->query->get('filterby'), true); $value = InputHelper::clean($request->query->get('value'), true); $filters = $session->get("$name.filters", []); if ('' == $value) { if (isset($filters[$filter])) { unset($filters[$filter]); } } else { $filters[$filter] = [ 'column' => $filter, 'expr' => 'like', 'value' => $value, 'strict' => false, ]; } $session->set("$name.filters", $filters); } } /** * Renders flashes' HTML. * * @return string */ protected function getFlashContent() { return $this->renderView('@MauticCore/Notification/flash_messages.html.twig'); } /** * Renders notification info for ajax. */ protected function getNotificationContent(Request $request = null): array { if (null === $request) { $request = $this->getCurrentRequest(); } $afterId = $request->get('mauticLastNotificationId', null); /** @var \Mautic\CoreBundle\Model\NotificationModel $model */ $model = $this->getModel('core.notification'); [$notifications, $showNewIndicator, $updateMessage] = $model->getNotificationContent($afterId, false, 200); $lastNotification = reset($notifications); return [ 'content' => ($notifications || $updateMessage) ? $this->renderView('@MauticCore/Notification/notification_messages.html.twig', [ 'notifications' => $notifications, 'updateMessage' => $updateMessage, ]) : '', 'lastId' => (!empty($lastNotification)) ? $lastNotification['id'] : $afterId, 'hasNewNotifications' => $showNewIndicator, 'updateAvailable' => (!empty($updateMessage)), ]; } /** * @param bool|true $isRead * * @deprecated Will be removed in Mautic 3.0 as unused. */ public function addNotification($message, $type = null, $isRead = true, $header = null, $iconClass = null, \DateTime $datetime = null): void { /** @var \Mautic\CoreBundle\Model\NotificationModel $notificationModel */ $notificationModel = $this->getModel('core.notification'); $notificationModel->addNotification($message, $type, $isRead, $header, $iconClass, $datetime); } /** * @param string $message * @param array $messageVars * @param string|null $level * @param string|null $domain * @param bool|null $addNotification */ public function addFlashMessage($message, $messageVars = [], $level = FlashBag::LEVEL_NOTICE, $domain = 'flashes', $addNotification = false): void { $this->flashBag->add($message, $messageVars, $level, $domain, $addNotification); } /** * @param array|\Iterator $toExport * * @return StreamedResponse */ public function exportResultsAs($toExport, $type, $filename, ExportHelper $exportHelper) { if (!in_array($type, $exportHelper->getSupportedExportTypes())) { throw new BadRequestHttpException($this->translator->trans('mautic.error.invalid.export.type', ['%type%' => $type])); } $dateFormat = $this->coreParametersHelper->get('date_format_dateonly'); $dateFormat = str_replace('--', '-', preg_replace('/[^a-zA-Z]/', '-', $dateFormat)); $filename = strtolower($filename.'_'.(new \DateTime())->format($dateFormat).'.'.$type); return $exportHelper->exportDataAs($toExport, $type, $filename); } /** * Standard function to generate an array of data via any model's "getEntities" method. * * Overwrite in your controller if required. * * @param AbstractCommonModel $model * * @return array */ protected function getDataForExport(AbstractCommonModel $model, array $args, callable $resultsCallback = null, ?int $start = 0) { $data = new DataExporterHelper(); return $data->getDataForExport($start, $model, $args, $resultsCallback); } /** * @return string */ protected function getDefaultOrderDirection() { return 'ASC'; } }