vendor/contao/core-bundle/src/Resources/contao/classes/DataContainer.php line 1651

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of Contao.
  4. *
  5. * (c) Leo Feyer
  6. *
  7. * @license LGPL-3.0-or-later
  8. */
  9. namespace Contao;
  10. use Contao\CoreBundle\Exception\AccessDeniedException;
  11. use Contao\CoreBundle\Exception\ResponseException;
  12. use Contao\CoreBundle\Picker\DcaPickerProviderInterface;
  13. use Contao\CoreBundle\Picker\PickerInterface;
  14. use Contao\CoreBundle\Security\ContaoCorePermissions;
  15. use Contao\Image\ResizeConfiguration;
  16. use Imagine\Gd\Imagine;
  17. use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
  18. /**
  19. * Provide methods to handle data container arrays.
  20. *
  21. * @property string|integer $id
  22. * @property string $table
  23. * @property mixed $value
  24. * @property string $field
  25. * @property string $inputName
  26. * @property string $palette
  27. * @property object|null $activeRecord
  28. * @property array $rootIds
  29. */
  30. abstract class DataContainer extends Backend
  31. {
  32. /**
  33. * Records are not sorted
  34. */
  35. public const MODE_UNSORTED = 0;
  36. /**
  37. * Records are sorted by a fixed field
  38. */
  39. public const MODE_SORTED = 1;
  40. /**
  41. * Records are sorted by a switchable field
  42. */
  43. public const MODE_SORTABLE = 2;
  44. /**
  45. * Records are sorted by the parent table
  46. */
  47. public const MODE_SORTED_PARENT = 3;
  48. /**
  49. * Displays the child records of a parent record (see content elements)
  50. */
  51. public const MODE_PARENT = 4;
  52. /**
  53. * Records are displayed as tree (see site structure)
  54. */
  55. public const MODE_TREE = 5;
  56. /**
  57. * Displays the child records within a tree structure (see articles module)
  58. */
  59. public const MODE_TREE_EXTENDED = 6;
  60. /**
  61. * Sort by initial letter ascending
  62. */
  63. public const SORT_INITIAL_LETTER_ASC = 1;
  64. /**
  65. * Sort by initial letter descending
  66. */
  67. public const SORT_INITIAL_LETTER_DESC = 2;
  68. /**
  69. * Sort by initial two letters ascending
  70. */
  71. public const SORT_INITIAL_LETTERS_ASC = 3;
  72. /**
  73. * Sort by initial two letters descending
  74. */
  75. public const SORT_INITIAL_LETTERS_DESC = 4;
  76. /**
  77. * Sort by day ascending
  78. */
  79. public const SORT_DAY_ASC = 5;
  80. /**
  81. * Sort by day descending
  82. */
  83. public const SORT_DAY_DESC = 6;
  84. /**
  85. * Sort by month ascending
  86. */
  87. public const SORT_MONTH_ASC = 7;
  88. /**
  89. * Sort by month descending
  90. */
  91. public const SORT_MONTH_DESC = 8;
  92. /**
  93. * Sort by year ascending
  94. */
  95. public const SORT_YEAR_ASC = 9;
  96. /**
  97. * Sort by year descending
  98. */
  99. public const SORT_YEAR_DESC = 10;
  100. /**
  101. * Sort ascending
  102. */
  103. public const SORT_ASC = 11;
  104. /**
  105. * Sort descending
  106. */
  107. public const SORT_DESC = 12;
  108. /**
  109. * Current ID
  110. * @var integer|string
  111. */
  112. protected $intId;
  113. /**
  114. * Name of the current table
  115. * @var string
  116. */
  117. protected $strTable;
  118. /**
  119. * Name of the current field
  120. * @var string
  121. */
  122. protected $strField;
  123. /**
  124. * Name attribute of the current input field
  125. * @var string
  126. */
  127. protected $strInputName;
  128. /**
  129. * Value of the current field
  130. * @var mixed
  131. */
  132. protected $varValue;
  133. /**
  134. * Name of the current palette
  135. * @var string
  136. */
  137. protected $strPalette;
  138. /**
  139. * IDs of all root records (permissions)
  140. * @var array
  141. */
  142. protected $root = array();
  143. /**
  144. * IDs of children of root records (permissions)
  145. * @var array
  146. */
  147. protected $rootChildren = array();
  148. /**
  149. * IDs of visible parents of the root records
  150. * @var array
  151. */
  152. protected $visibleRootTrails = array();
  153. /**
  154. * If pasting at root level is allowed (permissions)
  155. * @var bool
  156. */
  157. protected $rootPaste = false;
  158. /**
  159. * WHERE clause of the database query
  160. * @var array
  161. */
  162. protected $procedure = array();
  163. /**
  164. * Values for the WHERE clause of the database query
  165. * @var array
  166. */
  167. protected $values = array();
  168. /**
  169. * Form attribute "onsubmit"
  170. * @var array
  171. */
  172. protected $onsubmit = array();
  173. /**
  174. * Reload the page after the form has been submitted
  175. * @var boolean
  176. */
  177. protected $noReload = false;
  178. /**
  179. * Active record
  180. * @var Model|FilesModel
  181. */
  182. protected $objActiveRecord;
  183. /**
  184. * True if one of the form fields is uploadable
  185. * @var boolean
  186. */
  187. protected $blnUploadable = false;
  188. /**
  189. * DCA Picker instance
  190. * @var PickerInterface
  191. */
  192. protected $objPicker;
  193. /**
  194. * Callback to convert DCA value to picker value
  195. * @var callable
  196. */
  197. protected $objPickerCallback;
  198. /**
  199. * The picker value
  200. * @var array
  201. */
  202. protected $arrPickerValue = array();
  203. /**
  204. * The picker field type
  205. * @var string
  206. */
  207. protected $strPickerFieldType;
  208. /**
  209. * True if a new version has to be created
  210. * @var boolean
  211. */
  212. protected $blnCreateNewVersion = false;
  213. /**
  214. * Set an object property
  215. *
  216. * @param string $strKey
  217. * @param mixed $varValue
  218. */
  219. public function __set($strKey, $varValue)
  220. {
  221. switch ($strKey)
  222. {
  223. case 'activeRecord':
  224. $this->objActiveRecord = $varValue;
  225. break;
  226. case 'createNewVersion':
  227. $this->blnCreateNewVersion = (bool) $varValue;
  228. break;
  229. case 'id':
  230. $this->intId = $varValue;
  231. break;
  232. case 'field':
  233. $this->strField = $varValue;
  234. break;
  235. case 'inputName':
  236. $this->strInputName = $varValue;
  237. break;
  238. default:
  239. $this->$strKey = $varValue; // backwards compatibility
  240. break;
  241. }
  242. }
  243. /**
  244. * Return an object property
  245. *
  246. * @param string $strKey
  247. *
  248. * @return mixed
  249. */
  250. public function __get($strKey)
  251. {
  252. switch ($strKey)
  253. {
  254. case 'id':
  255. return $this->intId;
  256. case 'table':
  257. return $this->strTable;
  258. case 'value':
  259. return $this->varValue;
  260. case 'field':
  261. return $this->strField;
  262. case 'inputName':
  263. return $this->strInputName;
  264. case 'palette':
  265. return $this->strPalette;
  266. case 'activeRecord':
  267. return $this->objActiveRecord;
  268. case 'createNewVersion':
  269. return $this->blnCreateNewVersion;
  270. // Forward compatibility with Contao 5.0
  271. case 'currentPid':
  272. return ((int) (\defined('CURRENT_ID') ? CURRENT_ID : 0)) ?: null;
  273. }
  274. return parent::__get($strKey);
  275. }
  276. /**
  277. * Render a row of a box and return it as HTML string
  278. *
  279. * @param string|array|null $strPalette
  280. *
  281. * @return string
  282. *
  283. * @throws AccessDeniedException
  284. * @throws \Exception
  285. */
  286. protected function row($strPalette=null)
  287. {
  288. $arrData = $GLOBALS['TL_DCA'][$this->strTable]['fields'][$this->strField] ?? array();
  289. // Check if the field is excluded
  290. if ($arrData['exclude'] ?? null)
  291. {
  292. throw new AccessDeniedException('Field "' . $this->strTable . '.' . $this->strField . '" is excluded from being edited.');
  293. }
  294. $xlabel = '';
  295. // Toggle line wrap (textarea)
  296. if (($arrData['inputType'] ?? null) == 'textarea' && !isset($arrData['eval']['rte']))
  297. {
  298. $xlabel .= ' ' . Image::getHtml('wrap.svg', $GLOBALS['TL_LANG']['MSC']['wordWrap'], 'title="' . StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['wordWrap']) . '" class="toggleWrap" onclick="Backend.toggleWrap(\'ctrl_' . $this->strInputName . '\')"');
  299. }
  300. // Add the help wizard
  301. if ($arrData['eval']['helpwizard'] ?? null)
  302. {
  303. $xlabel .= ' <a href="' . StringUtil::specialcharsUrl(System::getContainer()->get('router')->generate('contao_backend_help', array('table' => $this->strTable, 'field' => $this->strField))) . '" title="' . StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['helpWizard']) . '" onclick="Backend.openModalIframe({\'title\':\'' . StringUtil::specialchars(str_replace("'", "\\'", $arrData['label'][0] ?? '')) . '\',\'url\':this.href});return false">' . Image::getHtml('about.svg', $GLOBALS['TL_LANG']['MSC']['helpWizard']) . '</a>';
  304. }
  305. // Add a custom xlabel
  306. if (\is_array($arrData['xlabel'] ?? null))
  307. {
  308. foreach ($arrData['xlabel'] as $callback)
  309. {
  310. if (\is_array($callback))
  311. {
  312. $this->import($callback[0]);
  313. $xlabel .= $this->{$callback[0]}->{$callback[1]}($this);
  314. }
  315. elseif (\is_callable($callback))
  316. {
  317. $xlabel .= $callback($this);
  318. }
  319. }
  320. }
  321. // Input field callback
  322. if (\is_array($arrData['input_field_callback'] ?? null))
  323. {
  324. $this->import($arrData['input_field_callback'][0]);
  325. return $this->{$arrData['input_field_callback'][0]}->{$arrData['input_field_callback'][1]}($this, $xlabel);
  326. }
  327. if (\is_callable($arrData['input_field_callback'] ?? null))
  328. {
  329. return $arrData['input_field_callback']($this, $xlabel);
  330. }
  331. $strClass = $GLOBALS['BE_FFL'][($arrData['inputType'] ?? null)] ?? null;
  332. // Return if the widget class does not exist
  333. if (!class_exists($strClass))
  334. {
  335. return '';
  336. }
  337. $arrData['eval']['required'] = false;
  338. if ($arrData['eval']['mandatory'] ?? null)
  339. {
  340. if (\is_array($this->varValue))
  341. {
  342. if (empty($this->varValue))
  343. {
  344. $arrData['eval']['required'] = true;
  345. }
  346. }
  347. elseif ('' === (string) $this->varValue)
  348. {
  349. $arrData['eval']['required'] = true;
  350. }
  351. }
  352. // Convert insert tags in src attributes (see #5965)
  353. if (isset($arrData['eval']['rte']) && strncmp($arrData['eval']['rte'], 'tiny', 4) === 0 && \is_string($this->varValue))
  354. {
  355. $this->varValue = StringUtil::insertTagToSrc($this->varValue);
  356. }
  357. // Use raw request if set globally but allow opting out setting useRawRequestData to false explicitly
  358. $useRawGlobally = isset($GLOBALS['TL_DCA'][$this->strTable]['config']['useRawRequestData']) && $GLOBALS['TL_DCA'][$this->strTable]['config']['useRawRequestData'] === true;
  359. $notRawForField = isset($arrData['eval']['useRawRequestData']) && $arrData['eval']['useRawRequestData'] === false;
  360. if ($useRawGlobally && !$notRawForField)
  361. {
  362. $arrData['eval']['useRawRequestData'] = true;
  363. }
  364. /** @var Widget $objWidget */
  365. $objWidget = new $strClass($strClass::getAttributesFromDca($arrData, $this->strInputName, $this->varValue, $this->strField, $this->strTable, $this));
  366. $objWidget->xlabel = $xlabel;
  367. $objWidget->currentRecord = $this->intId;
  368. // Validate the field
  369. if (Input::post('FORM_SUBMIT') == $this->strTable)
  370. {
  371. $suffix = $this->getFormFieldSuffix();
  372. $key = (Input::get('act') == 'editAll') ? 'FORM_FIELDS_' . $suffix : 'FORM_FIELDS';
  373. // Calculate the current palette
  374. $postPaletteFields = implode(',', Input::post($key));
  375. $postPaletteFields = array_unique(StringUtil::trimsplit('[,;]', $postPaletteFields));
  376. // Compile the palette if there is none
  377. if ($strPalette === null)
  378. {
  379. $newPaletteFields = StringUtil::trimsplit('[,;]', $this->getPalette());
  380. }
  381. else
  382. {
  383. // Use the given palette ($strPalette is an array in editAll mode)
  384. $newPaletteFields = \is_array($strPalette) ? $strPalette : StringUtil::trimsplit('[,;]', $strPalette);
  385. // Recompile the palette if the current field is a selector field and the value has changed
  386. if (isset($GLOBALS['TL_DCA'][$this->strTable]['palettes']['__selector__']) && $this->varValue != Input::post($this->strInputName) && \in_array($this->strField, $GLOBALS['TL_DCA'][$this->strTable]['palettes']['__selector__']))
  387. {
  388. $newPaletteFields = StringUtil::trimsplit('[,;]', $this->getPalette());
  389. }
  390. }
  391. // Adjust the names in editAll mode
  392. if (Input::get('act') == 'editAll')
  393. {
  394. foreach ($newPaletteFields as $k=>$v)
  395. {
  396. $newPaletteFields[$k] = $v . '_' . $suffix;
  397. }
  398. }
  399. $paletteFields = array_intersect($postPaletteFields, $newPaletteFields);
  400. // Deprecated since Contao 4.2, to be removed in Contao 5.0
  401. if (!isset($_POST[$this->strInputName]) && \in_array($this->strInputName, $paletteFields))
  402. {
  403. trigger_deprecation('contao/core-bundle', '4.2', 'Using $_POST[\'FORM_FIELDS\'] has been deprecated and will no longer work in Contao 5.0. Make sure to always submit at least an empty string in your widget.');
  404. }
  405. // Validate and save the field
  406. if ($objWidget->submitInput() && (\in_array($this->strInputName, $paletteFields) || Input::get('act') == 'overrideAll'))
  407. {
  408. $objWidget->validate();
  409. if ($objWidget->hasErrors())
  410. {
  411. // Skip mandatory fields on auto-submit (see #4077)
  412. if (!$objWidget->mandatory || $objWidget->value || Input::post('SUBMIT_TYPE') != 'auto')
  413. {
  414. $this->noReload = true;
  415. }
  416. }
  417. // The return value of submitInput() might have changed, therefore check it again here (see #2383)
  418. elseif ($objWidget->submitInput())
  419. {
  420. $varValue = $objWidget->value;
  421. // Sort array by key (fix for JavaScript wizards)
  422. if (\is_array($varValue))
  423. {
  424. ksort($varValue);
  425. $varValue = serialize($varValue);
  426. }
  427. // Convert file paths in src attributes (see #5965)
  428. if ($varValue && isset($arrData['eval']['rte']) && strncmp($arrData['eval']['rte'], 'tiny', 4) === 0)
  429. {
  430. $varValue = StringUtil::srcToInsertTag($varValue);
  431. }
  432. // Save the current value
  433. try
  434. {
  435. $this->save($varValue);
  436. // Confirm password changes
  437. if ($objWidget instanceof Password)
  438. {
  439. Message::addConfirmation($GLOBALS['TL_LANG']['MSC']['pw_changed']);
  440. }
  441. }
  442. catch (ResponseException $e)
  443. {
  444. throw $e;
  445. }
  446. catch (\Exception $e)
  447. {
  448. $this->noReload = true;
  449. $objWidget->addError($e->getMessage());
  450. }
  451. }
  452. }
  453. }
  454. $wizard = '';
  455. $strHelpClass = '';
  456. // Date picker
  457. if ($arrData['eval']['datepicker'] ?? null)
  458. {
  459. $rgxp = $arrData['eval']['rgxp'] ?? 'date';
  460. $format = Date::formatToJs(Config::get($rgxp . 'Format'));
  461. switch ($rgxp)
  462. {
  463. case 'datim':
  464. $time = ",\n timePicker: true";
  465. break;
  466. case 'time':
  467. $time = ",\n pickOnly: \"time\"";
  468. break;
  469. default:
  470. $time = '';
  471. break;
  472. }
  473. $strOnSelect = '';
  474. // Trigger the auto-submit function (see #8603)
  475. if ($arrData['eval']['submitOnChange'] ?? null)
  476. {
  477. $strOnSelect = ",\n onSelect: function() { Backend.autoSubmit(\"" . $this->strTable . "\"); }";
  478. }
  479. $wizard .= ' ' . Image::getHtml('assets/datepicker/images/icon.svg', '', 'title="' . StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['datepicker']) . '" id="toggle_' . $objWidget->id . '" style="cursor:pointer"') . '
  480. <script>
  481. window.addEvent("domready", function() {
  482. new Picker.Date($("ctrl_' . $objWidget->id . '"), {
  483. draggable: false,
  484. toggle: $("toggle_' . $objWidget->id . '"),
  485. format: "' . $format . '",
  486. positionOffset: {x:-211,y:-209}' . $time . ',
  487. pickerClass: "datepicker_bootstrap",
  488. useFadeInOut: !Browser.ie' . $strOnSelect . ',
  489. startDay: ' . $GLOBALS['TL_LANG']['MSC']['weekOffset'] . ',
  490. titleFormat: "' . $GLOBALS['TL_LANG']['MSC']['titleFormat'] . '"
  491. });
  492. });
  493. </script>';
  494. }
  495. // Color picker
  496. if ($arrData['eval']['colorpicker'] ?? null)
  497. {
  498. // Support single fields as well (see #5240)
  499. $strKey = ($arrData['eval']['multiple'] ?? null) ? $this->strField . '_0' : $this->strField;
  500. $wizard .= ' ' . Image::getHtml('pickcolor.svg', $GLOBALS['TL_LANG']['MSC']['colorpicker'], 'title="' . StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['colorpicker']) . '" id="moo_' . $this->strField . '" style="cursor:pointer"') . '
  501. <script>
  502. window.addEvent("domready", function() {
  503. var cl = $("ctrl_' . $strKey . '").value.hexToRgb(true) || [255, 0, 0];
  504. new MooRainbow("moo_' . $this->strField . '", {
  505. id: "ctrl_' . $strKey . '",
  506. startColor: cl,
  507. imgPath: "assets/colorpicker/images/",
  508. onComplete: function(color) {
  509. $("ctrl_' . $strKey . '").value = color.hex.replace("#", "");
  510. }
  511. });
  512. });
  513. </script>';
  514. }
  515. $arrClasses = StringUtil::trimsplit(' ', $arrData['eval']['tl_class'] ?? '');
  516. // DCA picker
  517. if (isset($arrData['eval']['dcaPicker']) && (\is_array($arrData['eval']['dcaPicker']) || $arrData['eval']['dcaPicker'] === true))
  518. {
  519. $arrClasses[] = 'dcapicker';
  520. $wizard .= Backend::getDcaPickerWizard($arrData['eval']['dcaPicker'], $this->strTable, $this->strField, $this->strInputName);
  521. }
  522. if (($arrData['inputType'] ?? null) == 'password')
  523. {
  524. $wizard .= Backend::getTogglePasswordWizard($this->strInputName);
  525. }
  526. // Add a custom wizard
  527. if (\is_array($arrData['wizard'] ?? null))
  528. {
  529. foreach ($arrData['wizard'] as $callback)
  530. {
  531. if (\is_array($callback))
  532. {
  533. $this->import($callback[0]);
  534. $wizard .= $this->{$callback[0]}->{$callback[1]}($this);
  535. }
  536. elseif (\is_callable($callback))
  537. {
  538. $wizard .= $callback($this);
  539. }
  540. }
  541. }
  542. $hasWizardClass = \in_array('wizard', $arrClasses);
  543. if ($wizard && !($arrData['eval']['disabled'] ?? false) && !($arrData['eval']['readonly'] ?? false))
  544. {
  545. $objWidget->wizard = $wizard;
  546. if (!$hasWizardClass)
  547. {
  548. $arrClasses[] = 'wizard';
  549. }
  550. }
  551. elseif ($hasWizardClass)
  552. {
  553. unset($arrClasses[array_search('wizard', $arrClasses)]);
  554. }
  555. // Set correct form enctype
  556. if ($objWidget instanceof UploadableWidgetInterface)
  557. {
  558. $this->blnUploadable = true;
  559. }
  560. $arrClasses[] = 'widget';
  561. // Mark floated single checkboxes
  562. if (($arrData['inputType'] ?? null) == 'checkbox' && !($arrData['eval']['multiple'] ?? null) && \in_array('w50', $arrClasses))
  563. {
  564. $arrClasses[] = 'cbx';
  565. }
  566. elseif (($arrData['inputType'] ?? null) == 'text' && ($arrData['eval']['multiple'] ?? null) && \in_array('wizard', $arrClasses))
  567. {
  568. $arrClasses[] = 'inline';
  569. }
  570. if (!empty($arrClasses))
  571. {
  572. $arrData['eval']['tl_class'] = implode(' ', array_unique($arrClasses));
  573. }
  574. $updateMode = '';
  575. // Replace the textarea with an RTE instance
  576. if (!empty($arrData['eval']['rte']))
  577. {
  578. list($file, $type) = explode('|', $arrData['eval']['rte'], 2) + array(null, null);
  579. $fileBrowserTypes = array();
  580. $pickerBuilder = System::getContainer()->get('contao.picker.builder');
  581. foreach (array('file' => 'image', 'link' => 'file') as $context => $fileBrowserType)
  582. {
  583. if ($pickerBuilder->supportsContext($context))
  584. {
  585. $fileBrowserTypes[] = $fileBrowserType;
  586. }
  587. }
  588. $objTemplate = new BackendTemplate('be_' . $file);
  589. $objTemplate->selector = 'ctrl_' . $this->strInputName;
  590. $objTemplate->type = $type;
  591. $objTemplate->fileBrowserTypes = $fileBrowserTypes;
  592. $objTemplate->source = $this->strTable . '.' . $this->intId;
  593. $objTemplate->readonly = (bool) ($arrData['eval']['readonly'] ?? false);
  594. // Deprecated since Contao 4.0, to be removed in Contao 5.0
  595. $objTemplate->language = Backend::getTinyMceLanguage();
  596. $updateMode = $objTemplate->parse();
  597. unset($file, $type, $pickerBuilder, $fileBrowserTypes, $fileBrowserType);
  598. }
  599. // Handle multi-select fields in "override all" mode
  600. elseif ((($arrData['inputType'] ?? null) == 'checkbox' || ($arrData['inputType'] ?? null) == 'checkboxWizard') && ($arrData['eval']['multiple'] ?? null) && Input::get('act') == 'overrideAll')
  601. {
  602. $updateMode = '
  603. </div>
  604. <div class="widget">
  605. <fieldset class="tl_radio_container">
  606. <legend>' . $GLOBALS['TL_LANG']['MSC']['updateMode'] . '</legend>
  607. <input type="radio" name="' . $this->strInputName . '_update" id="opt_' . $this->strInputName . '_update_1" class="tl_radio" value="add" onfocus="Backend.getScrollOffset()"> <label for="opt_' . $this->strInputName . '_update_1">' . $GLOBALS['TL_LANG']['MSC']['updateAdd'] . '</label><br>
  608. <input type="radio" name="' . $this->strInputName . '_update" id="opt_' . $this->strInputName . '_update_2" class="tl_radio" value="remove" onfocus="Backend.getScrollOffset()"> <label for="opt_' . $this->strInputName . '_update_2">' . $GLOBALS['TL_LANG']['MSC']['updateRemove'] . '</label><br>
  609. <input type="radio" name="' . $this->strInputName . '_update" id="opt_' . $this->strInputName . '_update_0" class="tl_radio" value="replace" checked="checked" onfocus="Backend.getScrollOffset()"> <label for="opt_' . $this->strInputName . '_update_0">' . $GLOBALS['TL_LANG']['MSC']['updateReplace'] . '</label>
  610. </fieldset>';
  611. }
  612. $strPreview = '';
  613. // Show a preview image (see #4948)
  614. if ($this->strTable == 'tl_files' && $this->strField == 'name' && $this->objActiveRecord !== null && $this->objActiveRecord->type == 'file')
  615. {
  616. $objFile = new File($this->objActiveRecord->path);
  617. if ($objFile->isImage)
  618. {
  619. $blnCanResize = true;
  620. if ($objFile->isSvgImage)
  621. {
  622. // SVG images with undefined sizes cannot be resized
  623. if (!$objFile->viewWidth || !$objFile->viewHeight)
  624. {
  625. $blnCanResize= false;
  626. }
  627. }
  628. elseif (System::getContainer()->get('contao.image.imagine') instanceof Imagine)
  629. {
  630. // Check the maximum width and height if the GDlib is used to resize images
  631. if ($objFile->height > Config::get('gdMaxImgHeight') || $objFile->width > Config::get('gdMaxImgWidth'))
  632. {
  633. $blnCanResize = false;
  634. }
  635. }
  636. if ($blnCanResize)
  637. {
  638. $container = System::getContainer();
  639. $projectDir = $container->getParameter('kernel.project_dir');
  640. try
  641. {
  642. $image = rawurldecode($container->get('contao.image.factory')->create($projectDir . '/' . $objFile->path, array(699, 524, ResizeConfiguration::MODE_BOX))->getUrl($projectDir));
  643. }
  644. catch (\Exception $e)
  645. {
  646. Message::addError($e->getMessage());
  647. $image = Image::getPath('placeholder.svg');
  648. }
  649. }
  650. else
  651. {
  652. $image = Image::getPath('placeholder.svg');
  653. }
  654. $objImage = new File($image);
  655. $ctrl = 'ctrl_preview_' . substr(md5($image), 0, 8);
  656. $strPreview = '
  657. <div id="' . $ctrl . '" class="tl_edit_preview">
  658. <img src="' . $objImage->dataUri . '" width="' . $objImage->width . '" height="' . $objImage->height . '" alt="">
  659. </div>';
  660. // Add the script to mark the important part
  661. if (basename($image) !== 'placeholder.svg')
  662. {
  663. $strPreview .= '<script>Backend.editPreviewWizard($(\'' . $ctrl . '\'));</script>';
  664. if (Config::get('showHelp'))
  665. {
  666. $strPreview .= '<p class="tl_help tl_tip">' . $GLOBALS['TL_LANG'][$this->strTable]['edit_preview_help'] . '</p>';
  667. }
  668. $strPreview = '<div class="widget">' . $strPreview . '</div>';
  669. }
  670. }
  671. }
  672. return $strPreview . '
  673. <div' . (!empty($arrData['eval']['tl_class']) ? ' class="' . trim($arrData['eval']['tl_class']) . '"' : '') . '>' . $objWidget->parse() . $updateMode . (!$objWidget->hasErrors() ? $this->help($strHelpClass) : '') . '
  674. </div>';
  675. }
  676. /**
  677. * Return the field explanation as HTML string
  678. *
  679. * @param string $strClass
  680. *
  681. * @return string
  682. */
  683. public function help($strClass='')
  684. {
  685. $return = $GLOBALS['TL_DCA'][$this->strTable]['fields'][$this->strField]['label'][1] ?? null;
  686. if (!$return || ($GLOBALS['TL_DCA'][$this->strTable]['fields'][$this->strField]['inputType'] ?? null) == 'password' || !Config::get('showHelp'))
  687. {
  688. return '';
  689. }
  690. return '
  691. <p class="tl_help tl_tip' . $strClass . '">' . $return . '</p>';
  692. }
  693. /**
  694. * Generate possible palette names from an array by taking the first value and either adding or not adding the following values
  695. *
  696. * @param array $names
  697. *
  698. * @return array
  699. */
  700. protected function combiner($names)
  701. {
  702. $return = array('');
  703. $names = array_values($names);
  704. for ($i=0, $c=\count($names); $i<$c; $i++)
  705. {
  706. $buffer = array();
  707. foreach ($return as $k=>$v)
  708. {
  709. $buffer[] = ($k%2 == 0) ? $v : $v . $names[$i];
  710. $buffer[] = ($k%2 == 0) ? $v . $names[$i] : $v;
  711. }
  712. $return = $buffer;
  713. }
  714. return array_filter($return);
  715. }
  716. /**
  717. * Return a query string that switches into edit mode
  718. *
  719. * @param integer $id
  720. *
  721. * @return string
  722. */
  723. protected function switchToEdit($id)
  724. {
  725. $arrKeys = array();
  726. $arrUnset = array('act', 'key', 'id', 'table', 'mode', 'pid');
  727. foreach (array_keys($_GET) as $strKey)
  728. {
  729. if (!\in_array($strKey, $arrUnset))
  730. {
  731. $arrKeys[$strKey] = $strKey . '=' . Input::get($strKey);
  732. }
  733. }
  734. $strUrl = TL_SCRIPT . '?' . implode('&', $arrKeys);
  735. return $strUrl . (!empty($arrKeys) ? '&' : '') . (Input::get('table') ? 'table=' . Input::get('table') . '&amp;' : '') . 'act=edit&amp;id=' . rawurlencode($id);
  736. }
  737. /**
  738. * Compile buttons from the table configuration array and return them as HTML
  739. *
  740. * @param array $arrRow
  741. * @param string $strTable
  742. * @param array $arrRootIds
  743. * @param boolean $blnCircularReference
  744. * @param array $arrChildRecordIds
  745. * @param string $strPrevious
  746. * @param string $strNext
  747. *
  748. * @return string
  749. */
  750. protected function generateButtons($arrRow, $strTable, $arrRootIds=array(), $blnCircularReference=false, $arrChildRecordIds=null, $strPrevious=null, $strNext=null)
  751. {
  752. if (!\is_array($GLOBALS['TL_DCA'][$strTable]['list']['operations'] ?? null))
  753. {
  754. return '';
  755. }
  756. $return = '';
  757. foreach ($GLOBALS['TL_DCA'][$strTable]['list']['operations'] as $k=>$v)
  758. {
  759. $v = \is_array($v) ? $v : array($v);
  760. $id = StringUtil::specialchars(rawurldecode($arrRow['id']));
  761. $label = $title = $k;
  762. if (isset($v['label']))
  763. {
  764. if (\is_array($v['label']))
  765. {
  766. $label = $v['label'][0] ?? null;
  767. $title = sprintf($v['label'][1] ?? '', $id);
  768. }
  769. else
  770. {
  771. $label = $title = sprintf($v['label'], $id);
  772. }
  773. }
  774. $attributes = !empty($v['attributes']) ? ' ' . ltrim(sprintf($v['attributes'], $id, $id)) : '';
  775. // Add the key as CSS class
  776. if (strpos($attributes, 'class="') !== false)
  777. {
  778. $attributes = str_replace('class="', 'class="' . $k . ' ', $attributes);
  779. }
  780. else
  781. {
  782. $attributes = ' class="' . $k . '"' . $attributes;
  783. }
  784. // Call a custom function instead of using the default button
  785. if (\is_array($v['button_callback'] ?? null))
  786. {
  787. $this->import($v['button_callback'][0]);
  788. $return .= $this->{$v['button_callback'][0]}->{$v['button_callback'][1]}($arrRow, $v['href'] ?? null, $label, $title, $v['icon'] ?? null, $attributes, $strTable, $arrRootIds, $arrChildRecordIds, $blnCircularReference, $strPrevious, $strNext, $this);
  789. continue;
  790. }
  791. if (\is_callable($v['button_callback'] ?? null))
  792. {
  793. $return .= $v['button_callback']($arrRow, $v['href'] ?? null, $label, $title, $v['icon'] ?? null, $attributes, $strTable, $arrRootIds, $arrChildRecordIds, $blnCircularReference, $strPrevious, $strNext, $this);
  794. continue;
  795. }
  796. // Generate all buttons except "move up" and "move down" buttons
  797. if ($k != 'move' && $v != 'move')
  798. {
  799. if ($k == 'show')
  800. {
  801. if (!empty($v['route']))
  802. {
  803. $href = System::getContainer()->get('router')->generate($v['route'], array('id' => $arrRow['id'], 'popup' => '1'));
  804. }
  805. else
  806. {
  807. $href = $this->addToUrl(($v['href'] ?? '') . '&amp;id=' . $arrRow['id'] . '&amp;popup=1');
  808. }
  809. $return .= '<a href="' . $href . '" title="' . StringUtil::specialchars($title) . '" onclick="Backend.openModalIframe({\'title\':\'' . StringUtil::specialchars(str_replace("'", "\\'", $label)) . '\',\'url\':this.href});return false"' . $attributes . '>' . Image::getHtml($v['icon'], $label) . '</a> ';
  810. }
  811. else
  812. {
  813. if (!empty($v['route']))
  814. {
  815. $href = System::getContainer()->get('router')->generate($v['route'], array('id' => $arrRow['id']));
  816. }
  817. else
  818. {
  819. $href = $this->addToUrl(($v['href'] ?? '') . '&amp;id=' . $arrRow['id'] . (Input::get('nb') ? '&amp;nc=1' : ''));
  820. }
  821. parse_str(StringUtil::decodeEntities($v['href'] ?? ''), $params);
  822. if (($params['act'] ?? null) == 'toggle' && isset($params['field']))
  823. {
  824. // Hide the toggle icon if the user does not have access to the field
  825. if (($GLOBALS['TL_DCA'][$strTable]['fields'][$params['field']]['toggle'] ?? false) !== true || (($GLOBALS['TL_DCA'][$strTable]['fields'][$params['field']]['exclude'] ?? false) && !System::getContainer()->get('security.helper')->isGranted(ContaoCorePermissions::USER_CAN_EDIT_FIELD_OF_TABLE, $strTable . '::' . $params['field'])))
  826. {
  827. continue;
  828. }
  829. $icon = $v['icon'];
  830. $_icon = pathinfo($v['icon'], PATHINFO_FILENAME) . '_.' . pathinfo($v['icon'], PATHINFO_EXTENSION);
  831. if (false !== strpos($v['icon'], '/'))
  832. {
  833. $_icon = \dirname($v['icon']) . '/' . $_icon;
  834. }
  835. if ($icon == 'visible.svg')
  836. {
  837. $_icon = 'invisible.svg';
  838. }
  839. $state = $arrRow[$params['field']] ? 1 : 0;
  840. if ($v['reverse'] ?? false)
  841. {
  842. $state = $arrRow[$params['field']] ? 0 : 1;
  843. }
  844. $return .= '<a href="' . $href . '" title="' . StringUtil::specialchars($title) . '" onclick="Backend.getScrollOffset();return AjaxRequest.toggleField(this,' . ($icon == 'visible.svg' ? 'true' : 'false') . ')">' . Image::getHtml($state ? $icon : $_icon, $label, 'data-icon="' . Image::getPath($icon) . '" data-icon-disabled="' . Image::getPath($_icon) . '" data-state="' . $state . '"') . '</a> ';
  845. }
  846. else
  847. {
  848. $return .= '<a href="' . $href . '" title="' . StringUtil::specialchars($title) . '"' . $attributes . '>' . Image::getHtml($v['icon'], $label) . '</a> ';
  849. }
  850. }
  851. continue;
  852. }
  853. trigger_deprecation('contao/core-bundle', '4.13', 'The DCA "move" operation is deprecated and will be removed in Contao 5.');
  854. $arrDirections = array('up', 'down');
  855. $arrRootIds = \is_array($arrRootIds) ? $arrRootIds : array($arrRootIds);
  856. foreach ($arrDirections as $dir)
  857. {
  858. $label = !empty($GLOBALS['TL_LANG'][$strTable][$dir][0]) ? $GLOBALS['TL_LANG'][$strTable][$dir][0] : $dir;
  859. $title = !empty($GLOBALS['TL_LANG'][$strTable][$dir][1]) ? $GLOBALS['TL_LANG'][$strTable][$dir][1] : $dir;
  860. $label = Image::getHtml($dir . '.svg', $label);
  861. $href = !empty($v['href']) ? $v['href'] : '&amp;act=move';
  862. if ($dir == 'up')
  863. {
  864. $return .= ((is_numeric($strPrevious) && (empty($GLOBALS['TL_DCA'][$strTable]['list']['sorting']['root']) || !\in_array($arrRow['id'], $arrRootIds))) ? '<a href="' . $this->addToUrl($href . '&amp;id=' . $arrRow['id']) . '&amp;sid=' . (int) $strPrevious . '" title="' . StringUtil::specialchars($title) . '"' . $attributes . '>' . $label . '</a> ' : Image::getHtml('up_.svg')) . ' ';
  865. }
  866. else
  867. {
  868. $return .= ((is_numeric($strNext) && (empty($GLOBALS['TL_DCA'][$strTable]['list']['sorting']['root']) || !\in_array($arrRow['id'], $arrRootIds))) ? '<a href="' . $this->addToUrl($href . '&amp;id=' . $arrRow['id']) . '&amp;sid=' . (int) $strNext . '" title="' . StringUtil::specialchars($title) . '"' . $attributes . '>' . $label . '</a> ' : Image::getHtml('down_.svg')) . ' ';
  869. }
  870. }
  871. }
  872. return trim($return);
  873. }
  874. /**
  875. * Compile global buttons from the table configuration array and return them as HTML
  876. *
  877. * @return string
  878. */
  879. protected function generateGlobalButtons()
  880. {
  881. if (!\is_array($GLOBALS['TL_DCA'][$this->strTable]['list']['global_operations'] ?? null))
  882. {
  883. return '';
  884. }
  885. $return = '';
  886. foreach ($GLOBALS['TL_DCA'][$this->strTable]['list']['global_operations'] as $k=>$v)
  887. {
  888. if (!($v['showOnSelect'] ?? null) && Input::get('act') == 'select')
  889. {
  890. continue;
  891. }
  892. $v = \is_array($v) ? $v : array($v);
  893. $title = $label = $k;
  894. if (isset($v['label']))
  895. {
  896. $label = \is_array($v['label']) ? $v['label'][0] : $v['label'];
  897. $title = \is_array($v['label']) ? ($v['label'][1] ?? null) : $v['label'];
  898. }
  899. $attributes = !empty($v['attributes']) ? ' ' . ltrim($v['attributes']) : '';
  900. // Custom icon (see #5541)
  901. if ($v['icon'] ?? null)
  902. {
  903. $v['class'] = trim(($v['class'] ?? '') . ' header_icon');
  904. // Add the theme path if only the file name is given
  905. if (strpos($v['icon'], '/') === false)
  906. {
  907. $v['icon'] = Image::getPath($v['icon']);
  908. }
  909. $attributes = sprintf(' style="background-image:url(\'%s\')"', Controller::addAssetsUrlTo($v['icon'])) . $attributes;
  910. }
  911. if (!$label)
  912. {
  913. $label = $k;
  914. }
  915. if (!$title)
  916. {
  917. $title = $label;
  918. }
  919. // Call a custom function instead of using the default button
  920. if (\is_array($v['button_callback'] ?? null))
  921. {
  922. $this->import($v['button_callback'][0]);
  923. $return .= $this->{$v['button_callback'][0]}->{$v['button_callback'][1]}($v['href'] ?? null, $label, $title, $v['class'] ?? null, $attributes, $this->strTable, $this->root);
  924. continue;
  925. }
  926. if (\is_callable($v['button_callback'] ?? null))
  927. {
  928. $return .= $v['button_callback']($v['href'] ?? null, $label, $title, $v['class'] ?? null, $attributes, $this->strTable, $this->root);
  929. continue;
  930. }
  931. if (!empty($v['route']))
  932. {
  933. $href = System::getContainer()->get('router')->generate($v['route']);
  934. }
  935. else
  936. {
  937. $href = $this->addToUrl($v['href'] ?? '');
  938. }
  939. $return .= '<a href="' . $href . '"' . (isset($v['class']) ? ' class="' . $v['class'] . '"' : '') . ' title="' . StringUtil::specialchars($title) . '"' . $attributes . '>' . $label . '</a> ';
  940. }
  941. return $return;
  942. }
  943. /**
  944. * Compile header buttons from the table configuration array and return them as HTML
  945. *
  946. * @param array $arrRow
  947. * @param string $strPtable
  948. *
  949. * @return string
  950. */
  951. protected function generateHeaderButtons($arrRow, $strPtable)
  952. {
  953. if (!\is_array($GLOBALS['TL_DCA'][$strPtable]['list']['operations'] ?? null))
  954. {
  955. return '';
  956. }
  957. $return = '';
  958. foreach ($GLOBALS['TL_DCA'][$strPtable]['list']['operations'] as $k=> $v)
  959. {
  960. if (empty($v['showInHeader']) || (Input::get('act') == 'select' && !($v['showOnSelect'] ?? null)))
  961. {
  962. continue;
  963. }
  964. $v = \is_array($v) ? $v : array($v);
  965. $id = StringUtil::specialchars(rawurldecode($arrRow['id']));
  966. $label = $title = $k;
  967. if (isset($v['label']))
  968. {
  969. if (\is_array($v['label']))
  970. {
  971. $label = $v['label'][0];
  972. $title = sprintf($v['label'][1], $id);
  973. }
  974. else
  975. {
  976. $label = $title = sprintf($v['label'], $id);
  977. }
  978. }
  979. $attributes = !empty($v['attributes']) ? ' ' . ltrim(sprintf($v['attributes'], $id, $id)) : '';
  980. // Add the key as CSS class
  981. if (strpos($attributes, 'class="') !== false)
  982. {
  983. $attributes = str_replace('class="', 'class="' . $k . ' ', $attributes);
  984. }
  985. else
  986. {
  987. $attributes = ' class="' . $k . '"' . $attributes;
  988. }
  989. // Add the parent table to the href
  990. if (isset($v['href']))
  991. {
  992. $v['href'] .= '&amp;table=' . $strPtable;
  993. }
  994. else
  995. {
  996. $v['href'] = 'table=' . $strPtable;
  997. }
  998. // Call a custom function instead of using the default button
  999. if (\is_array($v['button_callback'] ?? null))
  1000. {
  1001. $this->import($v['button_callback'][0]);
  1002. $return .= $this->{$v['button_callback'][0]}->{$v['button_callback'][1]}($arrRow, $v['href'], $label, $title, $v['icon'], $attributes, $strPtable, array(), null, false, null, null, $this);
  1003. continue;
  1004. }
  1005. if (\is_callable($v['button_callback'] ?? null))
  1006. {
  1007. $return .= $v['button_callback']($arrRow, $v['href'], $label, $title, $v['icon'], $attributes, $strPtable, array(), null, false, null, null, $this);
  1008. continue;
  1009. }
  1010. if ($k == 'show')
  1011. {
  1012. if (!empty($v['route']))
  1013. {
  1014. $href = System::getContainer()->get('router')->generate($v['route'], array('id' => $arrRow['id'], 'popup' => '1'));
  1015. }
  1016. else
  1017. {
  1018. $href = $this->addToUrl($v['href'] . '&amp;id=' . $arrRow['id'] . '&amp;popup=1');
  1019. }
  1020. $return .= '<a href="' . $href . '" title="' . StringUtil::specialchars($title) . '" onclick="Backend.openModalIframe({\'title\':\'' . StringUtil::specialchars(str_replace("'", "\\'", sprintf(\is_array($GLOBALS['TL_LANG'][$strPtable]['show'] ?? null) ? $GLOBALS['TL_LANG'][$strPtable]['show'][1] : ($GLOBALS['TL_LANG'][$strPtable]['show'] ?? ''), $arrRow['id']))) . '\',\'url\':this.href});return false"' . $attributes . '>' . Image::getHtml($v['icon'], $label) . '</a> ';
  1021. }
  1022. else
  1023. {
  1024. if (!empty($v['route']))
  1025. {
  1026. $href = System::getContainer()->get('router')->generate($v['route'], array('id' => $arrRow['id']));
  1027. }
  1028. else
  1029. {
  1030. $href = $this->addToUrl($v['href'] . '&amp;id=' . $arrRow['id'] . (Input::get('nb') ? '&amp;nc=1' : ''));
  1031. }
  1032. parse_str(StringUtil::decodeEntities($v['href']), $params);
  1033. if (($params['act'] ?? null) == 'toggle' && isset($params['field']))
  1034. {
  1035. // Hide the toggle icon if the user does not have access to the field
  1036. if (($GLOBALS['TL_DCA'][$strPtable]['fields'][$params['field']]['toggle'] ?? false) !== true || (($GLOBALS['TL_DCA'][$strPtable]['fields'][$params['field']]['exclude'] ?? false) && !System::getContainer()->get('security.helper')->isGranted(ContaoCorePermissions::USER_CAN_EDIT_FIELD_OF_TABLE, $strPtable . '::' . $params['field'])))
  1037. {
  1038. continue;
  1039. }
  1040. $icon = $v['icon'];
  1041. $_icon = pathinfo($v['icon'], PATHINFO_FILENAME) . '_.' . pathinfo($v['icon'], PATHINFO_EXTENSION);
  1042. if (false !== strpos($v['icon'], '/'))
  1043. {
  1044. $_icon = \dirname($v['icon']) . '/' . $_icon;
  1045. }
  1046. if ($icon == 'visible.svg')
  1047. {
  1048. $_icon = 'invisible.svg';
  1049. }
  1050. $state = $arrRow[$params['field']] ? 1 : 0;
  1051. if ($v['reverse'] ?? false)
  1052. {
  1053. $state = $arrRow[$params['field']] ? 0 : 1;
  1054. }
  1055. $return .= '<a href="' . $href . '" title="' . StringUtil::specialchars($title) . '" onclick="Backend.getScrollOffset();return AjaxRequest.toggleField(this)">' . Image::getHtml($state ? $icon : $_icon, $label, 'data-icon="' . Image::getPath($icon) . '" data-icon-disabled="' . Image::getPath($_icon) . '" data-state="' . $state . '"') . '</a> ';
  1056. }
  1057. else
  1058. {
  1059. $return .= '<a href="' . $href . '" title="' . StringUtil::specialchars($title) . '"' . $attributes . '>' . Image::getHtml($v['icon'], $label) . '</a> ';
  1060. }
  1061. }
  1062. }
  1063. return $return;
  1064. }
  1065. /**
  1066. * Initialize the picker
  1067. *
  1068. * @param PickerInterface $picker
  1069. *
  1070. * @return array|null
  1071. */
  1072. public function initPicker(PickerInterface $picker)
  1073. {
  1074. $provider = $picker->getCurrentProvider();
  1075. if (!$provider instanceof DcaPickerProviderInterface || $provider->getDcaTable($picker->getConfig()) != $this->strTable)
  1076. {
  1077. return null;
  1078. }
  1079. $attributes = $provider->getDcaAttributes($picker->getConfig());
  1080. $this->objPicker = $picker;
  1081. $this->strPickerFieldType = $attributes['fieldType'];
  1082. $this->objPickerCallback = static function ($value) use ($picker, $provider)
  1083. {
  1084. return $provider->convertDcaValue($picker->getConfig(), $value);
  1085. };
  1086. if (isset($attributes['value']))
  1087. {
  1088. $this->arrPickerValue = (array) $attributes['value'];
  1089. }
  1090. return $attributes;
  1091. }
  1092. /**
  1093. * Return the picker input field markup
  1094. *
  1095. * @param string $value
  1096. * @param string $attributes
  1097. *
  1098. * @return string
  1099. */
  1100. protected function getPickerInputField($value, $attributes='')
  1101. {
  1102. $id = is_numeric($value) ? $value : md5($value);
  1103. switch ($this->strPickerFieldType)
  1104. {
  1105. case 'checkbox':
  1106. return ' <input type="checkbox" name="picker[]" id="picker_' . $id . '" class="tl_tree_checkbox" value="' . StringUtil::specialchars(($this->objPickerCallback)($value)) . '" onfocus="Backend.getScrollOffset()"' . Widget::optionChecked($value, $this->arrPickerValue) . $attributes . '>';
  1107. case 'radio':
  1108. return ' <input type="radio" name="picker" id="picker_' . $id . '" class="tl_tree_radio" value="' . StringUtil::specialchars(($this->objPickerCallback)($value)) . '" onfocus="Backend.getScrollOffset()"' . Widget::optionChecked($value, $this->arrPickerValue) . $attributes . '>';
  1109. }
  1110. return '';
  1111. }
  1112. /**
  1113. * Return the data-picker-value attribute with the currently selected picker values (see #1816)
  1114. *
  1115. * @return string
  1116. */
  1117. protected function getPickerValueAttribute()
  1118. {
  1119. // Only load the previously selected values for the checkbox field type (see #2346)
  1120. if ($this->strPickerFieldType != 'checkbox')
  1121. {
  1122. return '';
  1123. }
  1124. $values = array_map($this->objPickerCallback, $this->arrPickerValue);
  1125. $values = array_map('strval', $values);
  1126. $values = json_encode($values);
  1127. $values = htmlspecialchars($values, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5);
  1128. return ' data-picker-value="' . $values . '"';
  1129. }
  1130. /**
  1131. * Build the sort panel and return it as string
  1132. *
  1133. * @return string
  1134. */
  1135. protected function panel()
  1136. {
  1137. if (!($GLOBALS['TL_DCA'][$this->strTable]['list']['sorting']['panelLayout'] ?? null))
  1138. {
  1139. return '';
  1140. }
  1141. // Reset all filters
  1142. if (isset($_POST['filter_reset']) && Input::post('FORM_SUBMIT') == 'tl_filters')
  1143. {
  1144. /** @var AttributeBagInterface $objSessionBag */
  1145. $objSessionBag = System::getContainer()->get('session')->getBag('contao_backend');
  1146. $data = $objSessionBag->all();
  1147. unset(
  1148. $data['filter'][$this->strTable],
  1149. $data['filter'][$this->strTable . '_' . CURRENT_ID],
  1150. $data['sorting'][$this->strTable],
  1151. $data['search'][$this->strTable]
  1152. );
  1153. $objSessionBag->replace($data);
  1154. $this->reload();
  1155. }
  1156. $intFilterPanel = 0;
  1157. $arrPanels = array();
  1158. $arrPanes = StringUtil::trimsplit(';', $GLOBALS['TL_DCA'][$this->strTable]['list']['sorting']['panelLayout'] ?? '');
  1159. foreach ($arrPanes as $strPanel)
  1160. {
  1161. $panels = '';
  1162. $arrSubPanels = StringUtil::trimsplit(',', $strPanel);
  1163. foreach ($arrSubPanels as $strSubPanel)
  1164. {
  1165. $panel = '';
  1166. switch ($strSubPanel)
  1167. {
  1168. case 'limit':
  1169. // The limit menu depends on other panels that may set a filter query, e.g. search and filter.
  1170. // In order to correctly calculate the total row count, the limit menu must be compiled last.
  1171. // We insert a placeholder here and compile the limit menu after all other panels.
  1172. $panel = '###limit_menu###';
  1173. break;
  1174. case 'search':
  1175. $panel = $this->searchMenu();
  1176. break;
  1177. case 'sort':
  1178. $panel = $this->sortMenu();
  1179. break;
  1180. case 'filter':
  1181. // Multiple filter subpanels can be defined to split the fields across panels
  1182. $panel = $this->filterMenu(++$intFilterPanel);
  1183. break;
  1184. default:
  1185. // Call the panel_callback
  1186. $arrCallback = $GLOBALS['TL_DCA'][$this->strTable]['list']['sorting']['panel_callback'][$strSubPanel] ?? null;
  1187. if (\is_array($arrCallback))
  1188. {
  1189. $this->import($arrCallback[0]);
  1190. $panel = $this->{$arrCallback[0]}->{$arrCallback[1]}($this);
  1191. }
  1192. elseif (\is_callable($arrCallback))
  1193. {
  1194. $panel = $arrCallback($this);
  1195. }
  1196. }
  1197. // Add the panel if it is not empty
  1198. if ($panel)
  1199. {
  1200. $panels = $panel . $panels;
  1201. }
  1202. }
  1203. // Add the group if it is not empty
  1204. if ($panels)
  1205. {
  1206. $arrPanels[] = $panels;
  1207. }
  1208. }
  1209. if (empty($arrPanels))
  1210. {
  1211. return '';
  1212. }
  1213. // Compile limit menu if placeholder is present
  1214. foreach ($arrPanels as $key => $strPanel)
  1215. {
  1216. if (strpos($strPanel, '###limit_menu###') === false)
  1217. {
  1218. continue;
  1219. }
  1220. $arrPanels[$key] = str_replace('###limit_menu###', $this->limitMenu(), $strPanel);
  1221. }
  1222. if (Input::post('FORM_SUBMIT') == 'tl_filters')
  1223. {
  1224. $this->reload();
  1225. }
  1226. $return = '';
  1227. $intTotal = \count($arrPanels);
  1228. $intLast = $intTotal - 1;
  1229. for ($i=0; $i<$intTotal; $i++)
  1230. {
  1231. $submit = '';
  1232. if ($i == $intLast)
  1233. {
  1234. $submit = '
  1235. <div class="tl_submit_panel tl_subpanel">
  1236. <button name="filter" id="filter" class="tl_img_submit filter_apply" title="' . StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['applyTitle']) . '">' . $GLOBALS['TL_LANG']['MSC']['apply'] . '</button>
  1237. <button name="filter_reset" id="filter_reset" value="1" class="tl_img_submit filter_reset" title="' . StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['resetTitle']) . '">' . $GLOBALS['TL_LANG']['MSC']['reset'] . '</button>
  1238. </div>';
  1239. }
  1240. $return .= '
  1241. <div class="tl_panel cf">
  1242. ' . $submit . $arrPanels[$i] . '
  1243. </div>';
  1244. }
  1245. $return = '
  1246. <form class="tl_form" method="post" aria-label="' . StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['searchAndFilter']) . '">
  1247. <div class="tl_formbody">
  1248. <input type="hidden" name="FORM_SUBMIT" value="tl_filters">
  1249. <input type="hidden" name="REQUEST_TOKEN" value="' . REQUEST_TOKEN . '">
  1250. ' . $return . '
  1251. </div>
  1252. </form>';
  1253. return $return;
  1254. }
  1255. /**
  1256. * Invalidate the cache tags associated with a given DC
  1257. *
  1258. * Call this whenever an entry is modified (added, updated, deleted).
  1259. */
  1260. public function invalidateCacheTags()
  1261. {
  1262. if (!System::getContainer()->has('fos_http_cache.cache_manager'))
  1263. {
  1264. return;
  1265. }
  1266. $tags = array('contao.db.' . $this->table . '.' . $this->id);
  1267. $this->addPtableTags($this->table, $this->id, $tags);
  1268. // Trigger the oninvalidate_cache_tags_callback
  1269. if (\is_array($GLOBALS['TL_DCA'][$this->table]['config']['oninvalidate_cache_tags_callback'] ?? null))
  1270. {
  1271. foreach ($GLOBALS['TL_DCA'][$this->table]['config']['oninvalidate_cache_tags_callback'] as $callback)
  1272. {
  1273. if (\is_array($callback))
  1274. {
  1275. $this->import($callback[0]);
  1276. $tags = $this->{$callback[0]}->{$callback[1]}($this, $tags);
  1277. }
  1278. elseif (\is_callable($callback))
  1279. {
  1280. $tags = $callback($this, $tags);
  1281. }
  1282. }
  1283. }
  1284. // Make sure tags are unique and empty ones are removed
  1285. $tags = array_filter(array_unique($tags));
  1286. System::getContainer()->get('fos_http_cache.cache_manager')->invalidateTags($tags);
  1287. }
  1288. public function addPtableTags($strTable, $intId, &$tags)
  1289. {
  1290. $ptable = $GLOBALS['TL_DCA'][$strTable]['list']['sorting']['mode'] == 5 ? $strTable : ($GLOBALS['TL_DCA'][$strTable]['config']['ptable'] ?? null);
  1291. if (!$ptable)
  1292. {
  1293. $tags[] = 'contao.db.' . $strTable;
  1294. return;
  1295. }
  1296. Controller::loadDataContainer($ptable);
  1297. $objPid = $this->Database->prepare('SELECT pid FROM ' . Database::quoteIdentifier($strTable) . ' WHERE id=?')
  1298. ->execute($intId);
  1299. if (!$objPid->numRows || $objPid->pid == 0)
  1300. {
  1301. $tags[] = 'contao.db.' . $strTable;
  1302. return;
  1303. }
  1304. $tags[] = 'contao.db.' . $ptable . '.' . $objPid->pid;
  1305. // Do not call recursively (see #4777)
  1306. }
  1307. /**
  1308. * @deprecated Deprecated since Contao 4.9, to be removed in Contao 5.0
  1309. */
  1310. public function addCtableTags($strTable, $intId, &$tags)
  1311. {
  1312. trigger_deprecation('contao/core-bundle', '4.9', 'Calling "%s()" has been deprecated and will no longer work in Contao 5.0.', __METHOD__);
  1313. $ctables = $GLOBALS['TL_DCA'][$strTable]['config']['ctable'] ?? array();
  1314. if (($GLOBALS['TL_DCA'][$strTable]['list']['sorting']['mode'] ?? null) == 5)
  1315. {
  1316. $ctables[] = $strTable;
  1317. }
  1318. if (!$ctables)
  1319. {
  1320. return;
  1321. }
  1322. foreach ($ctables as $ctable)
  1323. {
  1324. Controller::loadDataContainer($ctable);
  1325. if ($GLOBALS['TL_DCA'][$ctable]['config']['dynamicPtable'] ?? null)
  1326. {
  1327. $objIds = $this->Database->prepare('SELECT id FROM ' . Database::quoteIdentifier($ctable) . ' WHERE pid=? AND ptable=?')
  1328. ->execute($intId, $strTable);
  1329. }
  1330. else
  1331. {
  1332. $objIds = $this->Database->prepare('SELECT id FROM ' . Database::quoteIdentifier($ctable) . ' WHERE pid=?')
  1333. ->execute($intId);
  1334. }
  1335. if (!$objIds->numRows)
  1336. {
  1337. continue;
  1338. }
  1339. while ($objIds->next())
  1340. {
  1341. $tags[] = 'contao.db.' . $ctable . '.' . $objIds->id;
  1342. $this->addCtableTags($ctable, $objIds->id, $tags);
  1343. }
  1344. }
  1345. }
  1346. /**
  1347. * Return the form field suffix
  1348. *
  1349. * @return integer|string
  1350. */
  1351. protected function getFormFieldSuffix()
  1352. {
  1353. return $this->intId;
  1354. }
  1355. /**
  1356. * Return the name of the current palette
  1357. *
  1358. * @return string
  1359. */
  1360. abstract public function getPalette();
  1361. /**
  1362. * Save the current value
  1363. *
  1364. * @param mixed $varValue
  1365. *
  1366. * @throws \Exception
  1367. */
  1368. abstract protected function save($varValue);
  1369. /**
  1370. * Return the class name of the DataContainer driver for the given table.
  1371. *
  1372. * @param string $table
  1373. *
  1374. * @return string
  1375. *
  1376. * @todo Change the return type to ?string in Contao 5.0
  1377. */
  1378. public static function getDriverForTable(string $table): string
  1379. {
  1380. if (!isset($GLOBALS['TL_DCA'][$table]['config']['dataContainer']))
  1381. {
  1382. return '';
  1383. }
  1384. $dataContainer = $GLOBALS['TL_DCA'][$table]['config']['dataContainer'];
  1385. if ('' !== $dataContainer && false === strpos($dataContainer, '\\'))
  1386. {
  1387. trigger_deprecation('contao/core-bundle', '4.9', 'The usage of a non fully qualified class name "%s" for table "%s" as DataContainer name has been deprecated and will no longer work in Contao 5.0. Use the fully qualified class name instead, e.g. Contao\DC_Table::class.', $dataContainer, $table);
  1388. $dataContainer = 'DC_' . $dataContainer;
  1389. if (class_exists($dataContainer))
  1390. {
  1391. $ref = new \ReflectionClass($dataContainer);
  1392. return $ref->getName();
  1393. }
  1394. }
  1395. return $dataContainer;
  1396. }
  1397. /**
  1398. * Generates the label for a given data record according to the DCA configuration.
  1399. * Returns an array of strings if 'showColumns' is enabled in the DCA configuration.
  1400. *
  1401. * @param array $row The data record
  1402. * @param string $table The name of the data container
  1403. *
  1404. * @return string|array<string>
  1405. */
  1406. public function generateRecordLabel(array $row, ?string $table = null, bool $protected = false, bool $isVisibleRootTrailPage = false)
  1407. {
  1408. $table = $table ?? $this->strTable;
  1409. $labelConfig = &$GLOBALS['TL_DCA'][$table]['list']['label'];
  1410. $args = array();
  1411. foreach ($labelConfig['fields'] as $k=>$v)
  1412. {
  1413. // Decrypt the value
  1414. if ($GLOBALS['TL_DCA'][$table]['fields'][$v]['eval']['encrypt'] ?? null)
  1415. {
  1416. $row[$v] = Encryption::decrypt(StringUtil::deserialize($row[$v]));
  1417. }
  1418. if (strpos($v, ':') !== false)
  1419. {
  1420. list($strKey, $strTable) = explode(':', $v, 2);
  1421. list($strTable, $strField) = explode('.', $strTable, 2);
  1422. $objRef = Database::getInstance()
  1423. ->prepare("SELECT " . Database::quoteIdentifier($strField) . " FROM " . $strTable . " WHERE id=?")
  1424. ->limit(1)
  1425. ->execute($row[$strKey]);
  1426. $args[$k] = $objRef->numRows ? $objRef->$strField : '';
  1427. }
  1428. elseif (isset($row[$v], $GLOBALS['TL_DCA'][$table]['fields'][$v]['foreignKey']))
  1429. {
  1430. $key = explode('.', $GLOBALS['TL_DCA'][$table]['fields'][$v]['foreignKey'], 2);
  1431. $objRef = Database::getInstance()
  1432. ->prepare("SELECT " . Database::quoteIdentifier($key[1]) . " AS value FROM " . $key[0] . " WHERE id=?")
  1433. ->limit(1)
  1434. ->execute($row[$v]);
  1435. $args[$k] = $objRef->numRows ? $objRef->value : '';
  1436. }
  1437. elseif (\in_array($GLOBALS['TL_DCA'][$table]['fields'][$v]['flag'] ?? null, array(self::SORT_DAY_ASC, self::SORT_DAY_DESC, self::SORT_MONTH_ASC, self::SORT_MONTH_DESC, self::SORT_YEAR_ASC, self::SORT_YEAR_DESC)))
  1438. {
  1439. if (($GLOBALS['TL_DCA'][$table]['fields'][$v]['eval']['rgxp'] ?? null) == 'date')
  1440. {
  1441. $args[$k] = $row[$v] ? Date::parse(Config::get('dateFormat'), $row[$v]) : '-';
  1442. }
  1443. elseif (($GLOBALS['TL_DCA'][$table]['fields'][$v]['eval']['rgxp'] ?? null) == 'time')
  1444. {
  1445. $args[$k] = $row[$v] ? Date::parse(Config::get('timeFormat'), $row[$v]) : '-';
  1446. }
  1447. else
  1448. {
  1449. $args[$k] = $row[$v] ? Date::parse(Config::get('datimFormat'), $row[$v]) : '-';
  1450. }
  1451. }
  1452. elseif (($GLOBALS['TL_DCA'][$table]['fields'][$v]['eval']['isBoolean'] ?? null) || (($GLOBALS['TL_DCA'][$table]['fields'][$v]['inputType'] ?? null) == 'checkbox' && !($GLOBALS['TL_DCA'][$table]['fields'][$v]['eval']['multiple'] ?? null)))
  1453. {
  1454. $args[$k] = ($row[$v] ?? null) ? $GLOBALS['TL_LANG']['MSC']['yes'] : $GLOBALS['TL_LANG']['MSC']['no'];
  1455. }
  1456. elseif (isset($row[$v]))
  1457. {
  1458. $row_v = StringUtil::deserialize($row[$v]);
  1459. if (\is_array($row_v))
  1460. {
  1461. $args_k = array();
  1462. foreach ($row_v as $option)
  1463. {
  1464. $args_k[] = $GLOBALS['TL_DCA'][$table]['fields'][$v]['reference'][$option] ?? $option;
  1465. }
  1466. $args[$k] = implode(', ', iterator_to_array(new \RecursiveIteratorIterator(new \RecursiveArrayIterator($args_k)), false));
  1467. }
  1468. elseif (isset($GLOBALS['TL_DCA'][$table]['fields'][$v]['reference'][$row[$v]]))
  1469. {
  1470. $args[$k] = \is_array($GLOBALS['TL_DCA'][$table]['fields'][$v]['reference'][$row[$v]]) ? $GLOBALS['TL_DCA'][$table]['fields'][$v]['reference'][$row[$v]][0] : $GLOBALS['TL_DCA'][$table]['fields'][$v]['reference'][$row[$v]];
  1471. }
  1472. elseif ((($GLOBALS['TL_DCA'][$table]['fields'][$v]['eval']['isAssociative'] ?? null) || ArrayUtil::isAssoc($GLOBALS['TL_DCA'][$table]['fields'][$v]['options'] ?? null)) && isset($GLOBALS['TL_DCA'][$table]['fields'][$v]['options'][$row[$v]]))
  1473. {
  1474. $args[$k] = $GLOBALS['TL_DCA'][$table]['fields'][$v]['options'][$row[$v]] ?? null;
  1475. }
  1476. else
  1477. {
  1478. $args[$k] = $row[$v];
  1479. }
  1480. }
  1481. else
  1482. {
  1483. $args[$k] = null;
  1484. }
  1485. }
  1486. // Render the label
  1487. $label = vsprintf($labelConfig['format'] ?? '%s', $args);
  1488. // Shorten the label it if it is too long
  1489. if (($labelConfig['maxCharacters'] ?? null) > 0 && $labelConfig['maxCharacters'] < \strlen(strip_tags($label)))
  1490. {
  1491. $label = trim(StringUtil::substrHtml($label, $labelConfig['maxCharacters'])) . ' …';
  1492. }
  1493. // Remove empty brackets (), [], {}, <> and empty tags from the label
  1494. $label = preg_replace('/\( *\) ?|\[ *] ?|{ *} ?|< *> ?/', '', $label);
  1495. $label = preg_replace('/<[^\/!][^>]+>\s*<\/[^>]+>/', '', $label);
  1496. $mode = $GLOBALS['TL_DCA'][$table]['list']['sorting']['mode'] ?? self::MODE_SORTED;
  1497. // Execute label_callback
  1498. if (\is_array($labelConfig['label_callback'] ?? null) || \is_callable($labelConfig['label_callback'] ?? null))
  1499. {
  1500. if (\in_array($mode, array(self::MODE_TREE, self::MODE_TREE_EXTENDED)))
  1501. {
  1502. if (\is_array($labelConfig['label_callback'] ?? null))
  1503. {
  1504. $label = System::importStatic($labelConfig['label_callback'][0])->{$labelConfig['label_callback'][1]}($row, $label, $this, '', false, $protected, $isVisibleRootTrailPage);
  1505. }
  1506. else
  1507. {
  1508. $label = $labelConfig['label_callback']($row, $label, $this, '', false, $protected, $isVisibleRootTrailPage);
  1509. }
  1510. }
  1511. elseif ($mode === self::MODE_PARENT)
  1512. {
  1513. if (\is_array($labelConfig['label_callback'] ?? null))
  1514. {
  1515. $label = System::importStatic($labelConfig['label_callback'][0])->{$labelConfig['label_callback'][1]}($row, $label, $this);
  1516. }
  1517. else
  1518. {
  1519. $label = $labelConfig['label_callback']($row, $label, $this);
  1520. }
  1521. }
  1522. else
  1523. {
  1524. if (\is_array($labelConfig['label_callback'] ?? null))
  1525. {
  1526. $label = System::importStatic($labelConfig['label_callback'][0])->{$labelConfig['label_callback'][1]}($row, $label, $this, $args);
  1527. }
  1528. else
  1529. {
  1530. $label = $labelConfig['label_callback']($row, $label, $this, $args);
  1531. }
  1532. }
  1533. }
  1534. elseif (\in_array($mode, array(self::MODE_TREE, self::MODE_TREE_EXTENDED)))
  1535. {
  1536. $label = Image::getHtml('iconPLAIN.svg') . ' ' . $label;
  1537. }
  1538. if (($labelConfig['showColumns'] ?? null) && !\in_array($mode, array(self::MODE_PARENT, self::MODE_TREE, self::MODE_TREE_EXTENDED)))
  1539. {
  1540. return \is_array($label) ? $label : $args;
  1541. }
  1542. return $label;
  1543. }
  1544. protected function markAsCopy(string $label, string $value): string
  1545. {
  1546. // Do not mark as copy more than once (see #6058)
  1547. if (preg_match('/' . preg_quote(sprintf($label, ''), '/') . '/', StringUtil::decodeEntities($value)))
  1548. {
  1549. return $value;
  1550. }
  1551. return sprintf($label, $value);
  1552. }
  1553. }
  1554. class_alias(DataContainer::class, 'DataContainer');