[BUGFIX] Fix several typos in php comments
[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 string|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 proceeding 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 * Substitutes 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()
195 * @see substituteMarkerInObject()
196 */
197 public function substituteMarkerArray($content, $markContentArray, $wrap = '', $uppercase = false, $deleteUnused = false)
198 {
199 if (is_array($markContentArray)) {
200 $wrapArr = GeneralUtility::trimExplode('|', $wrap);
201 $search = [];
202 $replace = [];
203 foreach ($markContentArray as $marker => $markContent) {
204 if ($uppercase) {
205 // use strtr instead of strtoupper to avoid locale problems with Turkish
206 $marker = strtr($marker, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
207 }
208 if (isset($wrapArr[0], $wrapArr[1])) {
209 $marker = $wrapArr[0] . $marker . $wrapArr[1];
210 }
211 $search[] = $marker;
212 $replace[] = $markContent;
213 }
214 $content = str_replace($search, $replace, $content);
215 unset($search, $replace);
216 if ($deleteUnused) {
217 if (empty($wrap)) {
218 $wrapArr = ['###', '###'];
219 }
220 $content = preg_replace('/' . preg_quote($wrapArr[0], '/') . '([A-Z0-9_|\\-]*)' . preg_quote($wrapArr[1], '/') . '/is', '', $content);
221 }
222 }
223
224 return $content;
225 }
226
227 /**
228 * Replaces all markers and subparts in a template with the content provided in the structured array.
229 *
230 * The array is built like the template with its markers and subparts. Keys represent the marker name and the values the
231 * content.
232 * If the value is not an array the key will be treated as a single marker.
233 * If the value is an array the key will be treated as a subpart marker.
234 * Repeated subpart contents are of course elements in the array, so every subpart value must contain an array with its
235 * markers.
236 *
237 * $markersAndSubparts = array (
238 * '###SINGLEMARKER1###' => 'value 1',
239 * '###SUBPARTMARKER1###' => array(
240 * 0 => array(
241 * '###SINGLEMARKER2###' => 'value 2',
242 * ),
243 * 1 => array(
244 * '###SINGLEMARKER2###' => 'value 3',
245 * )
246 * ),
247 * '###SUBPARTMARKER2###' => array(
248 * ),
249 * )
250 * Subparts can be nested, so below the 'SINGLEMARKER2' it is possible to have another subpart marker with an array as the
251 * value, which in its turn contains the elements of the sub-subparts.
252 * Empty arrays for Subparts will cause the subtemplate to be cleared.
253 *
254 * @param string $content The content stream, typically HTML template content.
255 * @param array $markersAndSubparts The array of single markers and subpart contents.
256 * @param string $wrap A wrap value - [part1] | [part2] - for the markers before substitution.
257 * @param bool $uppercase If set, all marker string substitution is done with upper-case markers.
258 * @param bool $deleteUnused If set, all unused single markers are deleted.
259 *
260 * @return string The processed output stream
261 */
262 public function substituteMarkerAndSubpartArrayRecursive($content, array $markersAndSubparts, $wrap = '', $uppercase = false, $deleteUnused = false)
263 {
264 $wraps = GeneralUtility::trimExplode('|', $wrap);
265 $singleItems = [];
266 $compoundItems = [];
267 // Split markers and subparts into separate arrays
268 foreach ($markersAndSubparts as $markerName => $markerContent) {
269 if (is_array($markerContent)) {
270 $compoundItems[] = $markerName;
271 } else {
272 $singleItems[$markerName] = $markerContent;
273 }
274 }
275 $subTemplates = [];
276 $subpartSubstitutes = [];
277 // Build a cache for the sub template
278 foreach ($compoundItems as $subpartMarker) {
279 if ($uppercase) {
280 // Use strtr instead of strtoupper to avoid locale problems with Turkish
281 $subpartMarker = strtr($subpartMarker, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
282 }
283 if (isset($wraps[0], $wraps[1])) {
284 $subpartMarker = $wraps[0] . $subpartMarker . $wraps[1];
285 }
286 $subTemplates[$subpartMarker] = $this->getSubpart($content, $subpartMarker);
287 }
288 // Replace the subpart contents recursively
289 foreach ($compoundItems as $subpartMarker) {
290 $completeMarker = $subpartMarker;
291 if ($uppercase) {
292 // use strtr instead of strtoupper to avoid locale problems with Turkish
293 $completeMarker = strtr($completeMarker, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
294 }
295 if (isset($wraps[0], $wraps[1])) {
296 $completeMarker = $wraps[0] . $completeMarker . $wraps[1];
297 }
298 if (!empty($markersAndSubparts[$subpartMarker])) {
299 $subpartSubstitutes[$completeMarker] = '';
300 foreach ($markersAndSubparts[$subpartMarker] as $partialMarkersAndSubparts) {
301 $subpartSubstitutes[$completeMarker] .= $this->substituteMarkerAndSubpartArrayRecursive(
302 $subTemplates[$completeMarker],
303 $partialMarkersAndSubparts,
304 $wrap,
305 $uppercase,
306 $deleteUnused
307 );
308 }
309 } else {
310 $subpartSubstitutes[$completeMarker] = '';
311 }
312 }
313 // Substitute the single markers and subparts
314 $result = $this->substituteSubpartArray($content, $subpartSubstitutes);
315 $result = $this->substituteMarkerArray($result, $singleItems, $wrap, $uppercase, $deleteUnused);
316
317 return $result;
318 }
319
320 /**
321 * Multi substitution function with caching.
322 *
323 * This function should be a one-stop substitution function for working
324 * with HTML-template. It does not substitute by str_replace but by
325 * splitting. This secures that the value inserted does not themselves
326 * contain markers or subparts.
327 *
328 * Note that the "caching" won't cache the content of the substitution,
329 * but only the splitting of the template in various parts. So if you
330 * want only one cache-entry per template, make sure you always pass the
331 * exact same set of marker/subpart keys. Else you will be flooding the
332 * user's cache table.
333 *
334 * This function takes three kinds of substitutions in one:
335 * $markContentArray is a regular marker-array where the 'keys' are
336 * substituted in $content with their values
337 *
338 * $subpartContentArray works exactly like markContentArray only is whole
339 * subparts substituted and not only a single marker.
340 *
341 * $wrappedSubpartContentArray is an array of arrays with 0/1 keys where
342 * the subparts pointed to by the main key is wrapped with the 0/1 value
343 * alternating.
344 *
345 * @param string $content The content stream, typically HTML template content.
346 * @param array $markContentArray Regular marker-array where the 'keys' are substituted in $content with their values
347 * @param array $subpartContentArray Exactly like markContentArray only is whole subparts substituted and not only a single marker.
348 * @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.
349 * @return string The output content stream
350 * @see substituteSubpart()
351 * @see substituteMarker()
352 * @see substituteMarkerInObject()
353 */
354 public function substituteMarkerArrayCached($content, array $markContentArray = null, array $subpartContentArray = null, array $wrappedSubpartContentArray = null)
355 {
356 $runtimeCache = $this->getRuntimeCache();
357 // If not arrays then set them
358 if ($markContentArray === null) {
359 // Plain markers
360 $markContentArray = [];
361 }
362 if ($subpartContentArray === null) {
363 // Subparts being directly substituted
364 $subpartContentArray = [];
365 }
366 if ($wrappedSubpartContentArray === null) {
367 // Subparts being wrapped
368 $wrappedSubpartContentArray = [];
369 }
370 // Finding keys and check hash:
371 $sPkeys = array_keys($subpartContentArray);
372 $wPkeys = array_keys($wrappedSubpartContentArray);
373 $keysToReplace = array_merge(array_keys($markContentArray), $sPkeys, $wPkeys);
374 if (empty($keysToReplace)) {
375 return $content;
376 }
377 asort($keysToReplace);
378 $storeKey = md5('substituteMarkerArrayCached_storeKey:' . serialize([$content, $keysToReplace]));
379 $fromCache = $runtimeCache->get($storeKey);
380 if ($fromCache) {
381 $storeArr = $fromCache;
382 } else {
383 $cache = $this->getCache();
384 $storeArrDat = $cache->get($storeKey);
385 if (is_array($storeArrDat)) {
386 $storeArr = $storeArrDat;
387 // Setting the data in the first level cache
388 $runtimeCache->set($storeKey, $storeArr);
389 } else {
390 // Finding subparts and substituting them with the subpart as a marker
391 foreach ($sPkeys as $sPK) {
392 $content = $this->substituteSubpart($content, $sPK, $sPK);
393 }
394 // Finding subparts and wrapping them with markers
395 foreach ($wPkeys as $wPK) {
396 $content = $this->substituteSubpart($content, $wPK, [
397 $wPK,
398 $wPK
399 ]);
400 }
401
402 $storeArr = [];
403 // search all markers in the content
404 $result = preg_match_all('/###([^#](?:[^#]*+|#{1,2}[^#])+)###/', $content, $markersInContent);
405 if ($result !== false && !empty($markersInContent[1])) {
406 $keysToReplaceFlipped = array_flip($keysToReplace);
407 $regexKeys = [];
408 $wrappedKeys = [];
409 // Traverse keys and quote them for reg ex.
410 foreach ($markersInContent[1] as $key) {
411 if (isset($keysToReplaceFlipped['###' . $key . '###'])) {
412 $regexKeys[] = preg_quote($key, '/');
413 $wrappedKeys[] = '###' . $key . '###';
414 }
415 }
416 $regex = '/###(?:' . implode('|', $regexKeys) . ')###/';
417 $storeArr['c'] = preg_split($regex, $content); // contains all content parts around markers
418 $storeArr['k'] = $wrappedKeys; // contains all markers incl. ###
419 // Setting the data inside the second-level cache
420 $runtimeCache->set($storeKey, $storeArr);
421 // Storing the cached data permanently
422 $cache->set($storeKey, $storeArr, ['substMarkArrayCached'], 0);
423 }
424 }
425 }
426 if (!empty($storeArr['k']) && is_array($storeArr['k'])) {
427 // Substitution/Merging:
428 // Merging content types together, resetting
429 $valueArr = array_merge($markContentArray, $subpartContentArray, $wrappedSubpartContentArray);
430 $wSCA_reg = [];
431 $content = '';
432 // Traversing the keyList array and merging the static and dynamic content
433 foreach ($storeArr['k'] as $n => $keyN) {
434 // add content before marker
435 $content .= $storeArr['c'][$n];
436 if (!is_array($valueArr[$keyN])) {
437 // fetch marker replacement from $markContentArray or $subpartContentArray
438 $content .= $valueArr[$keyN];
439 } else {
440 if (!isset($wSCA_reg[$keyN])) {
441 $wSCA_reg[$keyN] = 0;
442 }
443 // fetch marker replacement from $wrappedSubpartContentArray
444 $content .= $valueArr[$keyN][$wSCA_reg[$keyN] % 2];
445 $wSCA_reg[$keyN]++;
446 }
447 }
448 // add remaining content
449 $content .= $storeArr['c'][count($storeArr['k'])];
450 }
451 return $content;
452 }
453
454 /**
455 * Substitute marker array in an array of values
456 *
457 * @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)
458 * @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.
459 * @return mixed The processed input variable.
460 * @see substituteMarker()
461 */
462 public function substituteMarkerInObject(&$tree, array $markContentArray)
463 {
464 if (is_array($tree)) {
465 foreach ($tree as $key => $value) {
466 $this->substituteMarkerInObject($tree[$key], $markContentArray);
467 }
468 } else {
469 $tree = $this->substituteMarkerArray($tree, $markContentArray);
470 }
471 return $tree;
472 }
473
474 /**
475 * Adds elements to the input $markContentArray based on the values from
476 * the fields from $fieldList found in $row
477 *
478 * @param array $markContentArray Array with key/values being marker-strings/substitution values.
479 * @param array $row An array with keys found in the $fieldList (typically a record) which values should be moved to the $markContentArray
480 * @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)
481 * @param bool $nl2br If set, all values added to $markContentArray will be nl2br()'ed
482 * @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 "###
483 * @param bool $htmlSpecialCharsValue If set, all values are passed through htmlspecialchars() - RECOMMENDED to avoid most obvious XSS and maintain XHTML compliance.
484 * @param bool $respectXhtml if set, and $nl2br is set, then the new lines are added with <br /> instead of <br>
485 * @return array The modified $markContentArray
486 */
487 public function fillInMarkerArray(array $markContentArray, array $row, $fieldList = '', $nl2br = true, $prefix = 'FIELD_', $htmlSpecialCharsValue = false, $respectXhtml = false)
488 {
489 if ($fieldList) {
490 $fArr = GeneralUtility::trimExplode(',', $fieldList, true);
491 foreach ($fArr as $field) {
492 $markContentArray['###' . $prefix . $field . '###'] = $nl2br ? nl2br($row[$field], $respectXhtml) : $row[$field];
493 }
494 } else {
495 if (is_array($row)) {
496 foreach ($row as $field => $value) {
497 if (!MathUtility::canBeInterpretedAsInteger($field)) {
498 if ($htmlSpecialCharsValue) {
499 $value = htmlspecialchars($value);
500 }
501 $markContentArray['###' . $prefix . $field . '###'] = $nl2br ? nl2br($value, $respectXhtml) : $value;
502 }
503 }
504 }
505 }
506 return $markContentArray;
507 }
508
509 /**
510 * Second-level cache
511 *
512 * @return \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
513 */
514 protected function getCache()
515 {
516 return GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
517 }
518
519 /**
520 * First-level cache (runtime cache)
521 *
522 * @return \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
523 */
524 protected function getRuntimeCache()
525 {
526 return GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
527 }
528 }