be7cb0f41d70426625fafa6dd477ee1911680303
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / InlineStackProcessor.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form;
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
17 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
18 use TYPO3\CMS\Backend\Utility\BackendUtility;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20
21 /**
22 * Handle inline stack.
23 *
24 * Code related to inline elements need to know their nesting level. This class takes
25 * care of the according handling and can return field prefixes to be used in DOM.
26 *
27 * @internal: This class may change any time or vanish altogether
28 */
29 class InlineStackProcessor
30 {
31 /**
32 * The structure/hierarchy where working in, e.g. cascading inline tables
33 *
34 * @var array
35 */
36 protected $inlineStructure = [];
37
38 /**
39 * One of two possible initialize methods setting a given structure.
40 *
41 * @param array $structure
42 */
43 public function initializeByGivenStructure(array $structure = [])
44 {
45 $this->inlineStructure = $structure;
46 }
47
48 /**
49 * Convert the DOM object-id of an inline container to an array.
50 * The object-id could look like 'data-parentPageId-tx_mmftest_company-1-employees'.
51 * This initializes $this->inlineStructure - used by AJAX entry points
52 * There are two keys:
53 * - 'stable': Containing full qualified identifiers (table, uid and field)
54 * - 'unstable': Containing partly filled data (e.g. only table and possibly field)
55 *
56 * @param string $domObjectId The DOM object-id
57 * @return void
58 */
59 public function initializeByParsingDomObjectIdString($domObjectId)
60 {
61 $unstable = [];
62 $vector = ['table', 'uid', 'field'];
63
64 // Substitute FlexForm addition and make parsing a bit easier
65 $domObjectId = str_replace('---', ':', $domObjectId);
66 // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
67 $pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/';
68
69 if (preg_match($pattern, $domObjectId, $match)) {
70 $inlineFirstPid = $match[1];
71 $parts = explode('-', $match[2]);
72 $partsCnt = count($parts);
73 for ($i = 0; $i < $partsCnt; $i++) {
74 if ($i > 0 && $i % 3 == 0) {
75 // Load the TCA configuration of the table field and store it in the stack
76 // @todo: This TCA loading here must fall - config sub-array shouldn't exist at all!
77 $unstable['config'] = $GLOBALS['TCA'][$unstable['table']]['columns'][$unstable['field']]['config'];
78 // Fetch TSconfig:
79 // @todo: aaargs ;)
80 $TSconfig = FormEngineUtility::getTSconfigForTableRow($unstable['table'], ['uid' => $unstable['uid'], 'pid' => $inlineFirstPid], $unstable['field']);
81 // Override TCA field config by TSconfig:
82 if (!$TSconfig['disabled']) {
83 $unstable['config'] = FormEngineUtility::overrideFieldConf($unstable['config'], $TSconfig);
84 }
85 $unstable['localizationMode'] = BackendUtility::getInlineLocalizationMode($unstable['table'], $unstable['config']);
86
87 // Extract FlexForm from field part (if any)
88 if (strpos($unstable['field'], ':') !== false) {
89 $fieldParts = GeneralUtility::trimExplode(':', $unstable['field']);
90 $unstable['field'] = array_shift($fieldParts);
91 // FlexForm parts start with data:
92 if (!empty($fieldParts) && $fieldParts[0] === 'data') {
93 $unstable['flexform'] = $fieldParts;
94 }
95 }
96
97 $this->inlineStructure['stable'][] = $unstable;
98 $unstable = [];
99 }
100 $unstable[$vector[$i % 3]] = $parts[$i];
101 }
102 if (!empty($unstable)) {
103 $this->inlineStructure['unstable'] = $unstable;
104 }
105 }
106 }
107
108 /**
109 * Injects configuration via AJAX calls.
110 * This is used by inline ajax calls that transfer configuration options back to the stack for initialization
111 * The configuration is validated using HMAC to avoid hijacking.
112 *
113 * @param string $contextString Given context string from ajax call
114 * @return void
115 * @todo: Review this construct - Why can't the ajax call fetch these data on its own and transfers it to client instead?
116 */
117 public function injectAjaxConfiguration($contextString = '')
118 {
119 $level = $this->calculateStructureLevel(-1);
120 if (empty($contextString) || $level === false) {
121 return;
122 }
123 $current = &$this->inlineStructure['stable'][$level];
124 $context = json_decode($contextString, true);
125 if (GeneralUtility::hmac(serialize($context['config'])) !== $context['hmac']) {
126 return;
127 }
128 // Remove the data structure pointers, only relevant for the FormInlineAjaxController
129 unset($context['flexDataStructurePointers']);
130 $current['config'] = $context['config'];
131 $current['localizationMode'] = BackendUtility::getInlineLocalizationMode(
132 $current['table'],
133 $current['config']
134 );
135 }
136
137 /**
138 * Get current structure stack
139 *
140 * @return array Current structure stack
141 */
142 public function getStructure()
143 {
144 return $this->inlineStructure;
145 }
146
147 /**
148 * Add a stable structure to the stack
149 *
150 * @param array $structureItem
151 * @return void
152 */
153 public function pushStableStructureItem(array $structureItem = [])
154 {
155 $this->inlineStructure['stable'][] = $structureItem;
156 }
157
158 /**
159 * Prefix for inline form fields
160 *
161 * @return string
162 */
163 public function getCurrentStructureFormPrefix()
164 {
165 $current = $this->getStructureLevel(-1);
166 $inlineFormName = '';
167 // If there are still more inline levels available
168 if ($current !== false) {
169 $inlineFormName = 'data' . $this->getStructureItemName($current, 'Disposal_AttributeName');
170 }
171 return $inlineFormName;
172 }
173
174 /**
175 * DOM object-id for this inline level
176 *
177 * @param int $inlineFirstPid Pid of top level inline element storage
178 * @return string
179 */
180 public function getCurrentStructureDomObjectIdPrefix($inlineFirstPid)
181 {
182 $current = $this->getStructureLevel(-1);
183 $inlineDomObjectId = '';
184 // If there are still more inline levels available
185 if ($current !== false) {
186 $inlineDomObjectId = 'data' . '-' . $inlineFirstPid . '-' . $this->getStructurePath();
187 }
188 return $inlineDomObjectId;
189 }
190
191 /**
192 * Get a level from the stack and return the data.
193 * If the $level value is negative, this function works top-down,
194 * if the $level value is positive, this function works bottom-up.
195 * Hint: If -1 is given, the "current" - most bottom "stable" item is returned
196 *
197 * @param int $level Which level to return
198 * @return array The item of the stack at the requested level
199 */
200 public function getStructureLevel($level)
201 {
202 $level = $this->calculateStructureLevel($level);
203
204 if ($level !== false) {
205 return $this->inlineStructure['stable'][$level];
206 } else {
207 return false;
208 }
209 }
210
211 /**
212 * Get the "unstable" structure item from structure stack.
213 * This is typically initialized by initializeByParsingDomObjectIdString()
214 *
215 * @return array Unstable structure item
216 * @throws \RuntimeException
217 */
218 public function getUnstableStructure()
219 {
220 if (!isset($this->inlineStructure['unstable'])) {
221 throw new \RuntimeException('No unstable inline structure found', 1428582655);
222 }
223 return $this->inlineStructure['unstable'];
224 }
225
226 /**
227 * Calculates structure level.
228 *
229 * @param int $level Which level to return
230 * @return bool|int
231 */
232 protected function calculateStructureLevel($level)
233 {
234 $result = false;
235 $inlineStructureCount = count($this->inlineStructure['stable']);
236 if ($level < 0) {
237 $level = $inlineStructureCount + $level;
238 }
239 if ($level >= 0 && $level < $inlineStructureCount) {
240 $result = $level;
241 }
242 return $result;
243 }
244
245 /**
246 * Get the identifiers of a given depth of level, from the top of the stack to the bottom.
247 * An identifier looks like "<table>-<uid>-<field>".
248 *
249 * @param int $structureDepth How much levels to output, beginning from the top of the stack
250 * @return string The path of identifiers
251 */
252 protected function getStructurePath($structureDepth = -1)
253 {
254 $structureLevels = [];
255 $structureCount = $this->getStructureDepth();
256 if ($structureDepth < 0 || $structureDepth > $structureCount) {
257 $structureDepth = $structureCount;
258 }
259 for ($i = 1; $i <= $structureDepth; $i++) {
260 array_unshift($structureLevels, $this->getStructureItemName($this->getStructureLevel(-$i), 'Disposal_AttributeId'));
261 }
262 return implode('-', $structureLevels);
263 }
264
265 /**
266 * Get the depth of the stable structure stack.
267 * (count($this->inlineStructure['stable'])
268 *
269 * @return int The depth of the structure stack
270 */
271 public function getStructureDepth()
272 {
273 return count($this->inlineStructure['stable']);
274 }
275
276 /**
277 * Create a name/id for usage in HTML output of a level of the structure stack to be used in form names.
278 *
279 * @param array $levelData Array of a level of the structure stack (containing the keys table, uid and field)
280 * @param string $disposal How the structure name is used (e.g. as <div id="..."> or <input name="..." />)
281 * @return string The name/id of that level, to be used for HTML output
282 */
283 protected function getStructureItemName($levelData, $disposal = 'Disposal_AttributeId')
284 {
285 $name = null;
286
287 if (is_array($levelData)) {
288 $parts = [$levelData['table'], $levelData['uid']];
289
290 if (!empty($levelData['field'])) {
291 $parts[] = $levelData['field'];
292 }
293
294 // Use in name attributes:
295 if ($disposal === 'Disposal_AttributeName') {
296 if (!empty($levelData['field']) && !empty($levelData['flexform']) && $this->getStructureLevel(-1) === $levelData) {
297 $parts[] = implode('][', $levelData['flexform']);
298 }
299 $name = '[' . implode('][', $parts) . ']';
300 // Use in object id attributes:
301 } else {
302 $name = implode('-', $parts);
303
304 if (!empty($levelData['field']) && !empty($levelData['flexform'])) {
305 array_unshift($levelData['flexform'], $name);
306 $name = implode('---', $levelData['flexform']);
307 }
308 }
309 }
310
311 return $name;
312 }
313 }