1<?php
2namespace TYPO3\CMS\Core\DataHandling\Localization;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Core\Utility\GeneralUtility;
18
19/**
20 * Value object for l10n_state field value.
21 */
22class State
23{
24    const STATE_CUSTOM = 'custom';
25    const STATE_PARENT = 'parent';
26    const STATE_SOURCE = 'source';
27
28    /**
29     * @param string $tableName
30     * @return State|null
31     */
32    public static function create(string $tableName)
33    {
34        if (!static::isApplicable($tableName)) {
35            return null;
36        }
37
38        return GeneralUtility::makeInstance(
39            static::class,
40            $tableName
41        );
42    }
43
44    /**
45     * @param string $tableName
46     * @param string|null $json
47     * @return State|null
48     */
49    public static function fromJSON(string $tableName, string $json = null)
50    {
51        if (!static::isApplicable($tableName)) {
52            return null;
53        }
54
55        $states = json_decode($json ?? '', true);
56        return GeneralUtility::makeInstance(
57            static::class,
58            $tableName,
59            $states ?? []
60        );
61    }
62
63    /**
64     * @param string $tableName
65     * @return bool
66     */
67    public static function isApplicable(string $tableName)
68    {
69        return
70            static::hasColumns($tableName)
71            && static::hasLanguageFieldName($tableName)
72            && static::hasTranslationParentFieldName($tableName)
73            && count(static::getFieldNames($tableName)) > 0
74        ;
75    }
76
77    /**
78     * @param string $tableName
79     * @return array
80     */
81    public static function getFieldNames(string $tableName)
82    {
83        return array_keys(
84            array_filter(
85                $GLOBALS['TCA'][$tableName]['columns'] ?? [],
86                function (array $fieldConfiguration) {
87                    return !empty(
88                        $fieldConfiguration['config']
89                            ['behaviour']['allowLanguageSynchronization']
90                    );
91                }
92            )
93        );
94    }
95
96    /**
97     * @param string $tableName
98     * @return bool
99     */
100    protected static function hasColumns(string $tableName)
101    {
102        return
103            !empty($GLOBALS['TCA'][$tableName]['columns'])
104            && is_array($GLOBALS['TCA'][$tableName]['columns'])
105        ;
106    }
107
108    /**
109     * @param string $tableName
110     * @return bool
111     */
112    protected static function hasLanguageFieldName(string $tableName)
113    {
114        return !empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField']);
115    }
116
117    /**
118     * @param string $tableName
119     * @return bool
120     */
121    protected static function hasTranslationParentFieldName(string $tableName)
122    {
123        return !empty($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']);
124    }
125
126    /**
127     * @var string
128     */
129    protected $tableName;
130
131    /**
132     * @var array
133     */
134    protected $states;
135
136    /**
137     * @var array
138     */
139    protected $originalStates;
140
141    /**
142     * @var array
143     */
144    protected $validStates = [
145        self::STATE_CUSTOM,
146        self::STATE_SOURCE,
147        self::STATE_PARENT,
148    ];
149
150    /**
151     * @param string $tableName
152     * @param array $states
153     */
154    public function __construct(string $tableName, array $states = [])
155    {
156        $this->tableName = $tableName;
157        $this->states = $states;
158        $this->originalStates = $states;
159
160        $this->states = $this->enrich(
161            $this->sanitize($states)
162        );
163    }
164
165    /**
166     * @param array $states
167     */
168    public function update(array $states)
169    {
170        $this->states = array_merge(
171            $this->states,
172            $this->sanitize($states)
173        );
174    }
175
176    /**
177     * Updates field names having a particular state to a target state.
178     *
179     * @param string $currentState
180     * @param string $targetState
181     */
182    public function updateStates(string $currentState, string $targetState)
183    {
184        $states = [];
185        foreach ($this->filterFieldNames($currentState) as $fieldName) {
186            $states[$fieldName] = $targetState;
187        }
188        if (!empty($states)) {
189            $this->update($states);
190        }
191    }
192
193    /**
194     * @return string|null
195     */
196    public function export()
197    {
198        if (empty($this->states)) {
199            return null;
200        }
201        return json_encode($this->states);
202    }
203
204    /**
205     * @return array
206     */
207    public function toArray(): array
208    {
209        return $this->states ?? [];
210    }
211
212    /**
213     * @return string[]
214     */
215    public function getModifiedFieldNames()
216    {
217        return array_keys(
218            array_diff_assoc(
219                $this->states,
220                $this->originalStates
221            )
222        );
223    }
224
225    /**
226     * @return bool
227     */
228    public function isModified()
229    {
230        return !empty($this->getModifiedFieldNames());
231    }
232
233    /**
234     * @param string $fieldName
235     * @return bool
236     */
237    public function isUndefined(string $fieldName)
238    {
239        return !isset($this->states[$fieldName]);
240    }
241
242    /**
243     * @param string $fieldName
244     * @return bool
245     */
246    public function isCustomState(string $fieldName)
247    {
248        return ($this->states[$fieldName] ?? null) === static::STATE_CUSTOM;
249    }
250
251    /**
252     * @param string $fieldName
253     * @return bool
254     */
255    public function isParentState(string $fieldName)
256    {
257        return ($this->states[$fieldName] ?? null) === static::STATE_PARENT;
258    }
259
260    /**
261     * @param string $fieldName
262     * @return bool
263     */
264    public function isSourceState(string $fieldName)
265    {
266        return ($this->states[$fieldName] ?? null) === static::STATE_SOURCE;
267    }
268
269    /**
270     * @param string $fieldName
271     * @return string|null
272     */
273    public function getState(string $fieldName)
274    {
275        return $this->states[$fieldName] ?? null;
276    }
277
278    /**
279     * Filters field names having a desired state.
280     *
281     * @param string $desiredState
282     * @param bool $modified
283     * @return string[]
284     */
285    public function filterFieldNames(string $desiredState, bool $modified = false)
286    {
287        if (!$modified) {
288            $fieldNames = array_keys($this->states);
289        } else {
290            $fieldNames = $this->getModifiedFieldNames();
291        }
292        return array_filter(
293            $fieldNames,
294            function ($fieldName) use ($desiredState) {
295                return $this->states[$fieldName] === $desiredState;
296            }
297        );
298    }
299
300    /**
301     * Filter out field names that don't exist in TCA.
302     *
303     * @param array $states
304     * @return array
305     */
306    protected function sanitize(array $states)
307    {
308        $fieldNames = static::getFieldNames($this->tableName);
309        return array_intersect_key(
310            $states,
311            array_combine($fieldNames, $fieldNames)
312        );
313    }
314
315    /**
316     * Add missing states for field names.
317     *
318     * @param array $states
319     * @return array
320     */
321    protected function enrich(array $states)
322    {
323        foreach (static::getFieldNames($this->tableName) as $fieldName) {
324            $isValid = in_array(
325                $states[$fieldName] ?? null,
326                $this->validStates,
327                true
328            );
329            if ($isValid) {
330                continue;
331            }
332            $states[$fieldName] = static::STATE_PARENT;
333        }
334        return $states;
335    }
336}
337