• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

heimrichhannot / contao-utils-bundle / 4819848537

pending completion
4819848537

push

github

Thomas Körner
fixed warning

1 of 1 new or added line in 1 file covered. (100.0%)

1188 of 5296 relevant lines covered (22.43%)

1.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Dca/DcaUtil.php
1
<?php
2

3
/*
4
 * Copyright (c) 2023 Heimrich & Hannot GmbH
5
 *
6
 * @license LGPL-3.0-or-later
7
 */
8

9
namespace HeimrichHannot\UtilsBundle\Dca;
10

11
use Contao\BackendUser;
12
use Contao\Config;
13
use Contao\Controller;
14
use Contao\CoreBundle\DataContainer\PaletteManipulator;
15
use Contao\CoreBundle\Framework\ContaoFrameworkInterface;
16
use Contao\Database;
17
use Contao\DataContainer;
18
use Contao\DcaExtractor;
19
use Contao\DiffRenderer;
20
use Contao\FrontendUser;
21
use Contao\Image;
22
use Contao\Input;
23
use Contao\Model;
24
use Contao\RequestToken;
25
use Contao\StringUtil;
26
use Contao\System;
27
use Contao\Validator;
28
use Doctrine\DBAL\Connection;
29
use HeimrichHannot\UtilsBundle\Choice\ModelInstanceChoice;
30
use HeimrichHannot\UtilsBundle\Driver\DC_Table_Utils;
31
use HeimrichHannot\UtilsBundle\Model\ModelUtil;
32
use HeimrichHannot\UtilsBundle\Routing\RoutingUtil;
33
use Symfony\Component\DependencyInjection\ContainerInterface;
34

35
class DcaUtil
36
{
37
    const PROPERTY_SESSION_ID = 'sessionID';
38
    const PROPERTY_AUTHOR = 'author';
39
    const PROPERTY_AUTHOR_TYPE = 'authorType';
40

41
    const AUTHOR_TYPE_NONE = 'none';
42
    const AUTHOR_TYPE_MEMBER = 'member';
43
    const AUTHOR_TYPE_USER = 'user';
44
    const AUTHOR_TYPE_SESSION = 'session';
45

46
    /** @var ContaoFrameworkInterface */
47
    protected $framework;
48
    /**
49
     * @var ContainerInterface
50
     */
51
    protected $container;
52
    /**
53
     * @var RoutingUtil
54
     */
55
    private $routingUtil;
56

57
    /**
58
     * @var Connection
59
     */
60
    private $connection;
61

62
    public function __construct(ContainerInterface $container, ContaoFrameworkInterface $framework, RoutingUtil $routingUtil, Connection $connection)
63
    {
64
        $this->container = $container;
×
65
        $this->framework = $framework;
×
66
        $this->routingUtil = $routingUtil;
×
67
        $this->connection = $connection;
×
68
    }
69

70
    /**
71
     * Get a contao backend modal edit link.
72
     *
73
     * @param string      $module Name of the module
74
     * @param int         $id     Id of the entity
75
     * @param string|null $label  The label text
76
     *
77
     * @return string The edit link
78
     */
79
    public function getEditLink(string $module, int $id, string $label = null): string
80
    {
81
        $url = $this->container->get('huh.utils.url')->getCurrentUrl([
×
82
            'skipParams' => true,
×
83
        ]);
×
84

85
        if (!$id) {
×
86
            return '';
×
87
        }
88

89
        $label = sprintf(StringUtil::specialchars($label ?: $GLOBALS['TL_LANG']['tl_content']['editalias'][1]), $id);
×
90

91
        return sprintf(
×
92
            ' <a href="'.$url.'?do=%s&amp;act=edit&amp;id=%s&amp;rt=%s" title="%s" style="padding-left: 5px; padding-top: 2px; display: inline-block;">%s</a>',
×
93
            $module,
×
94
            $id,
×
95
            $this->container->get('security.csrf.token_manager')->getToken($this->container->getParameter('contao.csrf_token_name'))->getValue(),
×
96
            $label,
×
97
            Image::getHtml('alias.svg', $label, 'style="vertical-align:top"')
×
98
        );
×
99
    }
100

101
    /**
102
     * Get a contao backend modal edit link.
103
     *
104
     * @param string      $module Name of the module
105
     * @param int         $id     Id of the entity
106
     * @param string|null $label  The label text
107
     * @param string      $table  The dataContainer table
108
     * @param int         $width  The modal window width
109
     *
110
     * @return string The modal edit link
111
     *
112
     * @deprecated Use DcaUtil::getPopupWizardLink() instead
113
     */
114
    public function getModalEditLink(string $module, int $id, string $label = null, string $table = '', int $width = 1024): string
115
    {
116
        $url = $this->container->get('huh.utils.url')->getCurrentUrl([
×
117
            'skipParams' => true,
×
118
        ]);
×
119

120
        if (!$id) {
×
121
            return '';
×
122
        }
123

124
        $label = sprintf(StringUtil::specialchars($label ?: $GLOBALS['TL_LANG']['tl_content']['editalias'][1]), $id);
×
125

126
        return sprintf(
×
127
            ' <a href="'.$url.'?do=%s&amp;act=edit&amp;id=%s%s&amp;popup=1&amp;nb=1&amp;rt=%s" title="%s" '
×
128
            .'style="padding-left: 5px; padding-top: 2px; display: inline-block;" onclick="Backend.openModalIframe({\'width\':%s,\'title\':\'%s'.'\',\'url\':this.href});return false">%s</a>',
×
129
            $module,
×
130
            $id,
×
131
            ($table ? '&amp;table='.$table : ''),
×
132
            $this->container->get('security.csrf.token_manager')->getToken($this->container->getParameter('contao.csrf_token_name'))->getValue(),
×
133
            $label,
×
134
            $width,
×
135
            $label,
×
136
            Image::getHtml('alias.svg', $label, 'style="vertical-align:top"')
×
137
        );
×
138
    }
139

140
    /**
141
     * Get a contao backend modal archive edit link.
142
     *
143
     * @param string      $module Name of the module
144
     * @param int         $id     Id of the entity
145
     * @param string      $table  The dataContainer table
146
     * @param string|null $label  The label text
147
     * @param int         $width  The modal window width
148
     *
149
     * @return string The modal archive edit link
150
     *
151
     * @deprecated Use DcaUtil::getPopupWizardLink() instead
152
     */
153
    public function getArchiveModalEditLink(string $module, int $id, string $table, string $label = null, int $width = 1024): string
154
    {
155
        $url = $this->container->get('huh.utils.url')->getCurrentUrl([
×
156
            'skipParams' => true,
×
157
        ]);
×
158

159
        if (!$id) {
×
160
            return '';
×
161
        }
162

163
        $label = sprintf(StringUtil::specialchars($label ?: $GLOBALS['TL_LANG']['tl_content']['editalias'][1]), $id);
×
164

165
        return sprintf(
×
166
            ' <a href="'.$url.'?do=%s&amp;id=%s&amp;table=%s&amp;popup=1&amp;nb=1&amp;rt=%s" title="%s" '
×
167
            .'style="padding-left:3px; float: right" onclick="Backend.openModalIframe({\'width\':\'%s\',\'title\':\'%s'.'\',\'url\':this.href});return false">%s</a>',
×
168
            $module,
×
169
            $id,
×
170
            $table,
×
171
            $this->container->get('security.csrf.token_manager')->getToken($this->container->getParameter('contao.csrf_token_name'))->getValue(),
×
172
            $label,
×
173
            $width,
×
174
            $label,
×
175
            Image::getHtml('alias.svg', $label, 'style="vertical-align:top"')
×
176
        );
×
177
    }
178

179
    /**
180
     * Get a contao backend popup link.
181
     *
182
     * Options:
183
     * - attributes: (array) Link attributes as key value pairs. Will override title and style option. href and onclick are not allowed and will be removed from list.
184
     * - title: (string) Overrride default link title
185
     * - style: (string) Override default css style properties
186
     * - onclick: (string) Override default onclick javascript code
187
     * - icon: (string) Link icon to show as link text. Overrides default icon.
188
     * - linkText: (string) A linkTitle to show as link text. Will be displayed after the link icon. Default empty.
189
     * - url-only: (boolean) Return only url instead of a complete link element
190
     *
191
     * @param array $parameter An array of parameter. Using string is deprecated and will be removed in a future version.
192
     *
193
     * @return string
194
     */
195
    public function getPopupWizardLink($parameter, array $options = [])
196
    {
197
        if (\is_string($parameter)) {
×
198
            @trigger_error('Using string as parameter is deprecated and will be removed in a future version.', \E_USER_DEPRECATED);
×
199
            $result = [];
×
200
            $query = parse_url($parameter, \PHP_URL_QUERY);
×
201

202
            if (\is_string($query)) {
×
203
                $parameter = $query;
×
204
            }
205
            parse_str($parameter, $result);
×
206
            $parameter = $result;
×
207
        }
208

209
        $route = $options['route'] ?? 'contao_backend';
×
210

211
        $parameter['popup'] = 1;
×
212
        $parameter['nb'] = 1;
×
213

214
        $url = $this->routingUtil->generateBackendRoute($parameter, true, true, $route);
×
215

216
        if (isset($options['url-only']) && true === $options['url-only']) {
×
217
            return $url;
×
218
        }
219

220
        $attributes = [];
×
221

222
        if (isset($options['attributes'])) {
×
223
            $attributes = $options['attributes'];
×
224
        }
225

226
        // title
227
        if (!isset($options['title']) || !$options['title']) {
×
228
            $title = $GLOBALS['TL_LANG']['tl_content']['edit'][0];
×
229
        } else {
230
            $title = StringUtil::specialchars($options['title']);
×
231
        }
232

233
        if (!isset($attributes['title'])) {
×
234
            $attributes['title'] = $title;
×
235
        }
236

237
        // style
238
        $style = !isset($options['style']) ? 'padding-left: 5px; padding-top: 2px; display: inline-block;' : $options['style'];
×
239

240
        if (!empty($style) && !isset($attributes['style'])) {
×
241
            $attributes['style'] = $style;
×
242
        }
243

244
        // onclick
245
        if (!isset($options['onclick']) || !$options['onclick']) {
×
246
            $popupWidth = !isset($options['popupWidth']) || !$options['popupWidth'] ? 991 : $options['popupWidth'];
×
247
            $popupTitle = !isset($options['popupTitle']) || !$options['popupTitle'] ? $title : $options['popupTitle'];
×
248

249
            $onclick = sprintf(
×
250
                'onclick="Backend.openModalIframe({\'width\':%s,\'title\':\'%s'.'\',\'url\':this.href});return false"',
×
251
                $popupWidth,
×
252
                $popupTitle
×
253
            );
×
254
        } else {
255
            $onclick = $options['onclick'];
×
256
        }
257

258
        if (!isset($attributes['onclick'])) {
×
259
            $attributes['onclick'] = $onclick;
×
260
        }
261

262
        // link text and icon
263
        $linkText = '';
×
264

265
        if (!isset($options['icon'])) {
×
266
            $linkText .= $this->framework->getAdapter(Image::class)->getHtml('alias.svg', $title, 'style="vertical-align:top"');
×
267
        } elseif (!empty($options['icon'])) {
×
268
            $linkText = $this->framework->getAdapter(Image::class)->getHtml($options['icon'], $title, 'style="vertical-align:top"');
×
269
        }
270

271
        if (isset($options['linkText']) || !empty($options['linkText'])) {
×
272
            $linkText .= $options['linkText'];
×
273
        }
274

275
        // Attributes
276
        $attributeQuery = '';
×
277

278
        foreach ($attributes as $key => $value) {
×
279
            if (\in_array($key, ['href', 'onclick'])) {
×
280
                continue;
×
281
            }
282
            $attributeQuery .= $key.'="'.htmlspecialchars($value).'" ';
×
283
        }
284

285
        return sprintf(
×
286
            '<a href="%s" %s %s>%s</a>',
×
287
            $url,
×
288
            $attributeQuery,
×
289
            $onclick,
×
290
            $linkText
×
291
        );
×
292
    }
293

294
    /**
295
     * Set initial $varData from dca.
296
     *
297
     * @param string $strTable Dca table name
298
     * @param mixed  $varData  Object or array
299
     *
300
     * @return mixed Object or array with the default values
301
     */
302
    public function setDefaultsFromDca($strTable, $varData = null, bool $includeSql = false)
303
    {
304
        $this->framework->getAdapter(Controller::class)->loadDataContainer($strTable);
×
305

306
        if (empty($GLOBALS['TL_DCA'][$strTable])) {
×
307
            return $varData;
×
308
        }
309

310
        $dbFields = [];
×
311

312
        foreach (Database::getInstance()->listFields($strTable) as $data) {
×
313
            if (!isset($data['default'])) {
×
314
                continue;
×
315
            }
316

317
            $dbFields[$data['name']] = $data['default'];
×
318
        }
319

320
        // Get all default values for the new entry
321
        foreach ($GLOBALS['TL_DCA'][$strTable]['fields'] as $k => $v) {
×
322
            $addDefaultValue = false;
×
323
            $defaultValue = null;
×
324

325
            // check sql definition
326
            if ($includeSql && isset($dbFields[$k])) {
×
327
                $addDefaultValue = true;
×
328
                $defaultValue = $dbFields[$k];
×
329
            }
330

331
            // check dca default value
332
            if (\array_key_exists('default', $v)) {
×
333
                $addDefaultValue = true;
×
334
                $defaultValue = \is_array($v['default']) ? serialize($v['default']) : $v['default'];
×
335
            }
336

337
            if (!$addDefaultValue) {
×
338
                continue;
×
339
            }
340

341
            // Encrypt the default value (see #3740)
342
            if ($GLOBALS['TL_DCA'][$strTable]['fields'][$k]['eval']['encrypt'] ?? false) {
×
343
                $defaultValue = $this->container->get('huh.utils.encryption')->encrypt($defaultValue);
×
344
            }
345

346
            if ($addDefaultValue) {
×
347
                if (\is_object($varData)) {
×
348
                    $varData->{$k} = $defaultValue;
×
349
                } else {
350
                    if (null === $varData) {
×
351
                        $varData = [];
×
352
                    }
353

354
                    if (\is_array($varData)) {
×
355
                        $varData[$k] = $defaultValue;
×
356
                    }
357
                }
358
            }
359
        }
360

361
        return $varData;
×
362
    }
363

364
    /**
365
     * Retrieves an array from a dca config (in most cases eval) in the following priorities:.
366
     *
367
     * 1. The value associated to $array[$property]
368
     * 2. The value retrieved by $array[$property . '_callback'] which is a callback array like ['Class', 'method'] or ['service.id', 'method']
369
     * 3. The value retrieved by $array[$property . '_callback'] which is a function closure array like ['Class', 'method']
370
     *
371
     * @param $property
372
     *
373
     * @return mixed|null The value retrieved in the way mentioned above or null
374
     */
375
    public function getConfigByArrayOrCallbackOrFunction(array $array, $property, array $arguments = [])
376
    {
377
        if (isset($array[$property])) {
×
378
            return $array[$property];
×
379
        }
380

381
        if (!isset($array[$property.'_callback'])) {
×
382
            return null;
×
383
        }
384

385
        if (\is_array($array[$property.'_callback'])) {
×
386
            $callback = $array[$property.'_callback'];
×
387

388
            if (!isset($callback[0]) || !isset($callback[1])) {
×
389
                return null;
×
390
            }
391

392
            try {
393
                $instance = Controller::importStatic($callback[0]);
×
394
            } catch (\Exception $e) {
×
395
                return null;
×
396
            }
397

398
            if (!method_exists($instance, $callback[1])) {
×
399
                return null;
×
400
            }
401

402
            try {
403
                return \call_user_func_array([$instance, $callback[1]], $arguments);
×
404
            } catch (\Error $e) {
×
405
                return null;
×
406
            }
407
        } elseif (\is_callable($array[$property.'_callback'])) {
×
408
            try {
409
                return \call_user_func_array($array[$property.'_callback'], $arguments);
×
410
            } catch (\Error $e) {
×
411
                return null;
×
412
            }
413
        }
414

415
        return null;
×
416
    }
417

418
    /**
419
     * Sets the current date as the date added -> usually used on submit.
420
     */
421
    public function setDateAdded(DataContainer $dc)
422
    {
423
        $modelUtil = $this->container->get('huh.utils.model');
×
424

425
        if (null === $dc || null === ($model = $modelUtil->findModelInstanceByPk($dc->table, $dc->id)) || $model->dateAdded > 0) {
×
426
            return null;
×
427
        }
428

429
        $this->framework->createInstance(Database::class)->prepare("UPDATE $dc->table SET dateAdded=? WHERE id=? AND dateAdded = 0")->execute(time(), $dc->id);
×
430
    }
431

432
    /**
433
     * Sets the current date as the date added -> usually used on copy.
434
     *
435
     * @param $insertId
436
     */
437
    public function setDateAddedOnCopy($insertId, DataContainer $dc)
438
    {
439
        $modelUtil = $this->container->get('huh.utils.model');
×
440

441
        if (null === $dc || null === ($model = $modelUtil->findModelInstanceByPk($dc->table, $insertId)) || $model->dateAdded > 0) {
×
442
            return null;
×
443
        }
444

445
        $this->framework->createInstance(Database::class)->prepare("UPDATE $dc->table SET dateAdded=? WHERE id=? AND dateAdded = 0")->execute(time(), $insertId);
×
446
    }
447

448
    /**
449
     * Returns a list of fields as an option array for dca fields.
450
     *
451
     * Possible options:
452
     * - array inputTypes Restrict to certain input types
453
     * - array evalConditions restrict to certain dca eval
454
     * - bool localizeLabels
455
     * - bool skipSorting
456
     *
457
     * @deprecated Use Utils service instead
458
     * @codeCoverageIgnore
459
     */
460
    public function getFields(string $table, array $options = []): array
461
    {
462
        $fields = [];
463

464
        if (!$table) {
465
            return $fields;
466
        }
467

468
        $this->framework->getAdapter(Controller::class)->loadDataContainer($table);
469
        System::loadLanguageFile($table);
470

471
        if (!isset($GLOBALS['TL_DCA'][$table]['fields'])) {
472
            return $fields;
473
        }
474

475
        foreach ($GLOBALS['TL_DCA'][$table]['fields'] as $name => $data) {
476
            // restrict to certain input types
477
            if (isset($options['inputTypes']) && \is_array($options['inputTypes']) && !empty($options['inputTypes']) && (isset($data['inputType']) && !\in_array($data['inputType'], $options['inputTypes']))) {
478
                continue;
479
            }
480

481
            // restrict to certain dca eval
482
            if (isset($options['evalConditions']) && \is_array($options['evalConditions']) && !empty($options['evalConditions'])) {
483
                foreach ($options['evalConditions'] as $key => $value) {
484
                    if (!isset($data['eval'][$key]) || $data['eval'][$key] !== $value) {
485
                        continue 2;
486
                    }
487
                }
488
            }
489

490
            if (isset($options['localizeLabels']) && !$options['localizeLabels']) {
491
                $fields[$name] = $name;
492
            } else {
493
                $label = $name;
494

495
                if (isset($data['label'][0]) && $data['label'][0]) {
496
                    $label .= ' <span style="display: inline; color:#999; padding-left:3px">['.$data['label'][0].']</span>';
497
                }
498

499
                $fields[$name] = $label;
500
            }
501
        }
502

503
        if (!isset($options['skipSorting']) || !$options['skipSorting']) {
504
            asort($fields);
505
        }
506

507
        return $fields;
508
    }
509

510
    /**
511
     * Adds an override selector to every field in $fields to the dca associated with $destinationTable.
512
     */
513
    public function addOverridableFields(array $fields, string $sourceTable, string $destinationTable, array $options = [])
514
    {
515
        $this->framework->getAdapter(Controller::class)->loadDataContainer($sourceTable);
×
516
        System::loadLanguageFile($sourceTable);
×
517
        $sourceDca = $GLOBALS['TL_DCA'][$sourceTable];
×
518

519
        $this->framework->getAdapter(Controller::class)->loadDataContainer($destinationTable);
×
520
        System::loadLanguageFile($destinationTable);
×
521
        $destinationDca = &$GLOBALS['TL_DCA'][$destinationTable];
×
522

523
        foreach ($fields as $field) {
×
524
            // add override boolean field
525
            $overrideFieldname = 'override'.ucfirst($field);
×
526

527
            $destinationDca['fields'][$overrideFieldname] = [
×
528
                'label' => &$GLOBALS['TL_LANG'][$destinationTable][$overrideFieldname],
×
529
                'exclude' => true,
×
530
                'inputType' => 'checkbox',
×
531
                'eval' => ['tl_class' => 'w50', 'submitOnChange' => true, 'isOverrideSelector' => true],
×
532
                'sql' => "char(1) NOT NULL default ''",
×
533
            ];
×
534

535
            if (isset($options['checkboxDcaEvalOverride']) && \is_array($options['checkboxDcaEvalOverride'])) {
×
536
                $destinationDca['fields'][$overrideFieldname]['eval'] = array_merge($destinationDca['fields'][$overrideFieldname]['eval'], $options['checkboxDcaEvalOverride']);
×
537
            }
538

539
            // important: nested selectors need to be in reversed order -> see DC_Table::getPalette()
540
            $destinationDca['palettes']['__selector__'] = array_merge([$overrideFieldname], isset($destinationDca['palettes']['__selector__']) && \is_array($destinationDca['palettes']['__selector__']) ? $destinationDca['palettes']['__selector__'] : []);
×
541

542
            // copy field
543
            $destinationDca['fields'][$field] = $sourceDca['fields'][$field];
×
544

545
            // subpalette
546
            $destinationDca['subpalettes'][$overrideFieldname] = $field;
×
547

548
            if (!isset($options['skipLocalization']) || !$options['skipLocalization']) {
×
549
                $GLOBALS['TL_LANG'][$destinationTable][$overrideFieldname] = [
×
550
                    $this->container->get('translator')->trans('huh.utils.misc.override.label', [
×
551
                        '%fieldname%' => $GLOBALS['TL_DCA'][$sourceTable]['fields'][$field]['label'][0] ?? $field,
×
552
                    ]),
×
553
                    $this->container->get('translator')->trans('huh.utils.misc.override.desc', [
×
554
                        '%fieldname%' => $GLOBALS['TL_DCA'][$sourceTable]['fields'][$field]['label'][0] ?? $field,
×
555
                    ]),
×
556
                ];
×
557
            }
558
        }
559
    }
560

561
    /**
562
     * Retrieves a property of given contao model instances by *ascending* priority, i.e. the last instance of $instances
563
     * will have the highest priority.
564
     *
565
     * CAUTION: This function assumes that you have used addOverridableFields() in this class!! That means, that a value in a
566
     * model instance is only used if it's either the first instance in $arrInstances or "overrideFieldname" is set to true
567
     * in the instance.
568
     *
569
     * @param string $property  The property name to retrieve
570
     * @param array  $instances An array of instances in ascending priority. Instances can be passed in the following form:
571
     *                          ['tl_some_table', $instanceId] or $objInstance
572
     *
573
     * @return mixed
574
     */
575
    public function getOverridableProperty(string $property, array $instances)
576
    {
577
        $result = null;
×
578
        $preparedInstances = [];
×
579

580
        // prepare instances
581
        foreach ($instances as $instance) {
×
582
            if (\is_array($instance)) {
×
583
                if (null !== ($objInstance = $this->container->get('huh.utils.model')->findModelInstanceByPk($instance[0], $instance[1]))) {
×
584
                    $preparedInstances[] = $objInstance;
×
585
                }
586
            } elseif ($instance instanceof Model || \is_object($instance)) {
×
587
                $preparedInstances[] = $instance;
×
588
            }
589
        }
590

591
        foreach ($preparedInstances as $i => $preparedInstance) {
×
592
            if (0 == $i || $preparedInstance->{'override'.ucfirst($property)}) {
×
593
                $result = $preparedInstance->{$property};
×
594
            }
595
        }
596

597
        return $result;
×
598
    }
599

600
    /**
601
     * This function transforms an entity's palette (that can also contain sub palettes and concatenated type selectors) to a flatten
602
     * palette where every field can be overridden.
603
     *
604
     * CAUTION: This function assumes that you have used addOverridableFields() for adding the fields that are overridable. The latter ones
605
     * are $overridableFields
606
     *
607
     * This function is useful if you want to adjust a palette for sub entities that can override properties of their ancestor(s).
608
     * Use $this->getOverridableProperty() for computing the correct value respecting the entity hierarchy.
609
     */
610
    public function flattenPaletteForSubEntities(string $table, array $overridableFields)
611
    {
612
        $this->framework->getAdapter(Controller::class)->loadDataContainer($table);
×
613

614
        $pm = PaletteManipulator::create();
×
615

616
        $dca = &$GLOBALS['TL_DCA'][$table];
×
617
        $arrayUtil = $this->container->get('huh.utils.array');
×
618

619
        // Contao 4.4 fix
620
        $replaceFields = [];
×
621

622
        // palette
623
        foreach ($overridableFields as $field) {
×
624
            if (true === ($dca['fields'][$field]['eval']['submitOnChange'] ?? false)) {
×
625
                unset($dca['fields'][$field]['eval']['submitOnChange']);
×
626

627
                if (\in_array($field, $dca['palettes']['__selector__'])) {
×
628
                    // flatten concatenated type selectors
629
                    foreach ($dca['subpalettes'] as $selector => $subPaletteFields) {
×
630
                        if (false !== strpos($selector, $field.'_')) {
×
631
                            if ($dca['subpalettes'][$selector]) {
×
632
                                $subPaletteFields = explode(',', $dca['subpalettes'][$selector]);
×
633

634
                                foreach (array_reverse($subPaletteFields) as $subPaletteField) {
×
635
                                    $pm->addField($subPaletteField, $field);
×
636
                                }
637
                            }
638

639
                            // remove nested field in order to avoid its normal "selector" behavior
640
                            $arrayUtil->removeValue($field, $dca['palettes']['__selector__']);
×
641
                            unset($dca['subpalettes'][$selector]);
×
642
                        }
643
                    }
644

645
                    // flatten sub palettes
646
                    if (isset($dca['subpalettes'][$field]) && $dca['subpalettes'][$field]) {
×
647
                        $subPaletteFields = explode(',', $dca['subpalettes'][$field]);
×
648

649
                        foreach (array_reverse($subPaletteFields) as $subPaletteField) {
×
650
                            $pm->addField($subPaletteField, $field);
×
651
                        }
652

653
                        // remove nested field in order to avoid its normal "selector" behavior
654
                        $arrayUtil->removeValue($field, $dca['palettes']['__selector__']);
×
655
                        unset($dca['subpalettes'][$field]);
×
656
                    }
657
                }
658
            }
659

660
            $replaceFields[] = $field;
×
661

662
//            $pm->addField('override'.ucfirst($field), $field)->removeField($field);
663
        }
664

665
        $pm->applyToPalette('default', $table);
×
666

667
        foreach ($replaceFields as $replaceField) {
×
668
            $dca['palettes']['default'] = str_replace($replaceField, 'override'.ucfirst($replaceField), $dca['palettes']['default']);
×
669
        }
670
    }
671

672
    /**
673
     * Return if the current alias already exist in table.
674
     *
675
     * @throws \Doctrine\DBAL\DBALException
676
     */
677
    public function aliasExist(string $alias, int $id, string $table, $options = []): bool
678
    {
679
        $aliasField = $options['aliasField'] ?? 'alias';
×
680

681
        $stmt = $this->connection->prepare('SELECT id FROM '.$table.' WHERE '.$aliasField.'=? AND id!=?');
×
682

683
        return $stmt->executeQuery([$alias, $id])->rowCount() > 0;
×
684
    }
685

686
    /**
687
     * Generate an alias with unique check.
688
     *
689
     * @param mixed       $alias       The current alias (if available)
690
     * @param int         $id          The entity's id
691
     * @param string|null $table       The entity's table (pass a comma separated list if the validation should be expanded to multiple tables like tl_news AND tl_member. ATTENTION: the first table needs to be the one we're currently in). Pass null to skip unqiue check.
692
     * @param string      $title       The value to use as a base for the alias
693
     * @param bool        $keepUmlauts Set to true if German umlauts should be kept
694
     *
695
     * @throws \Exception
696
     *
697
     * @return string
698
     */
699
    public function generateAlias(?string $alias, int $id, ?string $table, string $title, bool $keepUmlauts = true, $options = [])
700
    {
701
        $autoAlias = false;
×
702
        $aliasField = $options['aliasField'] ?? 'alias';
×
703

704
        // Generate alias if there is none
705
        if (empty($alias)) {
×
706
            $autoAlias = true;
×
707
            $alias = StringUtil::generateAlias($title);
×
708
        }
709

710
        if (!$keepUmlauts) {
×
711
            $alias = preg_replace(['/ä/i', '/ö/i', '/ü/i', '/ß/i'], ['ae', 'oe', 'ue', 'ss'], $alias);
×
712
        }
713

714
        if (null === $table) {
×
715
            return $alias;
×
716
        }
717

718
        $originalAlias = $alias;
×
719

720
        // multiple tables?
721
        if (false !== strpos($table, ',')) {
×
722
            $tables = explode(',', $table);
×
723

724
            foreach ($tables as $i => $partTable) {
×
725
                // the table in which the entity is
726
                if (0 === $i) {
×
727
                    if ($this->aliasExist($alias, $id, $table, $options)) {
×
728
                        if (!$autoAlias) {
×
729
                            throw new \InvalidArgumentException(sprintf($GLOBALS['TL_LANG']['ERR']['aliasExists'], $alias));
×
730
                        }
731

732
                        $alias = $originalAlias.'-'.$id;
×
733
                    }
734
                } else {
735
                    // another table
736
                    $stmt = $this->connection->prepare("SELECT id FROM {$partTable} WHERE ' . $aliasField . '=?");
×
737

738
                    // Check whether the alias exists
739
                    if ($stmt->execute([$alias])->rowCount() > 0) {
×
740
                        throw new \InvalidArgumentException(sprintf($GLOBALS['TL_LANG']['ERR']['aliasExists'], $alias));
×
741
                    }
742
                }
743
            }
744
        } else {
745
            if (!$this->aliasExist($alias, $id, $table, $options)) {
×
746
                return $alias;
×
747
            }
748

749
            // Check whether the alias exists
750
            if (!$autoAlias) {
×
751
                throw new \Exception(sprintf($GLOBALS['TL_LANG']['ERR']['aliasExists'], $alias));
×
752
            }
753

754
            // Add ID to alias
755
            $alias .= '-'.$id;
×
756
        }
757

758
        return $alias;
×
759
    }
760

761
    public function addAuthorFieldAndCallback(string $table, string $fieldPrefix = '')
762
    {
763
        $this->framework->getAdapter(Controller::class)->loadDataContainer($table);
×
764

765
        // callbacks
766
        $GLOBALS['TL_DCA'][$table]['config']['oncreate_callback']['setAuthorIDOnCreate'] = [self::class, 'setAuthorIDOnCreate'];
×
767
        $GLOBALS['TL_DCA'][$table]['config']['onload_callback']['modifyAuthorPaletteOnLoad'] = [self::class, 'modifyAuthorPaletteOnLoad', true];
×
768

769
        // fields
770
        $GLOBALS['TL_DCA'][$table]['fields'][$fieldPrefix ? $fieldPrefix.ucfirst(static::PROPERTY_AUTHOR_TYPE) : static::PROPERTY_AUTHOR_TYPE] = [
×
771
            'label' => &$GLOBALS['TL_LANG']['MSC']['utilsBundle']['authorType'],
×
772
            'exclude' => true,
×
773
            'filter' => true,
×
774
            'default' => static::AUTHOR_TYPE_NONE,
×
775
            'inputType' => 'select',
×
776
            'options' => [
×
777
                static::AUTHOR_TYPE_NONE,
×
778
                static::AUTHOR_TYPE_MEMBER,
×
779
                static::AUTHOR_TYPE_USER,
×
780
                // session is only added if it's already set in the dca
×
781
            ],
×
782
            'reference' => $GLOBALS['TL_LANG']['MSC']['utilsBundle']['authorType'],
×
783
            'eval' => ['doNotCopy' => true, 'submitOnChange' => true, 'mandatory' => true, 'tl_class' => 'w50 clr'],
×
784
            'sql' => "varchar(255) NOT NULL default 'none'",
×
785
        ];
×
786

787
        $GLOBALS['TL_DCA'][$table]['fields'][$fieldPrefix ? $fieldPrefix.ucfirst(static::PROPERTY_AUTHOR) : static::PROPERTY_AUTHOR] = [
×
788
            'label' => &$GLOBALS['TL_LANG']['MSC']['utilsBundle']['author'],
×
789
            'exclude' => true,
×
790
            'search' => true,
×
791
            'filter' => true,
×
792
            'inputType' => 'select',
×
793
            'default' => '0',
×
794
            'options_callback' => function () {
×
795
                return $this->container->get('huh.utils.choice.model_instance')->getCachedChoices([
×
796
                    'dataContainer' => 'tl_member',
×
797
                    'labelPattern' => '%firstname% %lastname% (ID %id%)',
×
798
                ]);
×
799
            },
×
800
            'save_callback' => [function ($value, $dc) {
×
801
                if (!$value) {
×
802
                    return 0;
×
803
                }
804

805
                return $value;
×
806
            }],
×
807
            'eval' => [
×
808
                'doNotCopy' => true,
×
809
                'chosen' => true,
×
810
                'includeBlankOption' => true,
×
811
                'tl_class' => 'w50',
×
812
            ],
×
813
            'sql' => "varchar(64) NOT NULL default '0'",
×
814
        ];
×
815
    }
816

817
    public function setAuthorIDOnCreate(string $table, int $id, array $row, DataContainer $dc)
818
    {
819
        /** @var Model $model */
820
        $model = $this->container->get(ModelUtil::class)->findModelInstanceByPk($table, $id);
×
821
        /** @var Database $db */
822
        $db = $this->framework->createInstance(Database::class);
×
823

824
        if (null === $model
×
825
            || !$db->fieldExists(static::PROPERTY_AUTHOR_TYPE, $table)
×
826
            || !$db->fieldExists(static::PROPERTY_AUTHOR, $table)) {
×
827
            return false;
×
828
        }
829

830
        $stmt = $db->prepare(
×
831
            'UPDATE '.$model::getTable()
×
832
                        .' SET '.static::PROPERTY_AUTHOR_TYPE.'=?, '.static::PROPERTY_AUTHOR.'=?'
×
833
                        .' WHERE id=?'
×
834
                );
×
835

836
        if ($this->container->get('huh.utils.container')->isFrontend()) {
×
837
            if (FE_USER_LOGGED_IN) {
×
838
                $stmt->execute(static::AUTHOR_TYPE_MEMBER, $this->framework->getAdapter(FrontendUser::class)->getInstance()->id, $model->id);
×
839
            } else {
840
                // php session
841
                $stmt->execute(static::AUTHOR_TYPE_SESSION, session_id(), $model->id);
×
842
            }
843
        } else {
844
            $stmt->execute(static::AUTHOR_TYPE_USER, $this->framework->getAdapter(BackendUser::class)->getInstance()->id, $model->id);
×
845
        }
846
    }
847

848
    public function modifyAuthorPaletteOnLoad(DataContainer $dc)
849
    {
850
        if (!$this->container->get('huh.utils.container')->isBackend()) {
×
851
            return false;
×
852
        }
853

854
        if (null === $dc || !$dc->id || !$dc->table) {
×
855
            return false;
×
856
        }
857

858
        if (null === ($model = $this->container->get('huh.utils.model')->findModelInstanceByPk($dc->table, $dc->id))) {
×
859
            return false;
×
860
        }
861

862
        $dca = &$GLOBALS['TL_DCA'][$dc->table];
×
863

864
        // author handling
865
        if ($model->{static::PROPERTY_AUTHOR_TYPE} == static::AUTHOR_TYPE_NONE) {
×
866
            unset($dca['fields'][static::PROPERTY_AUTHOR]);
×
867
        }
868

869
        if ($model->{static::PROPERTY_AUTHOR_TYPE} == static::AUTHOR_TYPE_USER) {
×
870
            $dca['fields'][static::PROPERTY_AUTHOR]['options_callback'] = function () {
871
                return $this->container->get(ModelInstanceChoice::class)->getCachedChoices([
×
872
                    'dataContainer' => 'tl_user',
×
873
                    'labelPattern' => '%name% (ID %id%)',
×
874
                ]);
×
875
            };
876
        }
877

878
        if ($model->{static::PROPERTY_AUTHOR_TYPE} == static::AUTHOR_TYPE_SESSION) {
×
879
            $dca['fields'][static::PROPERTY_AUTHOR_TYPE]['options'] = array_merge($dca['fields'][static::PROPERTY_AUTHOR_TYPE]['options'], [static::AUTHOR_TYPE_SESSION]);
×
880
            // do not allow to edit in backend
881
            $dca['fields'][static::PROPERTY_AUTHOR_TYPE]['eval']['readonly'] = true;
×
882

883
            unset($dca['fields'][static::PROPERTY_AUTHOR]['options_callback']);
×
884
            $dca['fields'][static::PROPERTY_AUTHOR]['inputType'] = 'text';
×
885
            // do not allow to edit in backend
886
            $dca['fields'][static::PROPERTY_AUTHOR]['eval']['readonly'] = true;
×
887
            $dca['fields'][static::PROPERTY_AUTHOR]['label'][0] = $GLOBALS['TL_LANG']['MSC']['utilsBundle'][static::PROPERTY_AUTHOR_TYPE][self::AUTHOR_TYPE_SESSION];
×
888
        }
889
    }
890

891
    /**
892
     * Returns (nearly) all registered datacontainers as array.
893
     *
894
     * Options:
895
     * - bool onlyTableType: Return only table data containers
896
     *
897
     * @return array
898
     */
899
    public function getDataContainers(array $options = [])
900
    {
901
        $dcaTables = $this->framework->createInstance(Database::class)->listTables();
×
902

903
        if (isset($options['onlyTableType']) && true === $options['onlyTableType']) {
×
904
            return $dcaTables;
×
905
        }
906

907
        foreach ($GLOBALS['BE_MOD'] as $arrSection) {
×
908
            foreach ($arrSection as $strModule => $arrModule) {
×
909
                foreach ($arrModule as $strKey => $varValue) {
×
910
                    if (\is_array($arrModule['tables'] ?? null)) {
×
911
                        $dcaTables = array_merge($dcaTables, $arrModule['tables']);
×
912
                    }
913
                }
914
            }
915
        }
916
        $dcaTables = array_unique($dcaTables);
×
917
        asort($dcaTables);
×
918

919
        return array_values($dcaTables);
×
920
    }
921

922
    /**
923
     * @param bool $includeNotificationCenterPlusTokens
924
     *
925
     * @return array
926
     */
927
    public function getNewNotificationTypeArray($includeNotificationCenterPlusTokens = false)
928
    {
929
        $type = [
×
930
            'recipients' => ['admin_email'],
×
931
            'email_subject' => ['admin_email'],
×
932
            'email_text' => ['admin_email'],
×
933
            'email_html' => ['admin_email'],
×
934
            'file_name' => ['admin_email'],
×
935
            'file_content' => ['admin_email'],
×
936
            'email_sender_name' => ['admin_email'],
×
937
            'email_sender_address' => ['admin_email'],
×
938
            'email_recipient_cc' => ['admin_email'],
×
939
            'email_recipient_bcc' => ['admin_email'],
×
940
            'email_replyTo' => ['admin_email'],
×
941
            'attachment_tokens' => [],
×
942
        ];
×
943

944
        if ($includeNotificationCenterPlusTokens) {
×
945
            foreach ($type as $field => $tokens) {
×
946
                $type[$field] = array_unique(array_merge([
×
947
                    'env_*',
×
948
                    'page_*',
×
949
                    'user_*',
×
950
                    'date',
×
951
                    'last_update',
×
952
                ], $tokens));
×
953
            }
954
        }
955

956
        return $type;
×
957
    }
958

959
    public function activateNotificationType($strGroup, $strType, $arrType)
960
    {
961
        $GLOBALS['NOTIFICATION_CENTER']['NOTIFICATION_TYPE'] = array_merge_recursive(
×
962
            (array) $GLOBALS['NOTIFICATION_CENTER']['NOTIFICATION_TYPE'],
×
963
            [
×
964
                $strGroup => [
×
965
                    $strType => $arrType,
×
966
                ],
×
967
            ]
×
968
        );
×
969
    }
970

971
    /**
972
     * Adds an alias field to the dca and to the desired palettes.
973
     *
974
     * @param       $dca
975
     * @param       $generateAliasCallback mixed The callback to call for generating the alias
976
     * @param       $paletteField          String The field after which to insert the alias field in the palettes
977
     * @param array $palettes              The palettes in which to insert the field
978
     */
979
    public function addAliasToDca(string $dca, $generateAliasCallback, string $paletteField, array $palettes = ['default'])
980
    {
981
        Controller::loadDataContainer($dca);
×
982

983
        $arrDca = &$GLOBALS['TL_DCA'][$dca];
×
984

985
        // add to palettes
986
        foreach ($palettes as $strPalette) {
×
987
            $arrDca['palettes'][$strPalette] = preg_replace('/('.$paletteField.')(;|,)/', '$1,alias$2', $arrDca['palettes'][$strPalette]);
×
988
        }
989

990
        // add field
991
        $arrDca['fields']['alias'] = [
×
992
            'label' => &$GLOBALS['TL_LANG']['MSC']['alias'],
×
993
            'exclude' => true,
×
994
            'search' => true,
×
995
            'inputType' => 'text',
×
996
            'eval' => ['rgxp' => 'alias', 'unique' => true, 'maxlength' => 128, 'tl_class' => 'w50'],
×
997
            'save_callback' => [$generateAliasCallback],
×
998
            'sql' => "varchar(128) COLLATE utf8_bin NOT NULL default ''",
×
999
        ];
×
1000
    }
1001

1002
    /**
1003
     * @param $strField
1004
     * @param $strTable
1005
     *
1006
     * @return mixed
1007
     */
1008
    public function getLocalizedFieldName($strField, $strTable)
1009
    {
1010
        Controller::loadDataContainer($strTable);
×
1011
        System::loadLanguageFile($strTable);
×
1012

1013
        return $GLOBALS['TL_DCA'][$strTable]['fields'][$strField]['label'][0] ?: $strField;
×
1014
    }
1015

1016
    /**
1017
     * Load a data container in a testable way.
1018
     */
1019
    public function loadDc(string $table)
1020
    {
1021
        if (!isset($GLOBALS['TL_DCA'][$table]) || null === $GLOBALS['TL_DCA'][$table]) {
×
1022
            /** @var Controller $controller */
1023
            $controller = $this->framework->getAdapter(Controller::class);
×
1024

1025
            $controller->loadDataContainer($table);
×
1026
        }
1027
    }
1028

1029
    /**
1030
     * Load a language file in a testable way.
1031
     */
1032
    public function loadLanguageFile(string $table)
1033
    {
1034
        /** @var System $system */
1035
        $system = $this->framework->getAdapter(System::class);
×
1036

1037
        $system->loadLanguageFile($table);
×
1038
    }
1039

1040
    public function isDcMultilingual(string $table)
1041
    {
1042
        $this->loadDc($table);
×
1043

1044
        $bundleName = 'Terminal42\DcMultilingualBundle\Terminal42DcMultilingualBundle';
×
1045

1046
        return isset($GLOBALS['TL_DCA'][$table]['config']['dataContainer']) &&
×
1047
            'Multilingual' === $GLOBALS['TL_DCA'][$table]['config']['dataContainer'] &&
×
1048
            $this->container->get('huh.utils.container')->isBundleActive($bundleName);
×
1049
    }
1050

1051
    public function isDcMultilingual3()
1052
    {
1053
        return class_exists('Terminal42\DcMultilingualBundle\Model\Multilingual') &&
×
1054
            !method_exists('Terminal42\DcMultilingualBundle\Model\Multilingual', 'createModelFromDbResult');
×
1055
    }
1056

1057
    public function generateDcOperationsButtons($row, $table, $rootIds = [], $options = [])
1058
    {
1059
        $return = '';
×
1060

1061
        // Edit multiple
1062
        if ('select' == Input::get('act')) {
×
1063
            $return .= '<input type="checkbox" name="IDS[]" id="ids_'.$row['id'].'" class="tl_tree_checkbox" value="'.$row['id'].'">';
×
1064
        } // Regular buttons
1065
        else {
1066
            $return .= $this->doGenerateDcOperationsButtons($row, $table, $rootIds, false, null, $options);
×
1067

1068
            // no picker support due to DataContainer not being extensible
1069
        }
1070

1071
        return $return;
×
1072
    }
1073

1074
    public function doGenerateDcOperationsButtons($arrRow, $strTable, $arrRootIds = [], $blnCircularReference = false, $arrChildRecordIds = null, $options = [])
1075
    {
1076
        if (empty($GLOBALS['TL_DCA'][$strTable]['list']['operations'])) {
×
1077
            return '';
×
1078
        }
1079

1080
        $return = '';
×
1081

1082
        $skipOperations = $options['skipOperations'] ?? [];
×
1083
        $operations = $options['operations'] ?? array_keys($GLOBALS['TL_DCA'][$strTable]['list']['operations']);
×
1084

1085
        if (!empty($skipOperations) && !isset($options['operations'])) {
×
1086
            $operations = array_diff($operations, $skipOperations);
×
1087
        }
1088

1089
        foreach ($GLOBALS['TL_DCA'][$strTable]['list']['operations'] as $k => $v) {
×
1090
            if (!\in_array($k, $operations)) {
×
1091
                continue;
×
1092
            }
1093

1094
            $v = \is_array($v) ? $v : [$v];
×
1095
            $id = StringUtil::specialchars(rawurldecode($arrRow['id']));
×
1096

1097
            $label = isset($v['label']) ? (\is_array($v['label']) ? $v['label'][0] : $v['label']) : $k;
×
1098
            $title = sprintf(isset($v['label']) ? (\is_array($v['label']) ? $v['label'][1] : $v['label']) : $k, $id);
×
1099
            $attributes = ('' != $v['attributes']) ? ' '.ltrim(sprintf($v['attributes'], $id, $id)) : '';
×
1100

1101
            parse_str(StringUtil::decodeEntities($v['href'] ?? ''), $params);
×
1102

1103
            if (version_compare(VERSION, '4.13', '>=') && \in_array($k, ['toggle', 'feature'])) {
×
1104
                $state = $arrRow[$params['field']] ? 1 : 0;
×
1105

1106
                if ($v['reverse'] ?? false) {
×
1107
                    $state = $arrRow[$params['field']] ? 0 : 1;
×
1108
                }
1109

1110
                $icon = $v['icon'];
×
1111
                $_icon = pathinfo($v['icon'], \PATHINFO_FILENAME).'_.'.pathinfo($v['icon'], \PATHINFO_EXTENSION);
×
1112

1113
                if (false !== strpos($v['icon'], '/')) {
×
1114
                    $_icon = \dirname($v['icon']).'/'.$_icon;
×
1115
                }
1116

1117
                if ('visible.svg' == $icon) {
×
1118
                    $_icon = 'invisible.svg';
×
1119
                }
1120

1121
                if (false === strpos($attributes, 'onclick')) {
×
1122
                    $attributes = sprintf(
×
1123
                        'onclick="Backend.getScrollOffset();return AjaxRequest.toggleField(this,'.($state ? 'true' : 'false').')"',
×
1124
                        $id, $id
×
1125
                    );
×
1126
                }
1127
            }
1128

1129
            // Add the key as CSS class
1130
            if (false !== strpos($attributes, 'class="')) {
×
1131
                $attributes = str_replace('class="', 'class="'.$k.' ', $attributes);
×
1132
            } else {
1133
                $attributes = ' class="'.$k.'"'.$attributes;
×
1134
            }
1135

1136
            // Call a custom function instead of using the default button
1137
            if (\is_array($v['button_callback'])) {
×
1138
                $callback = System::importStatic($v['button_callback'][0]);
×
1139
                $return .= $callback->{$v['button_callback'][1]}($arrRow, $v['href'], $label, $title, $v['icon'], $attributes, $strTable, $arrRootIds, $arrChildRecordIds, $blnCircularReference, null, null, $this);
×
1140

1141
                continue;
×
1142
            } elseif (\is_callable($v['button_callback'])) {
×
1143
                $return .= $v['button_callback']($arrRow, $v['href'], $label, $title, $v['icon'], $attributes, $strTable, $arrRootIds, $arrChildRecordIds, $blnCircularReference, null, null, $this);
×
1144

1145
                continue;
×
1146
            }
1147

1148
            // Generate all buttons except "move up" and "move down" buttons
1149
            if ('move' != $k && 'move' != $v) {
×
1150
                if ('show' == $k) {
×
1151
                    $return .= '<a href="'.Controller::addToUrl($v['href'].'&amp;id='.$arrRow['id'].'&amp;popup=1&amp;rt='.\RequestToken::get()).'" title="'.StringUtil::specialchars($title).'" onclick="Backend.openModalIframe({\'title\':\''.StringUtil::specialchars(str_replace("'", "\\'",
×
1152
                            sprintf($GLOBALS['TL_LANG'][$strTable]['show'][1], $arrRow['id']))).'\',\'url\':this.href});return false"'.$attributes.'>'.Image::getHtml($v['icon'], $label).'</a> ';
×
1153
                } else {
1154
                    $href = Controller::addToUrl($v['href'].'&amp;id='.$arrRow['id'].(Input::get('nb') ? '&amp;nc=1' : '')).'&amp;rt='.RequestToken::get();
×
1155

1156
                    if (version_compare(VERSION, '4.13', '>=') && \in_array($k, ['toggle', 'feature'])) {
×
1157
                        $icon = Image::getHtml($state ? $icon : $_icon, $label, 'data-icon="'.Image::getPath($icon).'" data-icon-disabled="'.Image::getPath($_icon).'" data-state="'.$state.'"');
×
1158
                    } else {
1159
                        $icon = Image::getHtml($v['icon'], $label);
×
1160
                    }
1161

1162
                    $return .= '<a href="'.$href.'" title="'.StringUtil::specialchars($title).'"'.$attributes.'>'.$icon.'</a> ';
×
1163
                }
1164

1165
                continue;
×
1166
            }
1167
        }
1168

1169
        return trim($return);
×
1170
    }
1171

1172
    public function generateSitemap()
1173
    {
1174
        $automator = System::importStatic('Automator');
×
1175
        $automator->generateSitemap();
×
1176
    }
1177

1178
    /**
1179
     * Mostly used for Form::prepareSpecialValueForOutput().
1180
     *
1181
     * @param $activeRecord
1182
     */
1183
    public function getDCTable(string $table, $activeRecord): DC_Table_Utils
1184
    {
1185
        $dc = new DC_Table_Utils($table);
×
1186
        $dc->activeRecord = $activeRecord;
×
1187
        $dc->id = $activeRecord->id;
×
1188

1189
        return $dc;
×
1190
    }
1191

1192
    public function getAuthorNameByUserId($id)
1193
    {
1194
        if (null !== ($user = $this->container->get('huh.utils.model')->findModelInstanceByPk('tl_user', $id))) {
×
1195
            return $user->name;
×
1196
        }
1197

1198
        return false;
×
1199
    }
1200

1201
    public function getAuthorNameLinkByUserId($id)
1202
    {
1203
        if (null !== ($user = $this->container->get('huh.utils.model')->findModelInstanceByPk('tl_user', $id))) {
×
1204
            return '<strong>'.Controller::replaceInsertTags('{{email_open::'.$user->email.'}}').$user->name.'</a></strong>';
×
1205
        }
1206

1207
        return false;
×
1208
    }
1209

1210
    public function setFieldsToReadOnly(&$dca, array $config = [])
1211
    {
1212
        $skipFields = $config['skipFields'] ?? [];
×
1213
        $fields = $config['fields'] ?? [];
×
1214

1215
        foreach ($dca['fields'] as $field => &$data) {
×
1216
            if (!empty($fields)) {
×
1217
                if (!\in_array($field, $fields)) {
×
1218
                    continue;
×
1219
                }
1220
            } elseif (\in_array($field, $skipFields)) {
×
1221
                continue;
×
1222
            }
1223

1224
            switch ($data['inputType']) {
×
1225
                case 'checkbox':
×
1226
                case 'radio':
×
1227
                case 'radioTable':
×
1228
                    $data['eval']['disabled'] = true;
×
1229

1230
                    break;
×
1231

1232
                case 'select':
×
1233
                case 'imageSize':
×
1234
                    $data['eval']['readonly'] = true;
×
1235
                    $data['eval']['class'] = 'readonly';
×
1236

1237
                    break;
×
1238

1239
                case 'fileTree':
×
1240
                case 'metaWizard':
×
1241
                case 'tagsinput':
×
1242
                    $data['eval']['readonly'] = true;
×
1243
                    $data['eval']['tl_class'] = $data['eval']['tl_class'].' readonly';
×
1244

1245
                    break;
×
1246

1247
                case 'multiColumnEditor':
×
1248
                    $data['eval']['readonly'] = true;
×
1249

1250
                    $this->setFieldsToReadOnly($data['eval']['multiColumnEditor'], $config);
×
1251

1252
                    break;
×
1253

1254
                default:
1255
                    $data['eval']['readonly'] = true;
×
1256

1257
                    // TODO dispatch event for custom
1258
                    break;
×
1259
            }
1260
        }
1261
    }
1262

1263
    public function getTranslatedModuleNameByTable(string $table)
1264
    {
1265
        foreach ($GLOBALS['BE_MOD'] as $groupName => $groupModules) {
×
1266
            if (empty($groupModules)) {
×
1267
                continue;
×
1268
            }
1269

1270
            foreach ($groupModules as $moduleName => $moduleConfig) {
×
1271
                if (!isset($moduleConfig['tables']) || !\is_array($moduleConfig['tables'])) {
×
1272
                    continue;
×
1273
                }
1274

1275
                if (\in_array($table, $moduleConfig['tables'])) {
×
1276
                    return StringUtil::specialchars($GLOBALS['TL_LANG']['MOD'][$moduleName][0]);
×
1277
                }
1278
            }
1279
        }
1280

1281
        return false;
×
1282
    }
1283

1284
    public function getRenderedDiff(string $table, array $source, array $target, array $config = [])
1285
    {
1286
        $result = '';
×
1287

1288
        $skipFields = $config['skipFields'] ?? [];
×
1289
        $restrictFields = $config['restrictFields'] ?? [];
×
1290
        $tableCallbacks = $config['tableCallbacks'] ?? [];
×
1291

1292
        $this->loadDc($table);
×
1293
        $this->loadLanguageFile($table);
×
1294

1295
        $dca = $GLOBALS['TL_DCA'][$table];
×
1296

1297
        $arrayUtil = System::getContainer()->get('huh.utils.array');
×
1298

1299
        // Get the order fields
1300
        $dcaExtractor = DcaExtractor::getInstance($table);
×
1301
        $fields = $dcaExtractor->getFields();
×
1302
        $orderFields = $dcaExtractor->getOrderFields();
×
1303

1304
        // Find the changed fields and highlight the changes
1305
        foreach ($target as $k => $v) {
×
1306
            if (empty($restrictFields) && \in_array($k, $skipFields)) {
×
1307
                continue;
×
1308
            }
1309

1310
            if (!empty($restrictFields) && !\in_array($k, $restrictFields)) {
×
1311
                continue;
×
1312
            }
1313

1314
            if ($source[$k] != $target[$k]) {
×
1315
                if ($dca['fields'][$k]['eval']['doNotShow'] || $dca['fields'][$k]['eval']['hideInput']) {
×
1316
                    continue;
×
1317
                }
1318

1319
                $isBinary = 0 === strncmp($fields[$k], 'binary(', 7) || 0 === strncmp($fields[$k], 'blob ', 5);
×
1320

1321
                if ($dca['fields'][$k]['eval']['multiple'] || \in_array($k, $orderFields)) {
×
1322
                    if (isset($dca['fields'][$k]['eval']['csv'])) {
×
1323
                        $delimiter = $dca['fields'][$k]['eval']['csv'];
×
1324

1325
                        if (isset($target[$k])) {
×
1326
                            $target[$k] = preg_replace('/'.preg_quote($delimiter, ' ?/').'/', $delimiter.' ', $target[$k]);
×
1327
                        }
1328

1329
                        if (isset($source[$k])) {
×
1330
                            $source[$k] = preg_replace('/'.preg_quote($delimiter, ' ?/').'/', $delimiter.' ', $source[$k]);
×
1331
                        }
1332
                    } else {
1333
                        // Convert serialized arrays into strings
1334
                        if (\is_array(($tmp = StringUtil::deserialize($target[$k]))) && !\is_array($target[$k])) {
×
1335
                            $target[$k] = $arrayUtil->implodeRecursive($tmp, $isBinary);
×
1336
                        }
1337

1338
                        if (\is_array(($tmp = StringUtil::deserialize($source[$k]))) && !\is_array($source[$k])) {
×
1339
                            $source[$k] = $arrayUtil->implodeRecursive($tmp, $isBinary);
×
1340
                        }
1341
                    }
1342
                }
1343

1344
                unset($tmp);
×
1345

1346
                // Convert binary UUIDs to their hex equivalents (see #6365)
1347
                if ($isBinary) {
×
1348
                    if (Validator::isBinaryUuid($target[$k])) {
×
1349
                        $target[$k] = StringUtil::binToUuid($target[$k]);
×
1350
                    }
1351

1352
                    if (Validator::isBinaryUuid($source[$k])) {
×
1353
                        $source[$k] = StringUtil::binToUuid($source[$k]);
×
1354
                    }
1355
                }
1356

1357
                // Convert date fields
1358
                if ('date' == $dca['fields'][$k]['eval']['rgxp']) {
×
1359
                    $target[$k] = \Date::parse(Config::get('dateFormat'), $target[$k] ?: '');
×
1360
                    $source[$k] = \Date::parse(Config::get('dateFormat'), $source[$k] ?: '');
×
1361
                } elseif ('time' == $dca['fields'][$k]['eval']['rgxp']) {
×
1362
                    $target[$k] = \Date::parse(Config::get('timeFormat'), $target[$k] ?: '');
×
1363
                    $source[$k] = \Date::parse(Config::get('timeFormat'), $source[$k] ?: '');
×
1364
                } elseif ('datim' == $dca['fields'][$k]['eval']['rgxp'] || 'tstamp' == $k) {
×
1365
                    $target[$k] = \Date::parse(Config::get('datimFormat'), $target[$k] ?: '');
×
1366
                    $source[$k] = \Date::parse(Config::get('datimFormat'), $source[$k] ?: '');
×
1367
                }
1368

1369
                // Decode entities if the "decodeEntities" flag is not set (see #360)
1370
                if (empty($dca['fields'][$k]['eval']['decodeEntities'])) {
×
1371
                    $target[$k] = StringUtil::decodeEntities($target[$k]);
×
1372
                    $source[$k] = StringUtil::decodeEntities($source[$k]);
×
1373
                }
1374

1375
                // Convert strings into arrays
1376
                if (!\is_array($target[$k])) {
×
1377
                    $target[$k] = explode("\n", $target[$k]);
×
1378
                }
1379

1380
                if (!\is_array($source[$k])) {
×
1381
                    $source[$k] = explode("\n", $source[$k]);
×
1382
                }
1383

1384
                // custom callbacks to modify data
1385
                if (isset($tableCallbacks[$table]) && \is_callable($tableCallbacks[$table])) {
×
1386
                    $tableCallbacks[$table]($k, $v, $source, $target);
×
1387
                }
1388

1389
                $diff = new \Diff($source[$k], $target[$k]);
×
1390
                $result .= $diff->render(new DiffRenderer(['field' => ($dca['fields'][$k]['label'][0] ?: (isset($GLOBALS['TL_LANG']['MSC'][$k]) ? (\is_array($GLOBALS['TL_LANG']['MSC'][$k]) ? $GLOBALS['TL_LANG']['MSC'][$k][0] : $GLOBALS['TL_LANG']['MSC'][$k]) : $k))]));
×
1391
            }
1392
        }
1393

1394
        // Identical versions
1395
        if ('' == $result) {
×
1396
            $result = '<p>'.$GLOBALS['TL_LANG']['MSC']['identicalVersions'].'</p>';
×
1397
        }
1398

1399
        return $result;
×
1400
    }
1401

1402
    public function prepareRowEntryForList($table, string $field, $value)
1403
    {
1404
        $this->loadDc($table);
×
1405
        $this->loadLanguageFile($table);
×
1406

1407
        $dca = $GLOBALS['TL_DCA'][$table];
×
1408

1409
        $arrayUtil = System::getContainer()->get('huh.utils.array');
×
1410

1411
        // Get the order fields
1412
        $dcaExtractor = DcaExtractor::getInstance($table);
×
1413
        $fields = $dcaExtractor->getFields();
×
1414
        $orderFields = $dcaExtractor->getOrderFields();
×
1415

1416
        if ($dca['fields'][$field]['eval']['doNotShow'] || $dca['fields'][$field]['eval']['hideInput']) {
×
1417
            return '';
×
1418
        }
1419

1420
        $sql = \is_array($fields[$field]) ? $fields[$field]['type'] : $fields[$field];
×
1421

1422
        $isBinary = 0 === strncmp($sql, 'binary(', 7) || 0 === strncmp($sql, 'blob ', 5);
×
1423

1424
        if ($dca['fields'][$field]['eval']['multiple'] || \in_array($field, $orderFields)) {
×
1425
            if (isset($dca['fields'][$field]['eval']['csv'])) {
×
1426
                $delimiter = $dca['fields'][$field]['eval']['csv'];
×
1427

1428
                if ($value) {
×
1429
                    $value = preg_replace('/'.preg_quote($delimiter, ' ?/').'/', $delimiter.' ', $value);
×
1430
                }
1431
            } else {
1432
                // Convert serialized arrays into strings
1433
                if (\is_array(($tmp = StringUtil::deserialize($value))) && !\is_array($value)) {
×
1434
                    $value = $arrayUtil->implodeRecursive($tmp, $isBinary);
×
1435
                }
1436
            }
1437
        }
1438

1439
        unset($tmp);
×
1440

1441
        // Convert binary UUIDs to their hex equivalents (see #6365)
1442
        if ($isBinary) {
×
1443
            if (Validator::isBinaryUuid($value)) {
×
1444
                $value = StringUtil::binToUuid($value);
×
1445
            }
1446
        }
1447

1448
        // Convert date fields
1449
        if ('date' == $dca['fields'][$field]['eval']['rgxp']) {
×
1450
            $value = \Date::parse(Config::get('dateFormat'), $value ?: '');
×
1451
        } elseif ('time' == $dca['fields'][$field]['eval']['rgxp']) {
×
1452
            $value = \Date::parse(Config::get('timeFormat'), $value ?: '');
×
1453
        } elseif ('datim' == $dca['fields'][$field]['eval']['rgxp'] || 'tstamp' == $field) {
×
1454
            $value = \Date::parse(Config::get('datimFormat'), $value ?: '');
×
1455
        }
1456

1457
        // Decode entities if the "decodeEntities" flag is not set (see #360)
1458
        if (empty($dca['fields'][$field]['eval']['decodeEntities'])) {
×
1459
            $value = StringUtil::decodeEntities($value);
×
1460
        }
1461

1462
        return $value;
×
1463
    }
1464

1465
    public function getFieldLabel(string $table, string $field)
1466
    {
1467
        $this->loadDc($table);
×
1468
        $this->loadLanguageFile($table);
×
1469

1470
        $dca = $GLOBALS['TL_DCA'][$table];
×
1471

1472
        return $dca['fields'][$field]['label'][0] ?: (isset($GLOBALS['TL_LANG']['MSC'][$field]) ? (\is_array($GLOBALS['TL_LANG']['MSC'][$field]) ? $GLOBALS['TL_LANG']['MSC'][$field][0] : $GLOBALS['TL_LANG']['MSC'][$field]) : $field);
×
1473
    }
1474

1475
    /**
1476
     * Returns the set of pid and sorting to be used in an sql update statement. Also updates the existing records according to the usage.
1477
     *
1478
     * The method can be used in several ways:
1479
     *
1480
     * <ul>
1481
     *   <li>Insert in an archive of a certain pid as first item: $pid must be set (0 is also ok), $insertAfterId needs to be null</li>
1482
     *   <li>Insert after a record of a certain id: $insertAfterId must be set, $pid can be set if necessary</li>
1483
     * </ul>
1484
     *
1485
     * @example
1486
     *
1487
     * // insert a new record after another one with the ID 82
1488
     *
1489
     * $news = new \Contao\NewsModel();
1490
     * $news->pid = 3;
1491
     * $news->tstamp = time();
1492
     * $news->title = 'Something';
1493
     * $news->save();
1494
     * $set = System::getContainer()->get('huh.utils.dca')->getNewSortingPosition(
1495
     *   'tl_news', $news->id, 3, 82
1496
     * );
1497
     *
1498
     * // store the returned set to the news record created above as usual
1499
     *
1500
     * Hint: Mostly taken from DC_Table::getNewPosition(). Removed: handling if only a pid field is present, mode handling (since we don't have it in this context).
1501
     */
1502
    public function getNewSortingPosition(string $table, int $id, $pid = null, $insertAfterId = null): array
1503
    {
1504
        $set = [];
×
1505

1506
        /* @var Database $db */
1507
        if (!($db = $this->framework->createInstance(Database::class))) {
×
1508
            return $set;
×
1509
        }
1510

1511
        // If there is pid and sorting
1512
        if ($db->fieldExists('pid', $table) && $db->fieldExists('sorting', $table)) {
×
1513
            // PID is set (insert after or into the parent record)
1514
            if (is_numeric($pid)) {
×
1515
                // ID is set (insert after the current record)
1516
                if ($insertAfterId) {
×
1517
                    $objCurrentRecord = $db->prepare("SELECT * FROM $table WHERE id=? AND pid=?")
×
1518
                        ->limit(1)
×
1519
                        ->execute($insertAfterId, $pid);
×
1520

1521
                    // Select current record
1522
                    if ($objCurrentRecord->numRows) {
×
1523
                        $newSorting = null;
×
1524
                        $curSorting = $objCurrentRecord->sorting;
×
1525

1526
                        $objNextSorting = $db->prepare("SELECT MIN(sorting) AS sorting FROM $table WHERE sorting>? AND pid=?")
×
1527
                            ->execute($curSorting, $pid);
×
1528

1529
                        // Select sorting value of the next record
1530
                        if ($objNextSorting->numRows && null !== $objNextSorting->sorting) {
×
1531
                            $nxtSorting = $objNextSorting->sorting;
×
1532

1533
                            // Resort if the new sorting value is no integer or bigger than a MySQL integer field
1534
                            if (0 != (($curSorting + $nxtSorting) % 2) || $nxtSorting >= 4294967295) {
×
1535
                                $count = 1;
×
1536

1537
                                $objNewSorting = $db->prepare("SELECT id, sorting FROM $table WHERE pid=? AND id!=? ORDER BY sorting")->execute($pid, $id);
×
1538

1539
                                while ($objNewSorting->next()) {
×
1540
                                    $db->prepare("UPDATE $table SET sorting=? WHERE id=? AND pid=?")
×
1541
                                        ->execute(($count++ * 128), $objNewSorting->id, $pid);
×
1542

1543
                                    if ($objNewSorting->sorting == $curSorting) {
×
1544
                                        $newSorting = ($count++ * 128);
×
1545
                                    }
1546
                                }
1547
                            } // Else new sorting = (current sorting + next sorting) / 2
1548
                            else {
1549
                                $newSorting = (($curSorting + $nxtSorting) / 2);
×
1550
                            }
1551
                        } // Else new sorting = (current sorting + 128)
1552
                        else {
1553
                            $newSorting = ($curSorting + 128);
×
1554
                        }
1555

1556
                        // Set new sorting
1557
                        $set['sorting'] = (int) $newSorting;
×
1558

1559
                        return $set;
×
1560
                    }
1561
                } else {
1562
                    // insert in first place
1563
                    $newPID = null;
×
1564
                    $newSorting = null;
×
1565

1566
                    $newPID = $pid;
×
1567

1568
                    $minSorting = $db->prepare("SELECT MIN(sorting) AS sorting FROM $table WHERE pid=?")->execute($pid);
×
1569

1570
                    // Select sorting value of the first record
1571
                    if ($minSorting->numRows) {
×
1572
                        $curSorting = $minSorting->sorting;
×
1573

1574
                        // Resort if the new sorting value is not an integer or smaller than 1
1575
                        if (0 != ($curSorting % 2) || $curSorting < 1) {
×
1576
                            $objNewSorting = $db->prepare("SELECT id FROM $table WHERE pid=? ORDER BY sorting")->execute($pid);
×
1577

1578
                            $count = 2;
×
1579
                            $newSorting = 128;
×
1580

1581
                            while ($objNewSorting->next()) {
×
1582
                                $db->prepare("UPDATE $table SET sorting=? WHERE id=?")
×
1583
                                    ->limit(1)
×
1584
                                    ->execute(($count++ * 128), $objNewSorting->id);
×
1585
                            }
1586
                        } // Else new sorting = (current sorting / 2)
1587
                        else {
1588
                            $newSorting = ($curSorting / 2);
×
1589
                        }
1590
                    } // Else new sorting = 128
1591
                    else {
1592
                        $newSorting = 128;
×
1593
                    }
1594

1595
                    // Set new sorting and new parent ID
1596
                    $set['pid'] = (int) $newPID;
×
1597
                    $set['sorting'] = (int) $newSorting;
×
1598
                }
1599
            }
1600
        } // If there is only sorting
1601
        elseif ($db->fieldExists('sorting', $table)) {
×
1602
            // ID is set (insert after the current record)
1603
            if ($insertAfterId) {
×
1604
                $objCurrentRecord = $db->prepare("SELECT * FROM $table WHERE id=?")
×
1605
                    ->limit(1)
×
1606
                    ->execute($insertAfterId);
×
1607

1608
                // Select current record
1609
                if ($objCurrentRecord->numRows) {
×
1610
                    $newSorting = null;
×
1611
                    $curSorting = $objCurrentRecord->sorting;
×
1612

1613
                    $objNextSorting = $db->prepare("SELECT MIN(sorting) AS sorting FROM $table WHERE sorting>?")
×
1614
                        ->execute($curSorting);
×
1615

1616
                    // Select sorting value of the next record
1617
                    if ($objNextSorting->numRows) {
×
1618
                        $nxtSorting = $objNextSorting->sorting;
×
1619

1620
                        // Resort if the new sorting value is no integer or bigger than a MySQL integer field
1621
                        if (0 != (($curSorting + $nxtSorting) % 2) || $nxtSorting >= 4294967295) {
×
1622
                            $count = 1;
×
1623

1624
                            $objNewSorting = $db->execute("SELECT id, sorting FROM $table ORDER BY sorting");
×
1625

1626
                            while ($objNewSorting->next()) {
×
1627
                                $db->prepare("UPDATE $table SET sorting=? WHERE id=?")
×
1628
                                    ->execute(($count++ * 128), $objNewSorting->id);
×
1629

1630
                                if ($objNewSorting->sorting == $curSorting) {
×
1631
                                    $newSorting = ($count++ * 128);
×
1632
                                }
1633
                            }
1634
                        } // Else new sorting = (current sorting + next sorting) / 2
1635
                        else {
1636
                            $newSorting = (($curSorting + $nxtSorting) / 2);
×
1637
                        }
1638
                    } // Else new sorting = (current sorting + 128)
1639
                    else {
1640
                        $newSorting = ($curSorting + 128);
×
1641
                    }
1642

1643
                    // Set new sorting
1644
                    $set['sorting'] = (int) $newSorting;
×
1645

1646
                    return $set;
×
1647
                }
1648
            }
1649

1650
            // ID is not set or not found (insert at the end)
1651
            $objNextSorting = $db->execute('SELECT MAX(sorting) AS sorting FROM '.$table);
×
1652
            $set['sorting'] = ((int) $objNextSorting->sorting + 128);
×
1653
        }
1654

1655
        return $set;
×
1656
    }
1657

1658
    /**
1659
     * Taken from \Contao\DataContainer.
1660
     */
1661
    public function getCurrentPaletteName(string $table, int $id): ?string
1662
    {
1663
        // Check whether there are selector fields
1664
        if (!empty($GLOBALS['TL_DCA'][$table]['palettes']['__selector__'])) {
×
1665
            $sValues = [];
×
1666
            $subpalettes = [];
×
1667

1668
            $objFields = Database::getInstance()->prepare('SELECT * FROM '.$table.' WHERE id=?')
×
1669
                ->limit(1)
×
1670
                ->execute($id);
×
1671

1672
            // Get selector values from DB
1673
            if ($objFields->numRows > 0) {
×
1674
                foreach ($GLOBALS['TL_DCA'][$table]['palettes']['__selector__'] as $name) {
×
1675
                    $trigger = $objFields->$name;
×
1676

1677
                    // Overwrite the trigger
1678
                    if (Input::post('FORM_SUBMIT') == $table) {
×
1679
                        $key = ('editAll' == Input::get('act')) ? $name.'_'.$id : $name;
×
1680

1681
                        if (isset($_POST[$key])) {
×
1682
                            $trigger = Input::post($key);
×
1683
                        }
1684
                    }
1685

1686
                    if ($trigger) {
×
1687
                        if ('checkbox' == ($GLOBALS['TL_DCA'][$table]['fields'][$name]['inputType'] ?? null) && !($GLOBALS['TL_DCA'][$table]['fields'][$name]['eval']['multiple'] ?? null)) {
×
1688
                            $sValues[] = $name;
×
1689

1690
                            // Look for a subpalette
1691
                            if (isset($GLOBALS['TL_DCA'][$table]['subpalettes'][$name])) {
×
1692
                                $subpalettes[$name] = $GLOBALS['TL_DCA'][$table]['subpalettes'][$name];
×
1693
                            }
1694
                        } else {
1695
                            $sValues[] = $trigger;
×
1696
                            $key = $name.'_'.$trigger;
×
1697

1698
                            // Look for a subpalette
1699
                            if (isset($GLOBALS['TL_DCA'][$table]['subpalettes'][$key])) {
×
1700
                                $subpalettes[$name] = $GLOBALS['TL_DCA'][$table]['subpalettes'][$key];
×
1701
                            }
1702
                        }
1703
                    }
1704
                }
1705
            }
1706

1707
            // Build possible palette names from the selector values
1708
            if (empty($sValues)) {
×
1709
                $names = ['default'];
×
1710
            } elseif (\count($sValues) > 1) {
×
1711
                foreach ($sValues as $k => $v) {
×
1712
                    // Unset selectors that just trigger subpalettes (see #3738)
1713
                    if (isset($GLOBALS['TL_DCA'][$table]['subpalettes'][$v])) {
×
1714
                        unset($sValues[$k]);
×
1715
                    }
1716
                }
1717

1718
                $names = $this->combiner($sValues);
×
1719
            } else {
1720
                $names = [$sValues[0]];
×
1721
            }
1722

1723
            // Get an existing palette
1724
            foreach ($names as $paletteName) {
×
1725
                if (isset($GLOBALS['TL_DCA'][$table]['palettes'][$paletteName])) {
×
1726
                    return $paletteName;
×
1727
                }
1728
            }
1729
        }
1730

1731
        return null;
×
1732
    }
1733

1734
    /**
1735
     * Returns true if the field is in at least one sub palette.
1736
     */
1737
    public function isSubPaletteField(string $field, string $table): bool
1738
    {
1739
        $this->framework->getAdapter(Controller::class)->loadDataContainer($table);
×
1740

1741
        if (!isset($GLOBALS['TL_DCA'][$table]['subpalettes']) || !\is_array($GLOBALS['TL_DCA'][$table]['subpalettes'])) {
×
1742
            return false;
×
1743
        }
1744

1745
        foreach ($GLOBALS['TL_DCA'][$table]['subpalettes'] as $fields) {
×
1746
            if (\in_array($field, explode(',', $fields))) {
×
1747
                return true;
×
1748
            }
1749
        }
1750

1751
        return false;
×
1752
    }
1753

1754
    /**
1755
     * Returns the selector of the sub palette a field is placed in. Currently doesn't support fields in multiple sub palettes.
1756
     */
1757
    public function getSubPaletteFieldSelector(string $field, string $table): string
1758
    {
1759
        $this->framework->getAdapter(Controller::class)->loadDataContainer($table);
×
1760

1761
        if (!isset($GLOBALS['TL_DCA'][$table]['subpalettes']) || !\is_array($GLOBALS['TL_DCA'][$table]['subpalettes'])) {
×
1762
            return false;
×
1763
        }
1764

1765
        foreach ($GLOBALS['TL_DCA'][$table]['subpalettes'] as $name => $fields) {
×
1766
            if (\in_array($field, explode(',', $fields))) {
×
1767
                return $name;
×
1768
            }
1769
        }
1770

1771
        return false;
×
1772
    }
1773

1774
    /**
1775
     * Taken from \Contao\DataContainer.
1776
     */
1777
    private function combiner($names)
1778
    {
1779
        $return = [''];
×
1780
        $names = array_values($names);
×
1781

1782
        for ($i = 0, $c = \count($names); $i < $c; ++$i) {
×
1783
            $buffer = [];
×
1784

1785
            foreach ($return as $k => $v) {
×
1786
                $buffer[] = (0 == $k % 2) ? $v : $v.$names[$i];
×
1787
                $buffer[] = (0 == $k % 2) ? $v.$names[$i] : $v;
×
1788
            }
1789

1790
            $return = $buffer;
×
1791
        }
1792

1793
        return array_filter($return);
×
1794
    }
1795
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc