Commit 9c100641 authored by Tino Goratsch's avatar Tino Goratsch

- major refactoring and reworking of the Response handling for widgets

  --> enabled the HTTP cache facility by default
  --> include the modules via ESI when being called from the load_module Smarty function
  --> TODO: Fix the js file import logic
  --> TODO: Research a smart logic for the max-age of cacheable content
  --> TODO: HTTP-Cache invalidation
parent 621c8eda
......@@ -29,15 +29,12 @@ abstract class AbstractBootstrap implements BootstrapInterface
public function __construct($appMode)
{
$this->appMode = $appMode;
$this->initializeApplicationPath($this->appMode);
$this->initializeApplicationPath();
}
/**
* @param string $appMode
*/
protected function initializeApplicationPath($appMode)
protected function initializeApplicationPath()
{
$this->appPath = new ApplicationPath($appMode);
$this->appPath = new ApplicationPath($this->appMode);
}
/**
......
......@@ -132,6 +132,10 @@ class Bootstrap extends AbstractBootstrap
try {
$response = $this->container->get('core.application.controller_action_dispatcher')->dispatch();
if ($request->getArea() !== AreaEnum::AREA_WIDGET) {
$response->headers->set('Surrogate-Control', 'content="ESI/1.0"');
}
} catch (\ACP3\Core\Controller\Exception\ResultNotExistsException $e) {
$response = $redirect->temporary('errors/index/not_found');
} catch (\ACP3\Core\Authentication\Exception\UnauthorizedAccessException $e) {
......
......@@ -8,6 +8,7 @@ namespace ACP3\Core\Application;
use ACP3\Core\Application\Event\ControllerActionDispatcherDispatchEvent;
use ACP3\Core\Controller\ActionInterface;
use ACP3\Core\Controller\AreaEnum;
use ACP3\Core\Http\RequestInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
......@@ -69,6 +70,11 @@ class ControllerActionDispatcher
$serviceId = $this->buildControllerServiceId();
}
if ($this->request->getArea() === AreaEnum::AREA_WIDGET &&
$this->request->getServer()->get('SERVER_ADDR') !== $this->request->getSymfonyRequest()->getClientIp()) {
throw new \RuntimeException('Loading widgets from outside is not allowed!');
}
if ($this->container->has($serviceId)) {
$this->eventDispatcher->dispatch(
'core.application.controller_action_dispatcher.before_dispatch',
......
<?php
/**
* Copyright (c) 2016 by the ACP3 Developers.
* See the LICENCE file at the top-level module directory for licencing details.
*/
namespace ACP3\Core\Cache;
use ACP3\Core\Environment\ApplicationMode;
use ACP3\Core\User;
use Symfony\Component\HttpFoundation\Response;
/**
* Class CacheResponseTrait
* @package ACP3\Core\Cache
*/
trait CacheResponseTrait
{
/**
* @return User
*/
abstract protected function getUser();
/**
* @return Response
*/
abstract protected function getResponse();
/**
* @return string
*/
abstract protected function getApplicationMode();
/**
* @param int $lifetime
*/
public function setCacheResponseCacheable($lifetime = 60)
{
$response = $this->getResponse();
if ($this->getUser()->isAuthenticated() || $this->getApplicationMode() === ApplicationMode::DEVELOPMENT) {
$response->setPrivate();
$lifetime = 0;
}
$response
->setPublic()
->setMaxAge($lifetime)
->setSharedMaxAge($lifetime);
}
}
......@@ -6,6 +6,8 @@
namespace ACP3\Core\Controller;
use Symfony\Component\HttpFoundation\Response;
/**
* Interface ActionInterface
* @package ACP3\Core\Controller
......@@ -27,7 +29,8 @@ interface ActionInterface
public function get($serviceId);
/**
* @param mixed $actionResult
* @param Response|string|array $actionResult
* @return Response
*/
public function display($actionResult);
}
......@@ -7,7 +7,6 @@
namespace ACP3\Core\Controller\Context;
use ACP3\Core;
use Symfony\Component\HttpFoundation\Response;
/**
* Class FrontendContext
......@@ -31,10 +30,6 @@ class FrontendContext extends Core\Controller\Context\WidgetContext
* @var \ACP3\Core\Modules\Helper\Action
*/
private $actionHelper;
/**
* @var \Symfony\Component\HttpFoundation\Response
*/
private $response;
/**
* @param \ACP3\Core\Controller\Context\WidgetContext $context
......@@ -42,15 +37,13 @@ class FrontendContext extends Core\Controller\Context\WidgetContext
* @param \ACP3\Core\Breadcrumb\Steps $breadcrumb
* @param \ACP3\Core\Breadcrumb\Title $title
* @param \ACP3\Core\Modules\Helper\Action $actionHelper
* @param \Symfony\Component\HttpFoundation\Response $response
*/
public function __construct(
Core\Controller\Context\WidgetContext $context,
Core\Assets $assets,
Core\Breadcrumb\Steps $breadcrumb,
Core\Breadcrumb\Title $title,
Core\Modules\Helper\Action $actionHelper,
Response $response
Core\Modules\Helper\Action $actionHelper
) {
parent::__construct(
$context->getContainer(),
......@@ -64,14 +57,14 @@ class FrontendContext extends Core\Controller\Context\WidgetContext
$context->getValidator(),
$context->getView(),
$context->getConfig(),
$context->getAppPath()
$context->getAppPath(),
$context->getResponse()
);
$this->assets = $assets;
$this->breadcrumb = $breadcrumb;
$this->title = $title;
$this->actionHelper = $actionHelper;
$this->response = $response;
}
/**
......@@ -105,12 +98,4 @@ class FrontendContext extends Core\Controller\Context\WidgetContext
{
return $this->actionHelper;
}
/**
* @return Response
*/
public function getResponse()
{
return $this->response;
}
}
......@@ -11,13 +11,13 @@ use ACP3\Core\Environment\ApplicationPath;
use ACP3\Core\Http\RequestInterface;
use ACP3\Core\I18n\Translator;
use ACP3\Core\Modules;
use ACP3\Core\Router;
use ACP3\Core\RouterInterface;
use ACP3\Core\User;
use ACP3\Core\Validation\Validator;
use ACP3\Core\View;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Response;
/**
* Class WidgetContext
......@@ -73,22 +73,27 @@ class WidgetContext
* @var \ACP3\Core\Environment\ApplicationPath
*/
protected $appPath;
/**
* @var \Symfony\Component\HttpFoundation\Response
*/
private $response;
/**
* WidgetContext constructor.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
* @param \ACP3\Core\ACL $acl
* @param \ACP3\Core\User $user
* @param \ACP3\Core\I18n\Translator $translator
* @param \ACP3\Core\Modules $modules
* @param \ACP3\Core\Http\RequestInterface $request
* @param \ACP3\Core\RouterInterface $router
* @param \ACP3\Core\Validation\Validator $validator
* @param \ACP3\Core\View $view
* @param \ACP3\Core\Config $config
* @param \ACP3\Core\Environment\ApplicationPath $appPath
* @param \ACP3\Core\ACL $acl
* @param \ACP3\Core\User $user
* @param \ACP3\Core\I18n\Translator $translator
* @param \ACP3\Core\Modules $modules
* @param \ACP3\Core\Http\RequestInterface $request
* @param \ACP3\Core\RouterInterface $router
* @param \ACP3\Core\Validation\Validator $validator
* @param \ACP3\Core\View $view
* @param \ACP3\Core\Config $config
* @param \ACP3\Core\Environment\ApplicationPath $appPath
* @param Response $response
*/
public function __construct(
ContainerInterface $container,
......@@ -102,7 +107,8 @@ class WidgetContext
Validator $validator,
View $view,
Config $config,
ApplicationPath $appPath
ApplicationPath $appPath,
Response $response
) {
$this->container = $container;
$this->eventDispatcher = $eventDispatcher;
......@@ -116,6 +122,7 @@ class WidgetContext
$this->view = $view;
$this->config = $config;
$this->appPath = $appPath;
$this->response = $response;
}
/**
......@@ -213,4 +220,12 @@ class WidgetContext
{
return $this->appPath;
}
/**
* @return Response
*/
public function getResponse()
{
return $this->response;
}
}
......@@ -30,7 +30,7 @@ trait DisplayActionTrait
/**
* Outputs the requested module controller action
*
* @param mixed $actionResult
* @param Response|string|array $actionResult
* @return Response
*/
public function display($actionResult)
......@@ -47,7 +47,7 @@ trait DisplayActionTrait
$this->getResponse()->headers->set('Content-type', $this->getContentType());
$this->getResponse()->setCharset($this->getCharset());
if (!$this->getContent()) {
if (!$this->getContent() && $this->getNoOutput() === false) {
// Set the template automatically
if ($this->getTemplate() === '') {
$this->setTemplate($this->applyTemplateAutomatically());
......@@ -80,6 +80,11 @@ trait DisplayActionTrait
* @return \ACP3\Core\View
*/
abstract protected function getView();
/**
* @return boolean
*/
abstract protected function getNoOutput();
/**
* Gibt den Content-Type der anzuzeigenden Seiten zurück
......
......@@ -14,8 +14,6 @@ use ACP3\Core;
*/
abstract class FrontendAction extends Core\Controller\WidgetAction
{
use Core\Controller\DisplayActionTrait;
/**
* @var \ACP3\Core\Assets
*/
......@@ -32,10 +30,6 @@ abstract class FrontendAction extends Core\Controller\WidgetAction
* @var Core\Helpers\RedirectMessages
*/
protected $redirectMessages;
/**
* @var \Symfony\Component\HttpFoundation\Response
*/
protected $response;
/**
* @var \ACP3\Core\Modules\Helper\Action
*/
......@@ -56,7 +50,6 @@ abstract class FrontendAction extends Core\Controller\WidgetAction
$this->breadcrumb = $context->getBreadcrumb();
$this->title = $context->getTitle();
$this->actionHelper = $context->getActionHelper();
$this->response = $context->getResponse();
}
/**
......@@ -97,14 +90,6 @@ abstract class FrontendAction extends Core\Controller\WidgetAction
return parent::preDispatch();
}
/**
* @inheritdoc
*/
protected function applyTemplateAutomatically()
{
return $this->request->getModule() . '/' . ucfirst($this->request->getArea()) . '/' . $this->request->getController() . '.' . $this->request->getAction() . '.tpl';
}
protected function addCustomTemplateVarsBeforeOutput()
{
$this->view->assign('BREADCRUMB', $this->breadcrumb->getBreadcrumb());
......@@ -135,22 +120,6 @@ abstract class FrontendAction extends Core\Controller\WidgetAction
return $this;
}
/**
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function getResponse()
{
return $this->response;
}
/**
* @return \ACP3\Core\View
*/
protected function getView()
{
return $this->view;
}
/**
* @return Core\Helpers\RedirectMessages
*/
......
......@@ -7,6 +7,7 @@
namespace ACP3\Core\Controller;
use ACP3\Core;
use Symfony\Component\HttpFoundation\Response;
/**
* Class WidgetAction
......@@ -14,6 +15,8 @@ use ACP3\Core;
*/
abstract class WidgetAction implements ActionInterface
{
use Core\Controller\DisplayActionTrait;
/**
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
......@@ -62,6 +65,10 @@ abstract class WidgetAction implements ActionInterface
* @var \ACP3\Core\Environment\ApplicationPath
*/
protected $appPath;
/**
* @var Response
*/
protected $response;
/**
* Nichts ausgeben
......@@ -93,6 +100,7 @@ abstract class WidgetAction implements ActionInterface
$this->modules = $context->getModules();
$this->config = $context->getConfig();
$this->appPath = $context->getAppPath();
$this->response = $context->getResponse();
}
/**
......@@ -140,13 +148,37 @@ abstract class WidgetAction implements ActionInterface
/**
* @inheritdoc
*/
public function display($actionResult)
protected function applyTemplateAutomatically()
{
return $this->request->getModule() . '/' . ucfirst($this->request->getArea()) . '/' . $this->request->getController() . '.' . $this->request->getAction() . '.tpl';
}
protected function addCustomTemplateVarsBeforeOutput()
{
}
/**
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function getResponse()
{
return $this->response;
}
/**
* @return Core\User
*/
protected function getUser()
{
if ($this->getNoOutput() === false && $this->getTemplate() !== '') {
return $this->view->fetchTemplate($this->getTemplate());
}
return $this->user;
}
return '';
/**
* @return \ACP3\Core\View
*/
protected function getView()
{
return $this->view;
}
/**
......@@ -173,4 +205,12 @@ abstract class WidgetAction implements ActionInterface
return $this;
}
/**
* @return string
*/
protected function getApplicationMode()
{
return $this->container->getParameter('core.environment');
}
}
......@@ -51,9 +51,9 @@ class ApplicationPath
/**
* ApplicationPath constructor.
*
* @param string $environment
* @param string $applicationMode
*/
public function __construct($environment)
public function __construct($applicationMode)
{
$this->phpSelf = htmlentities($_SERVER['SCRIPT_NAME']);
$this->webRoot = substr($this->phpSelf, 0, strrpos($this->phpSelf, '/') + 1);
......@@ -61,7 +61,7 @@ class ApplicationPath
$this->classesDir = $this->appDir . 'Core/';
$this->modulesDir = $this->appDir . 'Modules/';
$this->uploadsDir = ACP3_ROOT_DIR . 'uploads/';
$this->cacheDir = ACP3_ROOT_DIR . 'cache/' . $environment . '/';
$this->cacheDir = ACP3_ROOT_DIR . 'cache/' . $applicationMode . '/';
}
/**
......
......@@ -11,6 +11,7 @@ use ACP3\Core\Modules;
class Request extends AbstractRequest
{
const ADMIN_PANEL_PATTERN = '=^acp/=';
const WIDGET_PATTERN = '=^widget/=';
/**
* @var string
......@@ -128,7 +129,16 @@ class Request extends AbstractRequest
$this->symfonyRequest->attributes->set('_area', AreaEnum::AREA_ADMIN);
// strip "acp/"
$this->query = substr($this->query, 4);
} elseif (preg_match(self::WIDGET_PATTERN, $this->query)) {
$this->symfonyRequest->attributes->set('_area', AreaEnum::AREA_WIDGET);
// strip "widget/"
$this->query = substr($this->query, 7);
} else {
if (strpos($this->query, 'frontend/') === 0) {
$this->query = substr($this->query, 9);
}
$this->symfonyRequest->attributes->set('_area', AreaEnum::AREA_FRONTEND);
// Set the user defined homepage of the website
......@@ -212,7 +222,7 @@ class Request extends AbstractRequest
$this->symfonyRequest->attributes->add(['page' => (int)substr($query[$i], 5)]);
} elseif (preg_match('/^(id_(\d+))$/', $query[$i])) { // result ID
$this->symfonyRequest->attributes->add(['id' => (int)substr($query[$i], 3)]);
} elseif (preg_match('/^(([a-z0-9-]+)_(.+))$/', $query[$i])) { // Additional URI parameters
} elseif (preg_match('/^(([a-zA-Z0-9-]+)_(.+))$/', $query[$i])) { // Additional URI parameters
$param = explode('_', $query[$i], 2);
$this->symfonyRequest->attributes->add([$param[0] => $param[1]]);
}
......
......@@ -2,8 +2,8 @@
namespace ACP3\Core\View\Renderer\Smarty\Functions;
use ACP3\Core\ACL;
use ACP3\Core\Application\ControllerActionDispatcher;
use Symfony\Component\HttpFoundation\Response;
use ACP3\Core\Environment\ApplicationMode;
use ACP3\Core\Router;
/**
* Class LoadModule
......@@ -16,20 +16,29 @@ class LoadModule extends AbstractFunction
*/
protected $acl;
/**
* @var \ACP3\Core\Application\ControllerActionDispatcher
* @var Router
*/
protected $controllerActionDispatcher;
protected $router;
/**
* @var string
*/
protected $applicationMode;
/**
* LoadModule constructor.
*
* @param \ACP3\Core\ACL $acl
* @param \ACP3\Core\Application\ControllerActionDispatcher $controllerActionDispatcher
* @param Router $router
* @param string $applicationMode
*/
public function __construct(ACL $acl, ControllerActionDispatcher $controllerActionDispatcher)
public function __construct(
ACL $acl,
Router $router,
$applicationMode)
{
$this->acl = $acl;
$this->controllerActionDispatcher = $controllerActionDispatcher;
$this->router = $router;
$this->applicationMode = $applicationMode;
}
/**
......@@ -45,19 +54,13 @@ class LoadModule extends AbstractFunction
*/
public function process(array $params, \Smarty_Internal_Template $smarty)
{
$response = '';
$pathArray = $this->convertPathToArray($params['module']);
$path = $pathArray[0] . '/' . $pathArray[1] . '/' . $pathArray[2] . '/' . $pathArray[3];
if ($this->acl->hasPermission($path) === true) {
$serviceId = strtolower($pathArray[1] . '.controller.' . $pathArray[0] . '.' . $pathArray[2] . '.' . $pathArray[3]);
$response = $this->controllerActionDispatcher->dispatch(
$serviceId,
isset($params['args']) ? $params['args'] : []
);
$arguments = isset($params['args']) ? $params['args'] : [];
if ($response instanceof Response) {
$response = $response->getContent();
}
$response = '';
if ($this->acl->hasPermission($path) === true) {
$response = $this->esiInclude($path, $arguments);
}
return $response;
......@@ -80,4 +83,26 @@ class LoadModule extends AbstractFunction
}
return $pathArray;
}
/**
* @param string $path
* @param array $arguments
* @return string
*/
protected function esiInclude($path, array $arguments)
{
$routeArguments = '';
foreach ($arguments as $key => $value) {
$routeArguments.= '/' . $key . '_' . $value;
}
$debug = '';
if ($this->applicationMode === ApplicationMode::PRODUCTION) {
$debug = ' onerror="continue"';
}
$esiTag = '<esi:include src="' . $this->router->route($path . $routeArguments, true) . '"' . $debug . ' />';
return $esiTag;
}
}
......@@ -55,7 +55,10 @@ services:
smarty.plugin.load_module:
class: ACP3\Core\View\Renderer\Smarty\Functions\LoadModule
arguments: ['@core.acl', '@core.application.controller_action_dispatcher']
arguments:
- '@core.acl'
- '@core.router'
- '%core.environment%'
tags:
- { name: core.view.extension }
......
......@@ -14,6 +14,7 @@ services:
- '@core.view'
- '@core.config'
- '@core.environment.application_path'
- '@core.http.response'
public: false
core.context.admin:
......@@ -31,7 +32,6 @@ services:
- '@core.breadcrumb'
- '@core.breadcrumb.title'
- '@core.modules.helper.action'
- '@core.http.response'
public: false
core.controller.widget:
......
......@@ -15,6 +15,8 @@ use ACP3\Modules\ACP3\Articles;
*/
class Index extends Core\Controller\WidgetAction
{
use Core\Cache\CacheResponseTrait;
/**
* @var Core\Date
*/
......@@ -49,11 +51,16 @@ class Index extends Core\Controller\WidgetAction
/**
* @param string $template
* @return array
*/
public function execute($template = '')
{
$this->view->assign('sidebar_articles', $this->articleRepository->getAll($this->date->getCurrentDateTime(), 5));
$this->setCacheResponseCacheable();
$this->setTemplate($template !== '' ? $template : 'Articles/Widget/index.index.tpl');
return [
'sidebar_articles' => $this->articleRepository->getAll($this->date->getCurrentDateTime(), 5)
];
}
}
......@@ -15,6 +15,8 @@ use ACP3\Modules\ACP3\Articles;
*/
class Single extends Core\Controller\WidgetAction
{
use Core\Cache\CacheResponseTrait;
/**
* @var Core\Date
*/
......@@ -49,13 +51,16 @@ class Single extends Core\Controller\WidgetAction
/**
* @param int $id
* @return array
*/
public function execute($id)
{
if ($this->articleRepository->resultExists((int)$id, $this->date->getCurrentDateTime()) === true) {
$this->view->assign('sidebar_article', $this->articlesCache->getCache($id));
$this->setCacheResponseCacheable();
$this->setTemplate('Articles/Widget/index.single.tpl');
return [
'sidebar_article' => $this->articlesCache->getCache($id)
];
}
}
}
......@@ -59,7 +59,7 @@ class Create extends AbstractFrontendAction
* @param string $module
* @param int $entryId
*
* @return string
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function execute($module, $entryId)
{
......@@ -87,11 +87,10 @@ class Create extends AbstractFrontendAction
$defaults['message'] = '';
}
$this->view->assign('form', array_merge($defaults, $this->request->getPost()->all()));
$this->view->assign('form_token', $this->formTokenHelper->renderFormToken());
return $this->view->fetchTemplate('Comments/Frontend/index.create.tpl');
return [
'form' => array_merge($defaults, $this->request->getPost()->all()),
'form_token' => $this->formTokenHelper->renderFormToken()
];
}
/**
......
......@@ -26,15 +26,15 @@ class Index extends AbstractFrontendAction
protected $comm