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

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. * @deprecated Will be removed in Pimcore 11
  134. *
  135. * @param Editable $editable
  136. * @param AreabrickInterface|string|bool $brick
  137. *
  138. * @return bool
  139. */
  140. public function isBrickEnabled(Editable $editable, $brick)
  141. {
  142. if ($brick instanceof AreabrickInterface) {
  143. $brick = $brick->getId();
  144. }
  145. return $this->brickManager->isEnabled($brick);
  146. }
  147. /**
  148. * @param Editable\Areablock $editable
  149. * @param array $options
  150. *
  151. * @return array
  152. */
  153. public function getAvailableAreablockAreas(Editable\Areablock $editable, array $options)
  154. {
  155. $areas = [];
  156. foreach ($this->brickManager->getBricks() as $brick) {
  157. // don't show disabled bricks
  158. if (!isset($options['dontCheckEnabled']) || !$options['dontCheckEnabled']) {
  159. if (!$this->isBrickEnabled($editable, $brick)) {
  160. continue;
  161. }
  162. }
  163. if (!(empty($options['allowed']) || in_array($brick->getId(), $options['allowed']))) {
  164. continue;
  165. }
  166. $name = $brick->getName();
  167. $desc = $brick->getDescription();
  168. $icon = $brick->getIcon();
  169. $limit = $options['limits'][$brick->getId()] ?? null;
  170. $hasDialogBoxConfiguration = $brick instanceof EditableDialogBoxInterface;
  171. // autoresolve icon as <bundleName>/Resources/public/areas/<id>/icon.png or <bundleName>/public/areas/<id>/icon.png
  172. if (null === $icon) {
  173. $bundle = null;
  174. try {
  175. $bundle = $this->bundleLocator->getBundle($brick);
  176. // check if file exists
  177. $publicDir = is_dir($bundle->getPath().'/Resources/public') ? $bundle->getPath().'/Resources/public' : $bundle->getPath().'/public';
  178. $iconPath = sprintf('%s/areas/%s/icon.png', $publicDir, $brick->getId());
  179. if (file_exists($iconPath)) {
  180. // build URL to icon
  181. $icon = $this->webPathResolver->getPath($bundle, 'areas/' . $brick->getId(), 'icon.png');
  182. }
  183. } catch (\Exception $e) {
  184. $icon = '';
  185. }
  186. }
  187. $previewHtml = $brick instanceof PreviewAwareInterface
  188. ? $brick->getPreviewHtml()
  189. : null;
  190. if ($this->editmodeResolver->isEditmode()) {
  191. $name = $this->translator->trans($name);
  192. $desc = $this->translator->trans($desc);
  193. }
  194. $areas[$brick->getId()] = [
  195. 'name' => $name,
  196. 'description' => $desc,
  197. 'type' => $brick->getId(),
  198. 'icon' => $icon,
  199. 'previewHtml' => $previewHtml,
  200. 'limit' => $limit,
  201. 'needsReload' => $brick->needsReload(),
  202. 'hasDialogBoxConfiguration' => $hasDialogBoxConfiguration,
  203. ];
  204. }
  205. return $areas;
  206. }
  207. /**
  208. * @param Info $info
  209. * @param array $templateParams
  210. *
  211. * @return string
  212. */
  213. public function renderAreaFrontend(Info $info, $templateParams = []): string
  214. {
  215. $brick = $this->brickManager->getBrick($info->getId());
  216. $request = $this->requestHelper->getCurrentRequest();
  217. $brickInfoRestoreValue = $request->attributes->get(self::ATTRIBUTE_AREABRICK_INFO);
  218. $request->attributes->set(self::ATTRIBUTE_AREABRICK_INFO, $info);
  219. $info->setRequest($request);
  220. // call action
  221. $this->handleBrickActionResult($brick->action($info));
  222. $params = $info->getParams();
  223. $params['brick'] = $info;
  224. $params['info'] = $info;
  225. $params['instance'] = $brick;
  226. // check if view template exists and throw error before open tag is rendered
  227. $viewTemplate = $this->resolveBrickTemplate($brick, 'view');
  228. if (!$this->templating->exists($viewTemplate)) {
  229. $e = new ConfigurationException(sprintf(
  230. 'The view template "%s" for areabrick %s does not exist',
  231. $viewTemplate,
  232. $brick->getId()
  233. ));
  234. $this->logger->error($e->getMessage());
  235. throw $e;
  236. }
  237. // general parameters
  238. $editmode = $this->editmodeResolver->isEditmode();
  239. if (!isset($templateParams['isAreaBlock'])) {
  240. $templateParams['isAreaBlock'] = false;
  241. }
  242. // render complete areabrick
  243. // passing the engine interface is necessary otherwise rendering a
  244. // php template inside the twig template returns the content of the php file
  245. // instead of actually parsing the php template
  246. $html = $this->templating->render('@PimcoreCore/Areabrick/wrapper.html.twig', array_merge([
  247. 'brick' => $brick,
  248. 'info' => $info,
  249. 'templating' => $this->templating,
  250. 'editmode' => $editmode,
  251. 'viewTemplate' => $viewTemplate,
  252. 'viewParameters' => $params,
  253. ], $templateParams));
  254. if ($brickInfoRestoreValue === null) {
  255. $request->attributes->remove(self::ATTRIBUTE_AREABRICK_INFO);
  256. } else {
  257. $request->attributes->set(self::ATTRIBUTE_AREABRICK_INFO, $brickInfoRestoreValue);
  258. }
  259. // call post render
  260. $this->handleBrickActionResult($brick->postRenderAction($info));
  261. return $html;
  262. }
  263. /**
  264. * @param Response|null $result
  265. */
  266. protected function handleBrickActionResult($result)
  267. {
  268. // if the action result is a response object, push it onto the
  269. // response stack. this response will be used by the ResponseStackListener
  270. // and sent back to the client
  271. if ($result instanceof Response) {
  272. $this->responseStack->push($result);
  273. }
  274. }
  275. /**
  276. * Try to get the brick template from get*Template method. If method returns null and brick implements
  277. * TemplateAreabrickInterface fall back to auto-resolving the template reference. See interface for examples.
  278. *
  279. * @param AreabrickInterface $brick
  280. * @param string $type
  281. *
  282. * @return null|string
  283. */
  284. protected function resolveBrickTemplate(AreabrickInterface $brick, $type)
  285. {
  286. $cacheKey = sprintf('%s.%s', $brick->getId(), $type);
  287. if (isset($this->brickTemplateCache[$cacheKey])) {
  288. return $this->brickTemplateCache[$cacheKey];
  289. }
  290. $template = null;
  291. if ($type === 'view') {
  292. $template = $brick->getTemplate();
  293. }
  294. if (null === $template) {
  295. if ($brick instanceof TemplateAreabrickInterface) {
  296. $template = $this->buildBrickTemplateReference($brick, $type);
  297. } else {
  298. $e = new ConfigurationException(sprintf(
  299. 'Brick %s is configured to have a %s template but does not return a template path and does not implement %s',
  300. $brick->getId(),
  301. $type,
  302. TemplateAreabrickInterface::class
  303. ));
  304. $this->logger->error($e->getMessage());
  305. throw $e;
  306. }
  307. }
  308. $this->brickTemplateCache[$cacheKey] = $template;
  309. return $template;
  310. }
  311. /**
  312. * Return either bundle or global (= app/Resources) template reference
  313. *
  314. * @param TemplateAreabrickInterface $brick
  315. * @param string $type
  316. *
  317. * @return string
  318. */
  319. protected function buildBrickTemplateReference(TemplateAreabrickInterface $brick, $type)
  320. {
  321. if ($brick->getTemplateLocation() === TemplateAreabrickInterface::TEMPLATE_LOCATION_BUNDLE) {
  322. $bundle = $this->bundleLocator->getBundle($brick);
  323. $bundleName = $bundle->getName();
  324. if (str_ends_with($bundleName, 'Bundle')) {
  325. $bundleName = substr($bundleName, 0, -6);
  326. }
  327. foreach (['areas', 'Areas'] as $folderName) {
  328. $templateReference = sprintf(
  329. '@%s/%s/%s/%s.%s',
  330. $bundleName,
  331. $folderName,
  332. $brick->getId(),
  333. $type,
  334. $brick->getTemplateSuffix()
  335. );
  336. if ($this->templating->exists($templateReference)) {
  337. return $templateReference;
  338. }
  339. }
  340. // return the last reference, even we know that it doesn't exist -> let care the templating engine
  341. return $templateReference;
  342. } else {
  343. return sprintf(
  344. 'areas/%s/%s.%s',
  345. $brick->getId(),
  346. $type,
  347. $brick->getTemplateSuffix()
  348. );
  349. }
  350. }
  351. /**
  352. * @param string $controller
  353. * @param array $attributes
  354. * @param array $query
  355. *
  356. * @return string|Response
  357. */
  358. public function renderAction($controller, array $attributes = [], array $query = [])
  359. {
  360. $document = $attributes['document'] ?? null;
  361. if ($document && $document instanceof PageSnippet) {
  362. unset($attributes['document']);
  363. $attributes = $this->addDocumentAttributes($document, $attributes);
  364. }
  365. $uri = new ControllerReference($controller, $attributes, $query);
  366. if ($this->requestHelper->hasCurrentRequest()) {
  367. return $this->httpKernelRuntime->renderFragment($uri, $attributes);
  368. } else {
  369. // this case could happen when rendering on CLI, e.g. search-reindex ...
  370. $request = $this->requestHelper->createRequestWithContext();
  371. $this->requestStack->push($request);
  372. $response = $this->fragmentRenderer->render($uri, $request, $attributes);
  373. $this->requestStack->pop();
  374. return $response;
  375. }
  376. }
  377. /**
  378. * @param PageSnippet $document
  379. * @param array $attributes
  380. *
  381. * @return array
  382. */
  383. public function addDocumentAttributes(PageSnippet $document, array $attributes = [])
  384. {
  385. // The CMF dynamic router sets the 2 attributes contentDocument and contentTemplate to set
  386. // a route's document and template. Those attributes are later used by controller listeners to
  387. // determine what to render. By injecting those attributes into the sub-request we can rely on
  388. // the same rendering logic as in the routed request.
  389. $attributes[DynamicRouter::CONTENT_KEY] = $document;
  390. if ($document->getTemplate()) {
  391. $attributes[DynamicRouter::CONTENT_TEMPLATE] = $document->getTemplate();
  392. }
  393. if ($language = $document->getProperty('language')) {
  394. $attributes['_locale'] = $language;
  395. }
  396. return $attributes;
  397. }
  398. }