b8979776be255c58630af355b6aec3d3fd74ea57
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Service / MarkerBasedTemplateService.php
1 <?php
2 namespace TYPO3\CMS\Core\Service;
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 use TYPO3\CMS\Core\Cache\CacheManager;
17 use TYPO3\CMS\Core\Utility\GeneralUtility;
18 use TYPO3\CMS\Core\Utility\MathUtility;
19
20 /**
21 * Helper functionality for subparts and marker substitution
22 * ###MYMARKER###
23 */
24 class MarkerBasedTemplateService
25 {
26 /**
27 * Returns the first subpart encapsulated in the marker, $marker
28 * (possibly present in $content as a HTML comment)
29 *
30 * @param string $content Content with subpart wrapped in fx. "###CONTENT_PART###" inside.
31 * @param string $marker Marker string, eg. "###CONTENT_PART###
32 *
33 * @return string
34 */
35 public function getSubpart($content, $marker)
36 {
37 $start = strpos($content, $marker);
38 if ($start === false) {
39 return '';
40 }
41 $start += strlen($marker);
42 $stop = strpos($content, $marker, $start);
43 // Q: What shall get returned if no stop marker is given
44 // Everything till the end or nothing?
45 if ($stop === false) {
46 return '';
47 }
48 $content = substr($content, $start, $stop - $start);
49 $matches = [];
50 if (preg_match('/^([^\\<]*\\-\\-\\>)(.*)(\\<\\!\\-\\-[^\\>]*)$/s', $content, $matches) === 1) {
51 return $matches[2];
52 }
53 // Resetting $matches
54 $matches = [];
55 if (preg_match('/(.*)(\\<\\!\\-\\-[^\\>]*)$/s', $content, $matches) === 1) {
56 return $matches[1];
57 }
58 // Resetting $matches
59 $matches = [];
60 if (preg_match('/^([^\\<]*\\-\\-\\>)(.*)$/s', $content, $matches) === 1) {
61 return $matches[2];
62 }
63
64 return $content;
65 }
66
67 /**
68 * Substitutes a subpart in $content with the content of $subpartContent.
69 *
70 * @param string $content Content with subpart wrapped in fx. "###CONTENT_PART###" inside.
71 * @param string $marker Marker string, eg. "###CONTENT_PART###
72 * @param array $subpartContent If $subpartContent happens to be an array, it's [0] and [1] elements are wrapped around the content of the subpart (fetched by getSubpart())
73 * @param bool $recursive If $recursive is set, the function calls itself with the content set to the remaining part of the content after the second marker. This means that proceding subparts are ALSO substituted!
74 * @param bool $keepMarker If set, the marker around the subpart is not removed, but kept in the output
75 *
76 * @return string Processed input content
77 */
78 public function substituteSubpart($content, $marker, $subpartContent, $recursive = true, $keepMarker = false)
79 {
80 $start = strpos($content, $marker);
81 if ($start === false) {
82 return $content;
83 }
84 $startAM = $start + strlen($marker);
85 $stop = strpos($content, $marker, $startAM);
86 if ($stop === false) {
87 return $content;
88 }
89 $stopAM = $stop + strlen($marker);
90 $before = substr($content, 0, $start);
91 $after = substr($content, $stopAM);
92 $between = substr($content, $startAM, $stop - $startAM);
93 if ($recursive) {
94 $after = $this->substituteSubpart($after, $marker, $subpartContent, $recursive, $keepMarker);
95 }
96 if ($keepMarker) {
97 $matches = [];
98 if (preg_match('/^([^\\<]*\\-\\-\\>)(.*)(\\<\\!\\-\\-[^\\>]*)$/s', $between, $matches) === 1) {
99 $before .= $marker . $matches[1];
100 $between = $matches[2];
101 $after = $matches[3] . $marker . $after;
102 } elseif (preg_match('/^(.*)(\\<\\!\\-\\-[^\\>]*)$/s', $between, $matches) === 1) {
103 $before .= $marker;
104 $between = $matches[1];
105 $after = $matches[2] . $marker . $after;
106 } elseif (preg_match('/^([^\\<]*\\-\\-\\>)(.*)$/s', $between, $matches) === 1) {
107 $before .= $marker . $matches[1];
108 $between = $matches[2];
109 $after = $marker . $after;
110 } else {
111 $before .= $marker;
112 $after = $marker . $after;
113 }
114 } else {
115 $matches = [];
116 if (preg_match('/^(.*)\\<\\!\\-\\-[^\\>]*$/s', $before, $matches) === 1) {
117 $before = $matches[1];
118 }
119 if (is_array($subpartContent)) {
120 $matches = [];
121 if (preg_match('/^([^\\<]*\\-\\-\\>)(.*)(\\<\\!\\-\\-[^\\>]*)$/s', $between, $matches) === 1) {
122 $between = $matches[2];
123 } elseif (preg_match('/^(.*)(\\<\\!\\-\\-[^\\>]*)$/s', $between, $matches) === 1) {
124 $between = $matches[1];
125 } elseif (preg_match('/^([^\\<]*\\-\\-\\>)(.*)$/s', $between, $matches) === 1) {
126 $between = $matches[2];
127 }
128 }
129 $matches = [];
130 // resetting $matches
131 if (preg_match('/^[^\\<]*\\-\\-\\>(.*)$/s', $after, $matches) === 1) {
132 $after = $matches[1];
133 }
134 }
135 if (is_array($subpartContent)) {
136 $between = $subpartContent[0] . $between . $subpartContent[1];
137 } else {
138 $between = $subpartContent;
139 }
140
141 return $before . $between . $after;
142 }
143
144 /**
145 * Substitues multiple subparts at once
146 *
147 * @param string $content The content stream, typically HTML template content.
148 * @param array $subpartsContent The array of key/value pairs being subpart/content values used in the substitution. For each element in this array the function will substitute a subpart in the content stream with the content.
149 *
150 * @return string The processed HTML content string.
151 */
152 public function substituteSubpartArray($content, array $subpartsContent)
153 {
154 foreach ($subpartsContent as $subpartMarker => $subpartContent) {
155 $content = $this->substituteSubpart($content, $subpartMarker, $subpartContent);
156 }
157
158 return $content;
159 }
160
161 /**
162 * Substitutes a marker string in the input content
163 * (by a simple str_replace())
164 *
165 * @param string $content The content stream, typically HTML template content.
166 * @param string $marker The marker string, typically on the form "###[the marker string]###
167 * @param mixed $markContent The content to insert instead of the marker string found.
168 *
169 * @return string The processed HTML content string.
170 * @see substituteSubpart()
171 */
172 public function substituteMarker($content, $marker, $markContent)
173 {
174 return str_replace($marker, $markContent, $content);
175 }
176
177 /**
178 * Traverses the input $markContentArray array and for each key the marker
179 * by the same name (possibly wrapped and in upper case) will be
180 * substituted with the keys value in the array. This is very useful if you
181 * have a data-record to substitute in some content. In particular when you
182 * use the $wrap and $uppercase values to pre-process the markers. Eg. a
183 * key name like "myfield" could effectively be represented by the marker
184 * "###MYFIELD###" if the wrap value was "###|###" and the $uppercase
185 * boolean TRUE.
186 *
187 * @param string $content The content stream, typically HTML template content.
188 * @param array $markContentArray The array of key/value pairs being marker/content values used in the substitution. For each element in this array the function will substitute a marker in the content stream with the content.
189 * @param string $wrap A wrap value - [part 1] | [part 2] - for the markers before substitution
190 * @param bool $uppercase If set, all marker string substitution is done with upper-case markers.
191 * @param bool $deleteUnused If set, all unused marker are deleted.
192 *
193 * @return string The processed output stream
194 * @see substituteMarker(), substituteMarkerInObject(), TEMPLATE()
195 */
196 public function substituteMarkerArray($content, $markContentArray, $wrap = '', $uppercase = false, $deleteUnused = false)
197 {
198 if (is_array($markContentArray)) {
199 $wrapArr = GeneralUtility::trimExplode('|', $wrap);
200 $search = [];
201 $replace = [];
202 foreach ($markContentArray as $marker => $markContent) {
203 if ($uppercase) {
204 // use strtr instead of strtoupper to avoid locale problems with Turkish
205 $marker = strtr($marker, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
206 }
207 if (!empty($wrapArr)) {
208 $marker = $wrapArr[0] . $marker . $wrapArr[1];
209 }
210 $search[] = $marker;
211 $replace[] = $markContent;
212 }
213 $content = str_replace($search, $replace, $content);
214 unset($search, $replace);
215 if ($deleteUnused) {
216 if (empty($wrap)) {
217 $wrapArr = ['###', '###'];
218 }
219 $content = preg_replace('/' . preg_quote($wrapArr[0], '/') . '([A-Z0-9_|\\-]*)' . preg_quote($wrapArr[1], '/') . '/is', '', $content);
220 }
221 }
222
223 return $content;
224 }
225
226 /**
227 * Replaces all markers and subparts in a template with the content provided in the structured array.
228 *
229 * The array is built like the template with its markers and subparts. Keys represent the marker name and the values the
230 * content.
231 * If the value is not an array the key will be treated as a single marker.
232 * If the value is an array the key will be treated as a subpart marker.
233 * Repeated subpart contents are of course elements in the array, so every subpart value must contain an array with its
234 * markers.
235 *
236 * $markersAndSubparts = array (
237 * '###SINGLEMARKER1###' => 'value 1',
238 * '###SUBPARTMARKER1###' => array(
239 * 0 => array(
240 * '###SINGLEMARKER2###' => 'value 2',
241 * ),
242 * 1 => array(
243 * '###SINGLEMARKER2###' => 'value 3',
244 * )
245 * ),
246 * '###SUBPARTMARKER2###' => array(
247 * ),
248 * )
249 * Subparts can be nested, so below the 'SINGLEMARKER2' it is possible to have another subpart marker with an array as the
250 * value, which in its turn contains the elements of the sub-subparts.
251 * Empty arrays for Subparts will cause the subtemplate to be cleared.
252 *
253 * @param string $content The content stream, typically HTML template content.
254 * @param array $markersAndSubparts The array of single markers and subpart contents.
255 * @param string $wrap A wrap value - [part1] | [part2] - for the markers before substitution.
256 * @param bool $uppercase If set, all marker string substitution is done with upper-case markers.
257 * @param bool $deleteUnused If set, all unused single markers are deleted.
258 *
259 * @return string The processed output stream
260 */
261 public function substituteMarkerAndSubpartArrayRecursive($content, array $markersAndSubparts, $wrap = '', $uppercase = false, $deleteUnused = false)
262 {
263 $wraps = GeneralUtility::trimExplode('|', $wrap);
264 $singleItems = [];
265 $compoundItems = [];
266 // Split markers and subparts into separate arrays
267 foreach ($markersAndSubparts as $markerName => $markerContent) {
268 if (is_array($markerContent)) {
269 $compoundItems[] = $markerName;
270 } else {
271 $singleItems[$markerName] = $markerContent;
272 }
273 }
274 $subTemplates = [];
275 $subpartSubstitutes = [];
276 // Build a cache for the sub template
277 foreach ($compoundItems as $subpartMarker) {
278 if ($uppercase) {
279 // Use strtr instead of strtoupper to avoid locale problems with Turkish
280 $subpartMarker = strtr($subpartMarker, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
281 }
282 if (!empty($wraps)) {
283 $subpartMarker = $wraps[0] . $subpartMarker . $wraps[1];
284 }
285 $subTemplates[$subpartMarker] = $this->getSubpart($content, $subpartMarker);
286 }
287 // Replace the subpart contents recursively
288 foreach ($compoundItems as $subpartMarker) {
289 $completeMarker = $subpartMarker;
290 if ($uppercase) {
291 // use strtr instead of strtoupper to avoid locale problems with Turkish
292 $completeMarker = strtr($completeMarker, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
293 }
294 if (!empty($wraps)) {
295 $completeMarker = $wraps[0] . $completeMarker . $wraps[1];
296 }
297 if (!empty($markersAndSubparts[$subpartMarker])) {
298 foreach ($markersAndSubparts[$subpartMarker] as $partialMarkersAndSubparts) {
299 $subpartSubstitutes[$completeMarker] .= $this->substituteMarkerAndSubpartArrayRecursive($subTemplates[$completeMarker],
300 $partialMarkersAndSubparts, $wrap, $uppercase, $deleteUnused);
301 }
302 } else {
303 $subpartSubstitutes[$completeMarker] = '';
304 }
305 }
306 // Substitute the single markers and subparts
307 $result = $this->substituteSubpartArray($content, $subpartSubstitutes);
308 $result = $this->substituteMarkerArray($result, $singleItems, $wrap, $uppercase, $deleteUnused);
309
310 return $result;
311 }
312
313 /**
314 * Multi substitution function with caching.
315 *
316 * This function should be a one-stop substitution function for working
317 * with HTML-template. It does not substitute by str_replace but by
318 * splitting. This secures that the value inserted does not themselves
319 * contain markers or subparts.
320 *
321 * Note that the "caching" won't cache the content of the substition,
322 * but only the splitting of the template in various parts. So if you
323 * want only one cache-entry per template, make sure you always pass the
324 * exact same set of marker/subpart keys. Else you will be flooding the
325 * user's cache table.
326 *
327 * This function takes three kinds of substitutions in one:
328 * $markContentArray is a regular marker-array where the 'keys' are
329 * substituted in $content with their values
330 *
331 * $subpartContentArray works exactly like markContentArray only is whole
332 * subparts substituted and not only a single marker.
333 *
334 * $wrappedSubpartContentArray is an array of arrays with 0/1 keys where
335 * the subparts pointed to by the main key is wrapped with the 0/1 value
336 * alternating.
337 *
338 * @param string $content The content stream, typically HTML template content.
339 * @param array $markContentArray Regular marker-array where the 'keys' are substituted in $content with their values
340 * @param array $subpartContentArray Exactly like markContentArray only is whole subparts substituted and not only a single marker.
341 * @param array $wrappedSubpartContentArray An array of arrays with 0/1 keys where the subparts pointed to by the main key is wrapped with the 0/1 value alternating.
342 * @return string The output content stream
343 * @see substituteSubpart(), substituteMarker(), substituteMarkerInObject(), TEMPLATE()
344 */
345 public function substituteMarkerArrayCached($content, array $markContentArray = null, array $subpartContentArray = null, array $wrappedSubpartContentArray = null)
346 {
347 $runtimeCache = $this->getRuntimeCache();
348 // If not arrays then set them
349 if (is_null($markContentArray)) {
350 // Plain markers
351 $markContentArray = [];
352 }
353 if (is_null($subpartContentArray)) {
354 // Subparts being directly substituted
355 $subpartContentArray = [];
356 }
357 if (is_null($wrappedSubpartContentArray)) {
358 // Subparts being wrapped
359 $wrappedSubpartContentArray = [];
360 }
361 // Finding keys and check hash:
362 $sPkeys = array_keys($subpartContentArray);
363 $wPkeys = array_keys($wrappedSubpartContentArray);
364 $keysToReplace = array_merge(array_keys($markContentArray), $sPkeys, $wPkeys);
365 if (empty($keysToReplace)) {
366 return $content;
367 }
368 asort($keysToReplace);
369 $storeKey = md5('substituteMarkerArrayCached_storeKey:' . serialize([$content, $keysToReplace]));
370 if ($runtimeCache->get($storeKey)) {
371 $storeArr = $runtimeCache->get($storeKey);
372 } else {
373 $cache = $this->getCache();
374 $storeArrDat = $cache->get($storeKey);
375 if (is_array($storeArrDat)) {
376 $storeArr = $storeArrDat;
377 // Setting the data in the first level cache
378 $runtimeCache->set($storeKey, $storeArr);
379 } else {
380 // Finding subparts and substituting them with the subpart as a marker
381 foreach ($sPkeys as $sPK) {
382 $content = $this->substituteSubpart($content, $sPK, $sPK);
383 }
384 // Finding subparts and wrapping them with markers
385 foreach ($wPkeys as $wPK) {
386 $content = $this->substituteSubpart($content, $wPK, [
387 $wPK,
388 $wPK
389 ]);
390 }
391
392 $storeArr = [];
393 // search all markers in the content
394 $result = preg_match_all('/###([^#](?:[^#]*+|#{1,2}[^#])+)###/', $content, $markersInContent);
395 if ($result !== false && !empty($markersInContent[1])) {
396 $keysToReplaceFlipped = array_flip($keysToReplace);
397 $regexKeys = [];
398 $wrappedKeys = [];
399 // Traverse keys and quote them for reg ex.
400 foreach ($markersInContent[1] as $key) {
401 if (isset($keysToReplaceFlipped['###' . $key . '###'])) {
402 $regexKeys[] = preg_quote($key, '/');
403 $wrappedKeys[] = '###' . $key . '###';
404 }
405 }
406 $regex = '/###(?:' . implode('|', $regexKeys) . ')###/';
407 $storeArr['c'] = preg_split($regex, $content); // contains all content parts around markers
408 $storeArr['k'] = $wrappedKeys; // contains all markers incl. ###
409 // Setting the data inside the second-level cache
410 $runtimeCache->set($storeKey, $storeArr);
411 // Storing the cached data permanently
412 $cache->set($storeKey, $storeArr, ['substMarkArrayCached'], 0);
413 }
414 }
415 }
416 if (!empty($storeArr['k']) && is_array($storeArr['k'])) {
417 // Substitution/Merging:
418 // Merging content types together, resetting
419 $valueArr = array_merge($markContentArray, $subpartContentArray, $wrappedSubpartContentArray);
420 $wSCA_reg = [];
421 $content = '';
422 // Traversing the keyList array and merging the static and dynamic content
423 foreach ($storeArr['k'] as $n => $keyN) {
424 // add content before marker
425 $content .= $storeArr['c'][$n];
426 if (!is_array($valueArr[$keyN])) {
427 // fetch marker replacement from $markContentArray or $subpartContentArray
428 $content .= $valueArr[$keyN];
429 } else {
430 if (!isset($wSCA_reg[$keyN])) {
431 $wSCA_reg[$keyN] = 0;
432 }
433 // fetch marker replacement from $wrappedSubpartContentArray
434 $content .= $valueArr[$keyN][$wSCA_reg[$keyN] % 2];
435 $wSCA_reg[$keyN]++;
436 }
437 }
438 // add remaining content
439 $content .= $storeArr['c'][count($storeArr['k'])];
440 }
441 return $content;
442 }
443
444 /**
445 * Substitute marker array in an array of values
446 *
447 * @param mixed $tree If string, then it just calls substituteMarkerArray. If array(and even multi-dim) then for each key/value pair the marker array will be substituted (by calling this function recursively)
448 * @param array $markContentArray The array of key/value pairs being marker/content values used in the substitution. For each element in this array the function will substitute a marker in the content string/array values.
449 * @return mixed The processed input variable.
450 * @see substituteMarker()
451 */
452 public function substituteMarkerInObject(&$tree, array $markContentArray)
453 {
454 if (is_array($tree)) {
455 foreach ($tree as $key => $value) {
456 $this->substituteMarkerInObject($tree[$key], $markContentArray);
457 }
458 } else {
459 $tree = $this->substituteMarkerArray($tree, $markContentArray);
460 }
461 return $tree;
462 }
463
464 /**
465 * Adds elements to the input $markContentArray based on the values from
466 * the fields from $fieldList found in $row
467 *
468 * @param array $markContentArray Array with key/values being marker-strings/substitution values.
469 * @param array $row An array with keys found in the $fieldList (typically a record) which values should be moved to the $markContentArray
470 * @param string $fieldList A list of fields from the $row array to add to the $markContentArray array. If empty all fields from $row will be added (unless they are integers)
471 * @param bool $nl2br If set, all values added to $markContentArray will be nl2br()'ed
472 * @param string $prefix Prefix string to the fieldname before it is added as a key in the $markContentArray. Notice that the keys added to the $markContentArray always start and end with "###
473 * @param bool $htmlSpecialCharsValue If set, all values are passed through htmlspecialchars() - RECOMMENDED to avoid most obvious XSS and maintain XHTML compliance.
474 * @param bool $respectXhtml if set, and $nl2br is set, then the new lines are added with <br /> instead of <br>
475 * @return array The modified $markContentArray
476 */
477 public function fillInMarkerArray(array $markContentArray, array $row, $fieldList = '', $nl2br = true, $prefix = 'FIELD_', $htmlSpecialCharsValue = false, $respectXhtml = false)
478 {
479 if ($fieldList) {
480 $fArr = GeneralUtility::trimExplode(',', $fieldList, true);
481 foreach ($fArr as $field) {
482 $markContentArray['###' . $prefix . $field . '###'] = $nl2br ? nl2br($row[$field], $respectXhtml) : $row[$field];
483 }
484 } else {
485 if (is_array($row)) {
486 foreach ($row as $field => $value) {
487 if (!MathUtility::canBeInterpretedAsInteger($field)) {
488 if ($htmlSpecialCharsValue) {
489 $value = htmlspecialchars($value);
490 }
491 $markContentArray['###' . $prefix . $field . '###'] = $nl2br ? nl2br($value, $respectXhtml) : $value;
492 }
493 }
494 }
495 }
496 return $markContentArray;
497 }
498
499 /**
500 * Second-level cache
501 *
502 * @return \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
503 */
504 protected function getCache()
505 {
506 return GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_hash');
507 }
508
509 /**
510 * First-level cache (runtime cache)
511 *
512 * @return \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
513 */
514 protected function getRuntimeCache()
515 {
516 return GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
517 }
518 }