vendor/pimcore/pimcore/lib/Document/Editable/EditableHandler.php line 243

Open in your IDE?
  1. <?php
  2. /**
  3. * Pimcore
  4. *
  5. * This source file is available under two different licenses:
  6. * - GNU General Public License version 3 (GPLv3)
  7. * - Pimcore Commercial License (PCL)
  8. * Full copyright and license information is available in
  9. * LICENSE.md which is distributed with this source code.
  10. *
  11. * @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12. * @license http://www.pimcore.org/license GPLv3 and PCL
  13. */
  14. namespace Pimcore\Document\Editable;
  15. use Pimcore\Extension\Document\Areabrick\AreabrickInterface;
  16. use Pimcore\Extension\Document\Areabrick\AreabrickManagerInterface;
  17. use Pimcore\Extension\Document\Areabrick\EditableDialogBoxInterface;
  18. use Pimcore\Extension\Document\Areabrick\Exception\ConfigurationException;
  19. use Pimcore\Extension\Document\Areabrick\PreviewAwareInterface;
  20. use Pimcore\Extension\Document\Areabrick\TemplateAreabrickInterface;
  21. use Pimcore\Http\Request\Resolver\EditmodeResolver;
  22. use Pimcore\Http\RequestHelper;
  23. use Pimcore\Http\ResponseStack;
  24. use Pimcore\HttpKernel\BundleLocator\BundleLocatorInterface;
  25. use Pimcore\HttpKernel\WebPathResolver;
  26. use Pimcore\Model\Document\Editable;
  27. use Pimcore\Model\Document\Editable\Area\Info;
  28. use Pimcore\Model\Document\PageSnippet;
  29. use Psr\Log\LoggerAwareInterface;
  30. use Psr\Log\LoggerAwareTrait;
  31. use Symfony\Bridge\Twig\Extension\HttpKernelRuntime;
  32. use Symfony\Cmf\Bundle\RoutingBundle\Routing\DynamicRouter;
  33. use Symfony\Component\HttpFoundation\RequestStack;
  34. use Symfony\Component\HttpFoundation\Response;
  35. use Symfony\Component\HttpKernel\Controller\ControllerReference;
  36. use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface;
  37. use Symfony\Component\Templating\EngineInterface;
  38. use Symfony\Contracts\Translation\TranslatorInterface;
  39. /**
  40. * @internal
  41. */
  42. class EditableHandler implements LoggerAwareInterface
  43. {
  44. use LoggerAwareTrait;
  45. /**
  46. * @var AreabrickManagerInterface
  47. */
  48. protected $brickManager;
  49. /**
  50. * @var EngineInterface
  51. */
  52. protected $templating;
  53. /**
  54. * @var BundleLocatorInterface
  55. */
  56. protected $bundleLocator;
  57. /**
  58. * @var WebPathResolver
  59. */
  60. protected $webPathResolver;
  61. /**
  62. * @var RequestHelper
  63. */
  64. protected $requestHelper;
  65. /**
  66. * @var TranslatorInterface
  67. */
  68. protected $translator;
  69. /**
  70. * @var ResponseStack
  71. */
  72. protected $responseStack;
  73. /**
  74. * @var array<string, string>
  75. */
  76. protected $brickTemplateCache = [];
  77. /**
  78. * @var EditmodeResolver
  79. */
  80. protected $editmodeResolver;
  81. /**
  82. * @var HttpKernelRuntime
  83. */
  84. protected $httpKernelRuntime;
  85. /**
  86. * @var FragmentRendererInterface
  87. */
  88. protected $fragmentRenderer;
  89. /**
  90. * @var RequestStack
  91. */
  92. protected $requestStack;
  93. public const ATTRIBUTE_AREABRICK_INFO = '_pimcore_areabrick_info';
  94. /**
  95. * @param AreabrickManagerInterface $brickManager
  96. * @param EngineInterface $templating
  97. * @param BundleLocatorInterface $bundleLocator
  98. * @param WebPathResolver $webPathResolver
  99. * @param RequestHelper $requestHelper
  100. * @param TranslatorInterface $translator
  101. * @param ResponseStack $responseStack
  102. * @param EditmodeResolver $editmodeResolver
  103. * @param HttpKernelRuntime $httpKernelRuntime
  104. * @param FragmentRendererInterface $fragmentRenderer
  105. * @param RequestStack $requestStack
  106. */
  107. public function __construct(
  108. AreabrickManagerInterface $brickManager,
  109. EngineInterface $templating,
  110. BundleLocatorInterface $bundleLocator,
  111. WebPathResolver $webPathResolver,
  112. RequestHelper $requestHelper,
  113. TranslatorInterface $translator,
  114. ResponseStack $responseStack,
  115. EditmodeResolver $editmodeResolver,
  116. HttpKernelRuntime $httpKernelRuntime,
  117. FragmentRendererInterface $fragmentRenderer,
  118. RequestStack $requestStack
  119. ) {
  120. $this->brickManager = $brickManager;
  121. $this->templating = $templating;
  122. $this->bundleLocator = $bundleLocator;
  123. $this->webPathResolver = $webPathResolver;
  124. $this->requestHelper = $requestHelper;
  125. $this->translator = $translator;
  126. $this->responseStack = $responseStack;
  127. $this->editmodeResolver = $editmodeResolver;
  128. $this->httpKernelRuntime = $httpKernelRuntime;
  129. $this->fragmentRenderer = $fragmentRenderer;
  130. $this->requestStack = $requestStack;
  131. }
  132. /**
  133. * @param Editable $editable
  134. * @param AreabrickInterface|string|bool $brick
  135. *
  136. * @return bool
  137. */
  138. public function isBrickEnabled(Editable $editable, $brick)
  139. {
  140. if ($brick instanceof AreabrickInterface) {
  141. $brick = $brick->getId();
  142. }
  143. return $this->brickManager->isEnabled($brick);
  144. }
  145. /**
  146. * @param Editable\Areablock $editable
  147. * @param array $options
  148. *
  149. * @return array
  150. */
  151. public function getAvailableAreablockAreas(Editable\Areablock $editable, array $options)
  152. {
  153. $areas = [];
  154. foreach ($this->brickManager->getBricks() as $brick) {
  155. // don't show disabled bricks
  156. if (!isset($options['dontCheckEnabled']) || !$options['dontCheckEnabled']) {
  157. if (!$this->isBrickEnabled($editable, $brick)) {
  158. continue;
  159. }
  160. }
  161. if (!(empty($options['allowed']) || in_array($brick->getId(), $options['allowed']))) {
  162. continue;
  163. }
  164. $name = $brick->getName();
  165. $desc = $brick->getDescription();
  166. $icon = $brick->getIcon();
  167. $limit = $options['limits'][$brick->getId()] ?? null;
  168. $hasDialogBoxConfiguration = $brick instanceof EditableDialogBoxInterface;
  169. // autoresolve icon as <bundleName>/Resources/public/areas/<id>/icon.png or <bundleName>/public/areas/<id>/icon.png
  170. if (null === $icon) {
  171. $bundle = null;
  172. try {
  173. $bundle = $this->bundleLocator->getBundle($brick);
  174. // check if file exists
  175. $publicDir = is_dir($bundle->getPath().'/Resources/public') ? $bundle->getPath().'/Resources/public' : $bundle->getPath().'/public';
  176. $iconPath = sprintf('%s/areas/%s/icon.png', $publicDir, $brick->getId());
  177. if (file_exists($iconPath)) {
  178. // build URL to icon
  179. $icon = $this->webPathResolver->getPath($bundle, 'areas/' . $brick->getId(), 'icon.png');
  180. }
  181. } catch (\Exception $e) {
  182. $icon = '';
  183. }
  184. }
  185. $previewHtml = $brick instanceof PreviewAwareInterface
  186. ? $brick->getPreviewHtml()
  187. : null;
  188. if ($this->editmodeResolver->isEditmode()) {
  189. $name = $this->translator->trans($name);
  190. $desc = $this->translator->trans($desc);
  191. }
  192. $areas[$brick->getId()] = [
  193. 'name' => $name,
  194. 'description' => $desc,
  195. 'type' => $brick->getId(),
  196. 'icon' => $icon,
  197. 'previewHtml' => $previewHtml,
  198. 'limit' => $limit,
  199. 'needsReload' => $brick->needsReload(),
  200. 'hasDialogBoxConfiguration' => $hasDialogBoxConfiguration,
  201. ];
  202. }
  203. return $areas;
  204. }
  205. /**
  206. * @param Info $info
  207. * @param array $templateParams
  208. *
  209. * @return string
  210. */
  211. public function renderAreaFrontend(Info $info, $templateParams = []): string
  212. {
  213. $brick = $this->brickManager->getBrick($info->getId());
  214. $request = $this->requestHelper->getCurrentRequest();
  215. $brickInfoRestoreValue = $request->attributes->get(self::ATTRIBUTE_AREABRICK_INFO);
  216. $request->attributes->set(self::ATTRIBUTE_AREABRICK_INFO, $info);
  217. $info->setRequest($request);
  218. // call action
  219. $this->handleBrickActionResult($brick->action($info));
  220. $params = $info->getParams();
  221. $params['brick'] = $info;
  222. $params['info'] = $info;
  223. $params['instance'] = $brick;
  224. // check if view template exists and throw error before open tag is rendered
  225. $viewTemplate = $this->resolveBrickTemplate($brick, 'view');
  226. if (!$this->templating->exists($viewTemplate)) {
  227. $e = new ConfigurationException(sprintf(
  228. 'The view template "%s" for areabrick %s does not exist',
  229. $viewTemplate,
  230. $brick->getId()
  231. ));
  232. $this->logger->error($e->getMessage());
  233. throw $e;
  234. }
  235. // general parameters
  236. $editmode = $this->editmodeResolver->isEditmode();
  237. if (!isset($templateParams['isAreaBlock'])) {
  238. $templateParams['isAreaBlock'] = false;
  239. }
  240. // render complete areabrick
  241. // passing the engine interface is necessary otherwise rendering a
  242. // php template inside the twig template returns the content of the php file
  243. // instead of actually parsing the php template
  244. $html = $this->templating->render('@PimcoreCore/Areabrick/wrapper.html.twig', array_merge([
  245. 'brick' => $brick,
  246. 'info' => $info,
  247. 'templating' => $this->templating,
  248. 'editmode' => $editmode,
  249. 'viewTemplate' => $viewTemplate,
  250. 'viewParameters' => $params,
  251. ], $templateParams));
  252. if ($brickInfoRestoreValue === null) {
  253. $request->attributes->remove(self::ATTRIBUTE_AREABRICK_INFO);
  254. } else {
  255. $request->attributes->set(self::ATTRIBUTE_AREABRICK_INFO, $brickInfoRestoreValue);
  256. }
  257. // call post render
  258. $this->handleBrickActionResult($brick->postRenderAction($info));
  259. return $html;
  260. }
  261. /**
  262. * @param Response|null $result
  263. */
  264. protected function handleBrickActionResult($result)
  265. {
  266. // if the action result is a response object, push it onto the
  267. // response stack. this response will be used by the ResponseStackListener
  268. // and sent back to the client
  269. if ($result instanceof Response) {
  270. $this->responseStack->push($result);
  271. }
  272. }
  273. /**
  274. * Try to get the brick template from get*Template method. If method returns null and brick implements
  275. * TemplateAreabrickInterface fall back to auto-resolving the template reference. See interface for examples.
  276. *
  277. * @param AreabrickInterface $brick
  278. * @param string $type
  279. *
  280. * @return null|string
  281. */
  282. protected function resolveBrickTemplate(AreabrickInterface $brick, $type)
  283. {
  284. $cacheKey = sprintf('%s.%s', $brick->getId(), $type);
  285. if (isset($this->brickTemplateCache[$cacheKey])) {
  286. return $this->brickTemplateCache[$cacheKey];
  287. }
  288. $template = null;
  289. if ($type === 'view') {
  290. $template = $brick->getTemplate();
  291. }
  292. if (null === $template) {
  293. if ($brick instanceof TemplateAreabrickInterface) {
  294. $template = $this->buildBrickTemplateReference($brick, $type);
  295. } else {
  296. $e = new ConfigurationException(sprintf(
  297. 'Brick %s is configured to have a %s template but does not return a template path and does not implement %s',
  298. $brick->getId(),
  299. $type,
  300. TemplateAreabrickInterface::class
  301. ));
  302. $this->logger->error($e->getMessage());
  303. throw $e;
  304. }
  305. }
  306. $this->brickTemplateCache[$cacheKey] = $template;
  307. return $template;
  308. }
  309. /**
  310. * Return either bundle or global (= app/Resources) template reference
  311. *
  312. * @param TemplateAreabrickInterface $brick
  313. * @param string $type
  314. *
  315. * @return string
  316. */
  317. protected function buildBrickTemplateReference(TemplateAreabrickInterface $brick, $type)
  318. {
  319. if ($brick->getTemplateLocation() === TemplateAreabrickInterface::TEMPLATE_LOCATION_BUNDLE) {
  320. $bundle = $this->bundleLocator->getBundle($brick);
  321. $bundleName = $bundle->getName();
  322. if (str_ends_with($bundleName, 'Bundle')) {
  323. $bundleName = substr($bundleName, 0, -6);
  324. }
  325. foreach (['areas', 'Areas'] as $folderName) {
  326. $templateReference = sprintf(
  327. '@%s/%s/%s/%s.%s',
  328. $bundleName,
  329. $folderName,
  330. $brick->getId(),
  331. $type,
  332. $brick->getTemplateSuffix()
  333. );
  334. if ($this->templating->exists($templateReference)) {
  335. return $templateReference;
  336. }
  337. }
  338. // return the last reference, even we know that it doesn't exist -> let care the templating engine
  339. return $templateReference;
  340. } else {
  341. return sprintf(
  342. 'areas/%s/%s.%s',
  343. $brick->getId(),
  344. $type,
  345. $brick->getTemplateSuffix()
  346. );
  347. }
  348. }
  349. /**
  350. * @param string $controller
  351. * @param array $attributes
  352. * @param array $query
  353. *
  354. * @return string|Response
  355. */
  356. public function renderAction($controller, array $attributes = [], array $query = [])
  357. {
  358. $document = $attributes['document'] ?? null;
  359. if ($document && $document instanceof PageSnippet) {
  360. unset($attributes['document']);
  361. $attributes = $this->addDocumentAttributes($document, $attributes);
  362. }
  363. $uri = new ControllerReference($controller, $attributes, $query);
  364. if ($this->requestHelper->hasCurrentRequest()) {
  365. return $this->httpKernelRuntime->renderFragment($uri, $attributes);
  366. } else {
  367. // this case could happen when rendering on CLI, e.g. search-reindex ...
  368. $request = $this->requestHelper->createRequestWithContext();
  369. $this->requestStack->push($request);
  370. $response = $this->fragmentRenderer->render($uri, $request, $attributes);
  371. $this->requestStack->pop();
  372. return $response;
  373. }
  374. }
  375. /**
  376. * @param PageSnippet $document
  377. * @param array $attributes
  378. *
  379. * @return array
  380. */
  381. public function addDocumentAttributes(PageSnippet $document, array $attributes = [])
  382. {
  383. // The CMF dynamic router sets the 2 attributes contentDocument and contentTemplate to set
  384. // a route's document and template. Those attributes are later used by controller listeners to
  385. // determine what to render. By injecting those attributes into the sub-request we can rely on
  386. // the same rendering logic as in the routed request.
  387. $attributes[DynamicRouter::CONTENT_KEY] = $document;
  388. if ($document->getTemplate()) {
  389. $attributes[DynamicRouter::CONTENT_TEMPLATE] = $document->getTemplate();
  390. }
  391. if ($language = $document->getProperty('language')) {
  392. $attributes['_locale'] = $language;
  393. }
  394. return $attributes;
  395. }
  396. }