Spaces:
No application file
No application file
namespace Mautic\CoreBundle\Controller; | |
use Doctrine\Persistence\ManagerRegistry; | |
use Mautic\CoreBundle\CoreEvents; | |
use Mautic\CoreBundle\Event\CustomTemplateEvent; | |
use Mautic\CoreBundle\Factory\MauticFactory; | |
use Mautic\CoreBundle\Factory\ModelFactory; | |
use Mautic\CoreBundle\Helper\CoreParametersHelper; | |
use Mautic\CoreBundle\Helper\DataExporterHelper; | |
use Mautic\CoreBundle\Helper\ExportHelper; | |
use Mautic\CoreBundle\Helper\InputHelper; | |
use Mautic\CoreBundle\Helper\TrailingSlashHelper; | |
use Mautic\CoreBundle\Helper\UserHelper; | |
use Mautic\CoreBundle\Model\AbstractCommonModel; | |
use Mautic\CoreBundle\Security\Permissions\CorePermissions; | |
use Mautic\CoreBundle\Service\FlashBag; | |
use Mautic\CoreBundle\Translation\Translator; | |
use Mautic\PageBundle\Model\PageModel; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\EventDispatcher\EventDispatcherInterface; | |
use Symfony\Component\HttpFoundation\JsonResponse; | |
use Symfony\Component\HttpFoundation\RedirectResponse; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\RequestStack; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\HttpFoundation\StreamedResponse; | |
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; | |
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | |
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; | |
use Symfony\Component\HttpKernel\HttpKernelInterface; | |
class CommonController extends AbstractController implements MauticController | |
{ | |
use FormThemeTrait; | |
protected ?\Mautic\UserBundle\Entity\User $user; | |
/** | |
* @param ModelFactory<object> $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<object> | |
*/ | |
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<mixed> $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<object> $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'; | |
} | |
} | |