1<?php
2
3declare(strict_types=1);
4
5/*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18namespace TYPO3\CMS\Backend\Controller\Wizard;
19
20use Psr\Http\Message\ResponseInterface;
21use Psr\Http\Message\ServerRequestInterface;
22use TYPO3\CMS\Backend\Template\Components\ButtonBar;
23use TYPO3\CMS\Backend\Template\ModuleTemplate;
24use TYPO3\CMS\Backend\Utility\BackendUtility;
25use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
26use TYPO3\CMS\Core\DataHandling\DataHandler;
27use TYPO3\CMS\Core\Http\HtmlResponse;
28use TYPO3\CMS\Core\Http\RedirectResponse;
29use TYPO3\CMS\Core\Imaging\Icon;
30use TYPO3\CMS\Core\Imaging\IconFactory;
31use TYPO3\CMS\Core\Utility\GeneralUtility;
32use TYPO3\CMS\Core\Utility\MathUtility;
33
34/**
35 * Script Class for rendering the Table Wizard
36 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
37 */
38class TableController extends AbstractWizardController
39{
40    /**
41     * If TRUE, <input> fields are shown instead of textareas.
42     *
43     * @var bool
44     */
45    protected $inputStyle = false;
46
47    /**
48     * If set, the string version of the content is interpreted/written as XML
49     * instead of the original line-based kind. This variable still needs binding
50     * to the wizard parameters - but support is ready!
51     *
52     * @var int
53     */
54    protected $xmlStorage = 0;
55
56    /**
57     * Number of new rows to add in bottom of wizard
58     *
59     * @var int
60     */
61    protected $numNewRows = 1;
62
63    /**
64     * Name of field in parent record which MAY contain the number of columns for the table
65     * here hardcoded to the value of tt_content. Should be set by FormEngine parameters (from P)
66     *
67     * @var string
68     */
69    protected $colsFieldName = 'cols';
70
71    /**
72     * Wizard parameters, coming from FormEngine linking to the wizard.
73     *
74     * @var array
75     */
76    protected $P;
77
78    /**
79     * The array which is constantly submitted by the multidimensional form of this wizard.
80     *
81     * @var array
82     */
83    protected $TABLECFG;
84
85    /**
86     * Table parsing
87     * quoting of table cells
88     *
89     * @var string
90     */
91    protected $tableParsing_quote;
92
93    /**
94     * delimiter between table cells
95     *
96     * @var string
97     */
98    protected $tableParsing_delimiter;
99
100    /**
101     * @var IconFactory
102     */
103    protected $iconFactory;
104
105    /**
106     * ModuleTemplate object
107     *
108     * @var ModuleTemplate
109     */
110    protected $moduleTemplate;
111
112    /**
113     * Injects the request object for the current request or subrequest
114     * As this controller goes only through the main() method, it is rather simple for now
115     *
116     * @param ServerRequestInterface $request
117     * @return ResponseInterface
118     */
119    public function mainAction(ServerRequestInterface $request): ResponseInterface
120    {
121        $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
122        $this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_wizards.xlf');
123        $this->init($request);
124
125        $normalizedParams = $request->getAttribute('normalizedParams');
126        $requestUri = $normalizedParams->getRequestUri();
127        [$rUri] = explode('#', $requestUri);
128        $content = '<form action="' . htmlspecialchars($rUri) . '" method="post" id="TableController" name="wizardForm">';
129        if ($this->P['table'] && $this->P['field'] && $this->P['uid']) {
130            $tableWizard = $this->renderTableWizard($request);
131
132            if ($tableWizard instanceof RedirectResponse) {
133                return $tableWizard;
134            }
135
136            $content .= '<h2>' . htmlspecialchars($this->getLanguageService()->getLL('table_title')) . '</h2>'
137                . '<div>' . $tableWizard . '</div>';
138        } else {
139            $content .= '<h2>' . htmlspecialchars($this->getLanguageService()->getLL('table_title')) . '</h2>'
140                . '<div><span class="text-danger">' . htmlspecialchars($this->getLanguageService()->getLL('table_noData')) . '</span></div>';
141        }
142        $content .= '</form>';
143
144        // Setting up the buttons and markers for docHeader
145        $this->getButtons();
146        // Build the <body> for the module
147        $this->moduleTemplate->setContent($content);
148
149        return new HtmlResponse($this->moduleTemplate->renderContent());
150    }
151
152    /**
153     * Initialization of the class
154     *
155     * @param ServerRequestInterface $request
156     */
157    protected function init(ServerRequestInterface $request): void
158    {
159        $parsedBody = $request->getParsedBody();
160        $queryParams = $request->getQueryParams();
161        // GPvars:
162        $this->P = $parsedBody['P'] ?? $queryParams['P'] ?? null;
163        $this->TABLECFG = $parsedBody['TABLE'] ?? $queryParams['TABLE'] ?? null;
164        // Setting options:
165        $this->xmlStorage = $this->P['params']['xmlOutput'];
166        $this->numNewRows = MathUtility::forceIntegerInRange($this->P['params']['numNewRows'], 1, 50, 5);
167        // Textareas or input fields:
168        $this->inputStyle = isset($this->TABLECFG['textFields']) ? (bool)$this->TABLECFG['textFields'] : true;
169        $this->tableParsing_delimiter = '|';
170        $this->tableParsing_quote = '';
171    }
172
173    /**
174     * Create the panel of buttons for submitting the form or otherwise perform operations.
175     */
176    protected function getButtons(): void
177    {
178        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
179        if ($this->P['table'] && $this->P['field'] && $this->P['uid']) {
180            // CSH
181            $cshButton = $buttonBar->makeHelpButton()
182                ->setModuleName('xMOD_csh_corebe')
183                ->setFieldName('wizard_table_wiz');
184            $buttonBar->addButton($cshButton);
185            // Close
186            $closeButton = $buttonBar->makeLinkButton()
187                ->setHref($this->P['returnUrl'])
188                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL))
189                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
190                ->setShowLabelText(true);
191            $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
192            // Save
193            $saveButton = $buttonBar->makeInputButton()
194                ->setName('_savedok')
195                ->setValue('1')
196                ->setForm('TableController')
197                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
198                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'))
199                ->setShowLabelText(true);
200            $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
201            // Reload
202            $reloadButton = $buttonBar->makeInputButton()
203                ->setName('_refresh')
204                ->setValue('1')
205                ->setForm('TableController')
206                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-refresh', Icon::SIZE_SMALL))
207                ->setTitle($this->getLanguageService()->getLL('forms_refresh'));
208            $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
209        }
210    }
211
212    /**
213     * Draws the table wizard content
214     *
215     * @param ServerRequestInterface $request
216     * @return string|ResponseInterface HTML content for the form.
217     * @throws \RuntimeException
218     */
219    protected function renderTableWizard(ServerRequestInterface $request)
220    {
221        if (!$this->checkEditAccess($this->P['table'], $this->P['uid'])) {
222            throw new \RuntimeException('Wizard Error: No access', 1349692692);
223        }
224        // First, check the references by selecting the record:
225        $row = BackendUtility::getRecord($this->P['table'], $this->P['uid']);
226        if (!is_array($row)) {
227            throw new \RuntimeException('Wizard Error: No reference to record', 1294587125);
228        }
229        // This will get the content of the form configuration code field to us - possibly cleaned up,
230        // saved to database etc. if the form has been submitted in the meantime.
231        $tableCfgArray = $this->getConfiguration($row, $request);
232
233        if ($tableCfgArray instanceof ResponseInterface) {
234            return $tableCfgArray;
235        }
236
237        // Generation of the Table Wizards HTML code:
238        $content = $this->getTableWizard($tableCfgArray);
239        // Return content:
240        return $content;
241    }
242
243    /**
244     * Will get and return the configuration code string
245     * Will also save (and possibly redirect/exit) the content if a save button has been pressed
246     *
247     * @param array $row Current parent record row
248     * @param ServerRequestInterface $request
249     * @return array|ResponseInterface Table config code in an array
250     */
251    protected function getConfiguration(array $row, ServerRequestInterface $request)
252    {
253        // Get delimiter settings
254        $this->tableParsing_quote = $row['table_enclosure'] ? chr((int)$row['table_enclosure']) : '';
255        $this->tableParsing_delimiter = $row['table_delimiter'] ? chr((int)$row['table_delimiter']) : '|';
256        // If some data has been submitted, then construct
257        if (isset($this->TABLECFG['c'])) {
258            // Process incoming:
259            $this->manipulateTable();
260            // Convert to string (either line based or XML):
261            if ($this->xmlStorage) {
262                // Convert the input array to XML:
263                $bodyText = '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' . LF . GeneralUtility::array2xml($this->TABLECFG['c'], '', 0, 'T3TableWizard');
264                // Setting cfgArr directly from the input:
265                $configuration = $this->TABLECFG['c'];
266            } else {
267                // Convert the input array to a string of configuration code:
268                $bodyText = $this->configurationArrayToString($this->TABLECFG['c']);
269                // Create cfgArr from the string based configuration - that way it is cleaned up
270                // and any incompatibilities will be removed!
271                $configuration = $this->configurationStringToArray($bodyText, (int)$row[$this->colsFieldName]);
272            }
273            // If a save button has been pressed, then save the new field content:
274            if ($_POST['_savedok'] || $_POST['_saveandclosedok']) {
275                // Get DataHandler object:
276                /** @var DataHandler $dataHandler */
277                $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
278                // Put content into the data array:
279                $data = [];
280                if ($this->P['flexFormPath']) {
281                    // Current value of flexForm path:
282                    $currentFlexFormData = GeneralUtility::xml2array($row[$this->P['field']]);
283                    /** @var FlexFormTools $flexFormTools */
284                    $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
285                    $flexFormTools->setArrayValueByPath($this->P['flexFormPath'], $currentFlexFormData, $bodyText);
286                    $data[$this->P['table']][$this->P['uid']][$this->P['field']] = $currentFlexFormData;
287                } else {
288                    $data[$this->P['table']][$this->P['uid']][$this->P['field']] = $bodyText;
289                }
290                // Perform the update:
291                $dataHandler->start($data, []);
292                $dataHandler->process_datamap();
293                // If the save/close button was pressed, then redirect the screen:
294                if ($_POST['_saveandclosedok']) {
295                    return new RedirectResponse(GeneralUtility::sanitizeLocalUrl($this->P['returnUrl']));
296                }
297            }
298        } else {
299            // If nothing has been submitted, load the $bodyText variable from the selected database row:
300            if ($this->xmlStorage) {
301                $configuration = GeneralUtility::xml2array($row[$this->P['field']]);
302            } else {
303                if ($this->P['flexFormPath']) {
304                    // Current value of flexForm path:
305                    $currentFlexFormData = GeneralUtility::xml2array($row[$this->P['field']]);
306                    /** @var FlexFormTools $flexFormTools */
307                    $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
308                    $configuration = $flexFormTools->getArrayValueByPath(
309                        $this->P['flexFormPath'],
310                        $currentFlexFormData
311                    );
312                    $configuration = $this->configurationStringToArray((string)$configuration, 0);
313                } else {
314                    // Regular line based table configuration:
315                    $columns = $row[$this->colsFieldName] ?? 0;
316                    $configuration = $this->configurationStringToArray((string)($row[$this->P['field']] ?? ''), (int)$columns);
317                }
318            }
319            $configuration = is_array($configuration) ? $configuration : [];
320        }
321        return $configuration;
322    }
323
324    /**
325     * Creates the HTML for the Table Wizard:
326     *
327     * @param array $configuration Table config array
328     * @return string HTML for the table wizard
329     */
330    protected function getTableWizard(array $configuration): string
331    {
332        // Traverse the rows:
333        $tRows = [];
334        $k = 0;
335        $countLines = count($configuration);
336        foreach ($configuration as $cellArr) {
337            if (is_array($cellArr)) {
338                // Initialize:
339                $cells = [];
340                $a = 0;
341                // Traverse the columns:
342                foreach ($cellArr as $cellContent) {
343                    if ($this->inputStyle) {
344                        $cells[] = '<input class="form-control" type="text" name="TABLE[c][' . ($k + 1) * 2 . '][' . ($a + 1) * 2 . ']" value="' . htmlspecialchars($cellContent) . '" />';
345                    } else {
346                        $cellContent = preg_replace('/<br[ ]?[\\/]?>/i', LF, $cellContent);
347                        $cells[] = '<textarea class="form-control" rows="6" name="TABLE[c][' . ($k + 1) * 2 . '][' . ($a + 1) * 2 . ']">' . htmlspecialchars($cellContent) . '</textarea>';
348                    }
349                    // Increment counter:
350                    $a++;
351                }
352                // CTRL panel for a table row (move up/down/around):
353                $onClick = 'document.wizardForm.action+=' . GeneralUtility::quoteJSvalue('#ANC_' . (($k + 1) * 2 - 2)) . ';';
354                $onClick = ' onclick="' . htmlspecialchars($onClick) . '"';
355                $ctrl = '';
356                if ($k !== 0) {
357                    $ctrl .= '<button class="btn btn-default" name="TABLE[row_up][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_up')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-angle-up"></span></button>';
358                } else {
359                    $ctrl .= '<button class="btn btn-default" name="TABLE[row_bottom][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_bottom')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-angle-double-down"></span></button>';
360                }
361                if ($k + 1 !== $countLines) {
362                    $ctrl .= '<button class="btn btn-default" name="TABLE[row_down][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_down')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-angle-down"></span></button>';
363                } else {
364                    $ctrl .= '<button class="btn btn-default" name="TABLE[row_top][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_top')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-angle-double-up"></span></button>';
365                }
366                $ctrl .= '<button class="btn btn-default" name="TABLE[row_remove][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_removeRow')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-trash"></span></button>';
367                $ctrl .= '<button class="btn btn-default" name="TABLE[row_add][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_addRow')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-plus"></span></button>';
368                $tRows[] = '
369					<tr>
370						<td>
371							<a name="ANC_' . ($k + 1) * 2 . '"></a>
372							<span class="btn-group' . ($this->inputStyle ? '' : '-vertical') . '">' . $ctrl . '</span>
373						</td>
374						<td>' . implode('</td>
375						<td>', $cells) . '</td>
376					</tr>';
377                // Increment counter:
378                $k++;
379            }
380        }
381        // CTRL panel for a table column (move left/right/around/delete)
382        $cells = [];
383        $cells[] = '';
384        // Finding first row:
385        $firstRow = reset($configuration);
386        if (is_array($firstRow)) {
387            $cols = count($firstRow);
388            for ($a = 1; $a <= $cols; $a++) {
389                $b = $a * 2;
390                $ctrl = '';
391                if ($a !== 1) {
392                    $ctrl .= '<button class="btn btn-default" name="TABLE[col_left][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_left')) . '"><span class="t3-icon fa fa-fw fa-angle-left"></span></button>';
393                } else {
394                    $ctrl .= '<button class="btn btn-default" name="TABLE[col_end][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_end')) . '"><span class="t3-icon fa fa-fw fa-angle-double-right"></span></button>';
395                }
396                if ($a != $cols) {
397                    $ctrl .= '<button class="btn btn-default" name="TABLE[col_right][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_right')) . '"><span class="t3-icon fa fa-fw fa-angle-right"></span></button>';
398                } else {
399                    $ctrl .= '<button class="btn btn-default" name="TABLE[col_start][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_start')) . '"><span class="t3-icon fa fa-fw fa-angle-double-left"></span></button>';
400                }
401                $ctrl .= '<button class="btn btn-default" name="TABLE[col_remove][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_removeColumn')) . '"><span class="t3-icon fa fa-fw fa-trash"></span></button>';
402                $ctrl .= '<button class="btn btn-default" name="TABLE[col_add][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_addColumn')) . '"><span class="t3-icon fa fa-fw fa-plus"></span></button>';
403                $cells[] = '<span class="btn-group">' . $ctrl . '</span>';
404            }
405            $tRows[] = '
406				<tfoot>
407					<tr>
408						<td>' . implode('</td>
409						<td>', $cells) . '</td>
410					</tr>
411				</tfoot>';
412        }
413        $content = '';
414        // Implode all table rows into a string, wrapped in table tags.
415        $content .= '
416
417			<!-- Table wizard -->
418			<div class="table-fit table-fit-inline-block">
419				<table id="typo3-tablewizard" class="table table-center">
420					' . implode('', $tRows) . '
421				</table>
422			</div>';
423        // Input type checkbox:
424        $content .= '
425
426			<!-- Input mode check box: -->
427			<div class="checkbox">
428				<input type="hidden" name="TABLE[textFields]" value="0" />
429				<label for="textFields">
430					<input type="checkbox" data-global-event="change" data-action-submit="$form" name="TABLE[textFields]" id="textFields" value="1"' . ($this->inputStyle ? ' checked="checked"' : '') . ' />
431					' . $this->getLanguageService()->getLL('table_smallFields') . '
432				</label>
433			</div>';
434        return $content;
435    }
436
437    /**
438     * Detects if a control button (up/down/around/delete) has been pressed for an item and accordingly it will
439     * manipulate the internal TABLECFG array
440     */
441    protected function manipulateTable(): void
442    {
443        if ($this->TABLECFG['col_remove']) {
444            $kk = key($this->TABLECFG['col_remove']);
445            $cmd = 'col_remove';
446        } elseif ($this->TABLECFG['col_add']) {
447            $kk = key($this->TABLECFG['col_add']);
448            $cmd = 'col_add';
449        } elseif ($this->TABLECFG['col_start']) {
450            $kk = key($this->TABLECFG['col_start']);
451            $cmd = 'col_start';
452        } elseif ($this->TABLECFG['col_end']) {
453            $kk = key($this->TABLECFG['col_end']);
454            $cmd = 'col_end';
455        } elseif ($this->TABLECFG['col_left']) {
456            $kk = key($this->TABLECFG['col_left']);
457            $cmd = 'col_left';
458        } elseif ($this->TABLECFG['col_right']) {
459            $kk = key($this->TABLECFG['col_right']);
460            $cmd = 'col_right';
461        } elseif ($this->TABLECFG['row_remove']) {
462            $kk = key($this->TABLECFG['row_remove']);
463            $cmd = 'row_remove';
464        } elseif ($this->TABLECFG['row_add']) {
465            $kk = key($this->TABLECFG['row_add']);
466            $cmd = 'row_add';
467        } elseif ($this->TABLECFG['row_top']) {
468            $kk = key($this->TABLECFG['row_top']);
469            $cmd = 'row_top';
470        } elseif ($this->TABLECFG['row_bottom']) {
471            $kk = key($this->TABLECFG['row_bottom']);
472            $cmd = 'row_bottom';
473        } elseif ($this->TABLECFG['row_up']) {
474            $kk = key($this->TABLECFG['row_up']);
475            $cmd = 'row_up';
476        } elseif ($this->TABLECFG['row_down']) {
477            $kk = key($this->TABLECFG['row_down']);
478            $cmd = 'row_down';
479        } else {
480            $kk = '';
481            $cmd = '';
482        }
483        if ($cmd && MathUtility::canBeInterpretedAsInteger($kk)) {
484            if (strpos($cmd, 'row_') === 0) {
485                switch ($cmd) {
486                    case 'row_remove':
487                        unset($this->TABLECFG['c'][$kk]);
488                        break;
489                    case 'row_add':
490                        for ($a = 1; $a <= $this->numNewRows; $a++) {
491                            // Checking if set: The point is that any new row between existing rows
492                            // will be TRUE after one row is added while if rows are added in the bottom
493                            // of the table there will be no existing rows to stop the addition of new rows
494                            // which means it will add up to $this->numNewRows rows then.
495                            if (!isset($this->TABLECFG['c'][$kk + $a])) {
496                                $this->TABLECFG['c'][$kk + $a] = [];
497                            } else {
498                                break;
499                            }
500                        }
501                        break;
502                    case 'row_top':
503                        $this->TABLECFG['c'][1] = $this->TABLECFG['c'][$kk];
504                        unset($this->TABLECFG['c'][$kk]);
505                        break;
506                    case 'row_bottom':
507                        $this->TABLECFG['c'][10000000] = $this->TABLECFG['c'][$kk];
508                        unset($this->TABLECFG['c'][$kk]);
509                        break;
510                    case 'row_up':
511                        $this->TABLECFG['c'][$kk - 3] = $this->TABLECFG['c'][$kk];
512                        unset($this->TABLECFG['c'][$kk]);
513                        break;
514                    case 'row_down':
515                        $this->TABLECFG['c'][$kk + 3] = $this->TABLECFG['c'][$kk];
516                        unset($this->TABLECFG['c'][$kk]);
517                        break;
518                }
519                ksort($this->TABLECFG['c']);
520            }
521            if (strpos($cmd, 'col_') === 0) {
522                foreach ($this->TABLECFG['c'] as $cAK => $value) {
523                    switch ($cmd) {
524                        case 'col_remove':
525                            unset($this->TABLECFG['c'][$cAK][$kk]);
526                            break;
527                        case 'col_add':
528                            $this->TABLECFG['c'][$cAK][$kk + 1] = '';
529                            break;
530                        case 'col_start':
531                            $this->TABLECFG['c'][$cAK][1] = $this->TABLECFG['c'][$cAK][$kk];
532                            unset($this->TABLECFG['c'][$cAK][$kk]);
533                            break;
534                        case 'col_end':
535                            $this->TABLECFG['c'][$cAK][1000000] = $this->TABLECFG['c'][$cAK][$kk];
536                            unset($this->TABLECFG['c'][$cAK][$kk]);
537                            break;
538                        case 'col_left':
539                            $this->TABLECFG['c'][$cAK][$kk - 3] = $this->TABLECFG['c'][$cAK][$kk];
540                            unset($this->TABLECFG['c'][$cAK][$kk]);
541                            break;
542                        case 'col_right':
543                            $this->TABLECFG['c'][$cAK][$kk + 3] = $this->TABLECFG['c'][$cAK][$kk];
544                            unset($this->TABLECFG['c'][$cAK][$kk]);
545                            break;
546                    }
547                    ksort($this->TABLECFG['c'][$cAK]);
548                }
549            }
550        }
551        // Convert line breaks to <br /> tags:
552        foreach ($this->TABLECFG['c'] as $a => $value) {
553            foreach ($this->TABLECFG['c'][$a] as $b => $value2) {
554                $this->TABLECFG['c'][$a][$b] = str_replace(
555                    [CR, LF],
556                    ['', '<br />'],
557                    $this->TABLECFG['c'][$a][$b]
558                );
559            }
560        }
561    }
562
563    /**
564     * Converts the input array to a configuration code string
565     *
566     * @param array $cfgArr Array of table configuration (follows the input structure from the table wizard POST form)
567     * @return string The array converted into a string with line-based configuration.
568     * @see configurationStringToArray()
569     */
570    protected function configurationArrayToString(array $cfgArr): string
571    {
572        $inLines = [];
573        // Traverse the elements of the table wizard and transform the settings into configuration code.
574        foreach ($cfgArr as $valueA) {
575            $thisLine = [];
576            foreach ($valueA as $valueB) {
577                $thisLine[] = $this->tableParsing_quote
578                    . str_replace($this->tableParsing_delimiter, '', $valueB) . $this->tableParsing_quote;
579            }
580            $inLines[] = implode($this->tableParsing_delimiter, $thisLine);
581        }
582        // Finally, implode the lines into a string:
583        return implode(LF, $inLines);
584    }
585
586    /**
587     * Converts the input configuration code string into an array
588     *
589     * @param string $configurationCode Configuration code
590     * @param int $columns Default number of columns
591     * @return array Configuration array
592     * @see configurationArrayToString()
593     */
594    protected function configurationStringToArray(string $configurationCode, int $columns): array
595    {
596        // Explode lines in the configuration code - each line is a table row.
597        $tableLines = explode(LF, $configurationCode);
598        // Setting number of columns
599        // auto...
600        if (!$columns && trim($tableLines[0])) {
601            $exploded = explode($this->tableParsing_delimiter, $tableLines[0]);
602            $columns = is_array($exploded) ? count($exploded) : 0;
603        }
604        $columns = $columns ?: 4;
605        // Traverse the number of table elements:
606        $configurationArray = [];
607        foreach ($tableLines as $key => $value) {
608            // Initialize:
609            $valueParts = explode($this->tableParsing_delimiter, $value);
610            // Traverse columns:
611            for ($a = 0; $a < $columns; $a++) {
612                if ($this->tableParsing_quote
613                    && $valueParts[$a][0] === $this->tableParsing_quote
614                    && substr($valueParts[$a], -1, 1) === $this->tableParsing_quote
615                ) {
616                    $valueParts[$a] = substr(trim($valueParts[$a]), 1, -1);
617                }
618                $configurationArray[$key][$a] = (string)$valueParts[$a];
619            }
620        }
621        return $configurationArray;
622    }
623}
624