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

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