Passed
Push — master ( 694c78...f1dcbc )
by Aimeos
06:27 queued 02:01
created

Base::view()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2012
6
 * @copyright Aimeos (aimeos.org), 2015-2021
7
 * @package Client
8
 * @subpackage Html
9
 */
10
11
12
namespace Aimeos\Client\Html;
13
14
use \Aimeos\MW\Logger\Base as Log;
15
16
17
/**
18
 * Common abstract class for all HTML client classes.
19
 *
20
 * @package Client
21
 * @subpackage Html
22
 */
23
abstract class Base
24
	implements \Aimeos\Client\Html\Iface, \Aimeos\MW\Macro\Iface
25
{
26
	use \Aimeos\MW\Macro\Traits;
27
28
29
	private $view;
30
	private $cache;
31
	private $object;
32
	private $context;
33
	private $subclients;
34
35
36
	/**
37
	 * Initializes the class instance.
38
	 *
39
	 * @param \Aimeos\MShop\Context\Item\Iface $context Context object
40
	 */
41
	public function __construct( \Aimeos\MShop\Context\Item\Iface $context )
42
	{
43
		$this->context = $context;
44
	}
45
46
47
	/**
48
	 * Catch unknown methods
49
	 *
50
	 * @param string $name Name of the method
51
	 * @param array $param List of method parameter
52
	 * @throws \Aimeos\Client\Html\Exception If method call failed
53
	 */
54
	public function __call( string $name, array $param )
55
	{
56
		throw new \Aimeos\Client\Html\Exception( sprintf( 'Unable to call method "%1$s"', $name ) );
57
	}
58
59
60
	/**
61
	 * Adds the data to the view object required by the templates
62
	 *
63
	 * @param \Aimeos\MW\View\Iface $view The view object which generates the HTML output
64
	 * @param array &$tags Result array for the list of tags that are associated to the output
65
	 * @param string|null &$expire Result variable for the expiration date of the output (null for no expiry)
66
	 * @return \Aimeos\MW\View\Iface The view object with the data required by the templates
67
	 * @since 2018.01
68
	 */
69
	public function data( \Aimeos\MW\View\Iface $view, array &$tags = [], string &$expire = null ) : \Aimeos\MW\View\Iface
70
	{
71
		foreach( $this->getSubClients() as $name => $subclient ) {
72
			$view = $subclient->data( $view, $tags, $expire );
73
		}
74
75
		return $view;
76
	}
77
78
79
	/**
80
	 * Returns the HTML string for insertion into the header.
81
	 *
82
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
83
	 * @return string|null String including HTML tags for the header on error
84
	 */
85
	public function header( string $uid = '' ) : ?string
86
	{
87
		$html = '';
88
89
		foreach( $this->getSubClients() as $subclient ) {
90
			$html .= $subclient->setView( $this->view )->header( $uid );
91
		}
92
93
		return $html;
94
	}
95
96
97
	/**
98
	 * Processes the input, e.g. store given values.
99
	 *
100
	 * A view must be available and this method doesn't generate any output
101
	 * besides setting view variables.
102
	 */
103
	public function init()
104
	{
105
		$view = $this->view();
106
107
		foreach( $this->getSubClients() as $subclient ) {
108
			$subclient->setView( $view )->init();
109
		}
110
	}
111
112
113
	/**
114
	 * Returns the outmost decorator of the decorator stack
115
	 *
116
	 * @return \Aimeos\Client\Html\Iface Outmost decorator object
117
	 */
118
	protected function getObject() : \Aimeos\Client\Html\Iface
119
	{
120
		if( $this->object !== null ) {
121
			return $this->object;
122
		}
123
124
		return $this;
125
	}
126
127
128
	/**
129
	 * Returns the view object that will generate the HTML output.
130
	 *
131
	 * @return \Aimeos\MW\View\Iface $view The view object which generates the HTML output
132
	 */
133
	public function view() : \Aimeos\MW\View\Iface
134
	{
135
		if( !isset( $this->view ) ) {
136
			throw new \Aimeos\Client\Html\Exception( sprintf( 'No view available' ) );
137
		}
138
139
		return $this->view;
140
	}
141
142
143
	/**
144
	 * Modifies the cached body content to replace content based on sessions or cookies.
145
	 *
146
	 * @param string $content Cached content
147
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
148
	 * @return string Modified body content
149
	 */
150
	public function modifyBody( string $content, string $uid ) : string
151
	{
152
		$view = $this->view();
153
154
		foreach( $this->getSubClients() as $subclient )
155
		{
156
			$subclient->setView( $view );
157
			$content = $subclient->modifyBody( $content, $uid );
158
		}
159
160
		return $content;
161
	}
162
163
164
	/**
165
	 * Modifies the cached header content to replace content based on sessions or cookies.
166
	 *
167
	 * @param string $content Cached content
168
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
169
	 * @return string Modified header content
170
	 */
171
	public function modifyHeader( string $content, string $uid ) : string
172
	{
173
		$view = $this->view();
174
175
		foreach( $this->getSubClients() as $subclient )
176
		{
177
			$subclient->setView( $view );
178
			$content = $subclient->modifyHeader( $content, $uid );
179
		}
180
181
		return $content;
182
	}
183
184
185
	/**
186
	 * Injects the reference of the outmost client object or decorator
187
	 *
188
	 * @param \Aimeos\Client\Html\Iface $object Reference to the outmost client or decorator
189
	 * @return \Aimeos\Client\Html\Iface Client object for chaining method calls
190
	 */
191
	public function setObject( \Aimeos\Client\Html\Iface $object ) : \Aimeos\Client\Html\Iface
192
	{
193
		$this->object = $object;
194
		return $this;
195
	}
196
197
198
	/**
199
	 * Sets the view object that will generate the HTML output.
200
	 *
201
	 * @param \Aimeos\MW\View\Iface $view The view object which generates the HTML output
202
	 * @return \Aimeos\Client\Html\Iface Reference to this object for fluent calls
203
	 */
204
	public function setView( \Aimeos\MW\View\Iface $view ) : \Aimeos\Client\Html\Iface
205
	{
206
		$this->view = $view;
207
		return $this;
208
	}
209
210
211
	/**
212
	 * Adds the decorators to the client object
213
	 *
214
	 * @param \Aimeos\Client\Html\Iface $client Client object
215
	 * @param array $decorators List of decorator name that should be wrapped around the client
216
	 * @param string $classprefix Decorator class prefix, e.g. "\Aimeos\Client\Html\Catalog\Decorator\"
217
	 * @return \Aimeos\Client\Html\Iface Client object
218
	 */
219
	protected function addDecorators( \Aimeos\Client\Html\Iface $client, array $decorators, string $classprefix ) : \Aimeos\Client\Html\Iface
220
	{
221
		foreach( $decorators as $name )
222
		{
223
			if( ctype_alnum( $name ) === false )
224
			{
225
				$classname = is_string( $name ) ? $classprefix . $name : '<not a string>';
226
				throw new \Aimeos\Client\Html\Exception( sprintf( 'Invalid class name "%1$s"', $classname ) );
227
			}
228
229
			$classname = $classprefix . $name;
230
231
			if( class_exists( $classname ) === false ) {
232
				throw new \Aimeos\Client\Html\Exception( sprintf( 'Class "%1$s" not found', $classname ) );
233
			}
234
235
			$client = new $classname( $client, $this->context );
236
237
			\Aimeos\MW\Common\Base::checkClass( '\\Aimeos\\Client\\Html\\Common\\Decorator\\Iface', $client );
238
		}
239
240
		return $client;
241
	}
242
243
244
	/**
245
	 * Adds the decorators to the client object
246
	 *
247
	 * @param \Aimeos\Client\Html\Iface $client Client object
248
	 * @param string $path Client string in lower case, e.g. "catalog/detail/basic"
249
	 * @return \Aimeos\Client\Html\Iface Client object
250
	 */
251
	protected function addClientDecorators( \Aimeos\Client\Html\Iface $client, string $path ) : \Aimeos\Client\Html\Iface
252
	{
253
		if( !is_string( $path ) || $path === '' ) {
0 ignored issues
show
introduced by
The condition is_string($path) is always true.
Loading history...
254
			throw new \Aimeos\Client\Html\Exception( sprintf( 'Invalid domain "%1$s"', $path ) );
255
		}
256
257
		$localClass = str_replace( '/', '\\', ucwords( $path, '/' ) );
258
		$config = $this->context->getConfig();
259
260
		$classprefix = '\\Aimeos\\Client\\Html\\Common\\Decorator\\';
261
		$decorators = $config->get( 'client/html/' . $path . '/decorators/global', [] );
262
		$client = $this->addDecorators( $client, $decorators, $classprefix );
263
264
		$classprefix = '\\Aimeos\\Client\\Html\\' . $localClass . '\\Decorator\\';
265
		$decorators = $config->get( 'client/html/' . $path . '/decorators/local', [] );
266
		$client = $this->addDecorators( $client, $decorators, $classprefix );
267
268
		return $client;
269
	}
270
271
272
	/**
273
	 * Adds the cache tags to the given list and sets a new expiration date if necessary based on the given item.
274
	 *
275
	 * @param array|\Aimeos\MShop\Common\Item\Iface $items Item or list of items, maybe with associated list items
276
	 * @param string|null &$expire Expiration date that will be overwritten if an earlier date is found
277
	 * @param array &$tags List of tags the new tags will be added to
278
	 * @param array $custom List of custom tags which are added too
279
	 */
280
	protected function addMetaItems( $items, string &$expire = null, array &$tags, array $custom = [] )
281
	{
282
		/** client/html/common/cache/tag-all
283
		 * Adds tags for all items used in a cache entry
284
		 *
285
		 * Each cache entry storing rendered parts for the HTML header or body
286
		 * can be tagged with information which items like texts, media, etc.
287
		 * are used in the HTML. This allows removing only those cache entries
288
		 * whose content has really changed and only that entries have to be
289
		 * rebuild the next time.
290
		 *
291
		 * The standard behavior stores only tags for each used domain, e.g. if
292
		 * a text is used, only the tag "text" is added. If you change a text
293
		 * in the administration interface, all cache entries with the tag
294
		 * "text" will be removed from the cache. This effectively wipes out
295
		 * almost all cached entries, which have to be rebuild with the next
296
		 * request.
297
		 *
298
		 * Important: As a list or detail view can use several hundred items,
299
		 * this configuration option will also add this number of tags to the
300
		 * cache entry. When using a cache adapter that can't insert all tags
301
		 * at once, this slows down the initial cache insert (and therefore the
302
		 * page speed) drastically! It's only recommended to enable this option
303
		 * if you use the DB, Mysql or Redis adapter that can insert all tags
304
		 * at once.
305
		 *
306
		 * @param boolean True to add tags for all items, false to use only a domain tag
307
		 * @since 2014.07
308
		 * @category Developer
309
		 * @category User
310
		 * @see client/html/common/cache/force
311
		 * @see madmin/cache/manager/name
312
		 * @see madmin/cache/name
313
		 */
314
		$tagAll = $this->context->getConfig()->get( 'client/html/common/cache/tag-all', false );
315
316
		if( !is_array( $items ) && !is_map( $items ) ) {
317
			$items = map( [$items] );
318
		}
319
320
		$expires = $idMap = [];
321
322
		foreach( $items as $item )
323
		{
324
			if( $item instanceof \Aimeos\MShop\Common\Item\ListsRef\Iface )
325
			{
326
				$this->addMetaItemRef( $item, $expires, $tags, $tagAll );
327
				$idMap[$item->getResourceType()][] = $item->getId();
328
			}
329
330
			$this->addMetaItemSingle( $item, $expires, $tags, $tagAll );
331
		}
332
333
		if( $expire !== null ) {
334
			$expires[] = $expire;
335
		}
336
337
		if( !empty( $expires ) ) {
338
			$expire = min( $expires );
339
		}
340
341
		$tags = array_unique( array_merge( $tags, $custom ) );
342
	}
343
344
345
	/**
346
	 * Adds expire date and tags for a single item.
347
	 *
348
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item, maybe with associated list items
349
	 * @param array &$expires Will contain the list of expiration dates
350
	 * @param array &$tags List of tags the new tags will be added to
351
	 * @param bool $tagAll True of tags for all items should be added, false if only for the main item
352
	 */
353
	private function addMetaItemSingle( \Aimeos\MShop\Common\Item\Iface $item, array &$expires, array &$tags, bool $tagAll )
354
	{
355
		$domain = str_replace( '/', '_', $item->getResourceType() ); // maximum compatiblity
356
357
		if( $tagAll === true ) {
358
			$tags[] = $domain . '-' . $item->getId();
359
		} else {
360
			$tags[] = $domain;
361
		}
362
363
		if( $item instanceof \Aimeos\MShop\Common\Item\Time\Iface && ( $date = $item->getDateEnd() ) !== null ) {
364
			$expires[] = $date;
365
		}
366
367
		if( $item instanceof \Aimeos\MShop\Common\Item\ListsRef\Iface ) {
368
			$this->addMetaItemRef( $item, $expires, $tags, $tagAll );
369
		}
370
	}
371
372
373
	/**
374
	 * Adds expire date and tags for referenced items
375
	 *
376
	 * @param \Aimeos\MShop\Common\Item\ListsRef\Iface $item Item with associated list items
377
	 * @param array &$expires Will contain the list of expiration dates
378
	 * @param array &$tags List of tags the new tags will be added to
379
	 * @param bool $tagAll True of tags for all items should be added, false if only for the main item
380
	 */
381
	private function addMetaItemRef( \Aimeos\MShop\Common\Item\ListsRef\Iface $item, array &$expires, array &$tags, bool $tagAll )
382
	{
383
		foreach( $item->getListItems() as $listitem )
384
		{
385
			if( ( $refItem = $listitem->getRefItem() ) === null ) {
386
				continue;
387
			}
388
389
			if( $tagAll === true ) {
390
				$tags[] = str_replace( '/', '_', $listitem->getDomain() ) . '-' . $listitem->getRefId();
391
			}
392
393
			if( ( $date = $listitem->getDateEnd() ) !== null ) {
394
				$expires[] = $date;
395
			}
396
397
			$this->addMetaItemSingle( $refItem, $expires, $tags, $tagAll );
398
		}
399
	}
400
401
402
	/**
403
	 * Returns the sub-client given by its name.
404
	 *
405
	 * @param string $path Name of the sub-part in lower case (can contain a path like catalog/filter/tree)
406
	 * @param string|null $name Name of the implementation, will be from configuration (or Default) if null
407
	 * @return \Aimeos\Client\Html\Iface Sub-part object
408
	 */
409
	protected function createSubClient( string $path, string $name = null ) : \Aimeos\Client\Html\Iface
410
	{
411
		$path = strtolower( $path );
412
413
		if( $name === null ) {
414
			$name = $this->context->getConfig()->get( 'client/html/' . $path . '/name', 'Standard' );
415
		}
416
417
		if( empty( $name ) || ctype_alnum( $name ) === false ) {
418
			throw new \Aimeos\Client\Html\Exception( sprintf( 'Invalid characters in client name "%1$s"', $name ) );
419
		}
420
421
		$subnames = str_replace( '/', '\\', ucwords( $path, '/' ) );
422
		$classname = '\\Aimeos\\Client\\Html\\' . $subnames . '\\' . $name;
423
424
		if( class_exists( $classname ) === false ) {
425
			throw new \Aimeos\Client\Html\Exception( sprintf( 'Class "%1$s" not available', $classname ) );
426
		}
427
428
		$object = new $classname( $this->context );
429
		$object = \Aimeos\MW\Common\Base::checkClass( '\\Aimeos\\Client\\Html\\Iface', $object );
430
		$object = $this->addClientDecorators( $object, $path );
431
432
		return $object->setObject( $object );
433
	}
434
435
436
	/**
437
	 * Returns the minimal expiration date.
438
	 *
439
	 * @param string|null $first First expiration date or null
440
	 * @param string|null $second Second expiration date or null
441
	 * @return string|null Expiration date
442
	 */
443
	protected function expires( string $first = null, string $second = null ) : ?string
444
	{
445
		return ( $first !== null ? ( $second !== null ? min( $first, $second ) : $first ) : $second );
446
	}
447
448
449
	/**
450
	 * Returns the parameters used by the html client.
451
	 *
452
	 * @param array $params Associative list of all parameters
453
	 * @param array $prefixes List of prefixes the parameters must start with
454
	 * @return array Associative list of parameters used by the html client
455
	 */
456
	protected function getClientParams( array $params, array $prefixes = ['f_', 'l_', 'd_'] ) : array
457
	{
458
		return map( $params )->filter( function( $val, $key ) use ( $prefixes ) {
459
			return \Aimeos\MW\Str::starts( $key, $prefixes );
460
		} )->toArray();
461
	}
462
463
464
	/**
465
	 * Returns the context object.
466
	 *
467
	 * @return \Aimeos\MShop\Context\Item\Iface Context object
468
	 */
469
	protected function getContext() : \Aimeos\MShop\Context\Item\Iface
470
	{
471
		return $this->context;
472
	}
473
474
475
	/**
476
	 * Generates an unique hash from based on the input suitable to be used as part of the cache key
477
	 *
478
	 * @param array $prefixes List of prefixes the parameters must start with
479
	 * @param string $key Unique identifier if the content is placed more than once on the same page
480
	 * @param array $config Multi-dimensional array of configuration options used by the client and sub-clients
481
	 * @return string Unique hash
482
	 */
483
	protected function getParamHash( array $prefixes = ['f_', 'l_', 'd_'], string $key = '', array $config = [] ) : string
484
	{
485
		$locale = $this->getContext()->getLocale();
486
		$pstr = map( $this->getClientParams( $this->view()->param(), $prefixes ) )->ksort()->toJson();
487
488
		if( ( $cstr = json_encode( $config ) ) === false ) {
489
			throw new \Aimeos\Client\Html\Exception( 'Unable to encode parameters or configuration options' );
490
		}
491
492
		return md5( $key . $pstr . $cstr . $locale->getLanguageId() . $locale->getCurrencyId() . $locale->getSiteId() );
493
	}
494
495
496
	/**
497
	 * Returns the list of sub-client names configured for the client.
498
	 *
499
	 * @return array List of HTML client names
500
	 */
501
	abstract protected function getSubClientNames() : array;
502
503
504
	/**
505
	 * Returns the configured sub-clients or the ones named in the default parameter if none are configured.
506
	 *
507
	 * @return array List of sub-clients implementing \Aimeos\Client\Html\Iface	ordered in the same way as the names
508
	 */
509
	protected function getSubClients() : array
510
	{
511
		if( !isset( $this->subclients ) )
512
		{
513
			$this->subclients = [];
514
515
			foreach( $this->getSubClientNames() as $name ) {
516
				$this->subclients[$name] = $this->getSubClient( $name );
517
			}
518
		}
519
520
		return $this->subclients;
521
	}
522
523
524
	/**
525
	 * Returns the template for the given configuration key
526
	 *
527
	 * If the "l_type" parameter is present, a specific template for this given
528
	 * type is used if available.
529
	 *
530
	 * @param string $confkey Key to the configuration setting for the template
531
	 * @param string $default Default template if none is configured or not found
532
	 * @return string Relative template path
533
	 */
534
	protected function getTemplatePath( string $confkey, string $default ) : string
535
	{
536
		if( ( $type = $this->view->param( 'l_type' ) ) !== null && ctype_alnum( $type ) !== false ) {
537
			return $this->view->config( $confkey . '-' . $type, $this->view->config( $confkey, $default ) );
538
		}
539
540
		return $this->view->config( $confkey, $default );
541
	}
542
543
544
	/**
545
	 * Returns the cache entry for the given unique ID and type.
546
	 *
547
	 * @param string $type Type of the cache entry, i.e. "body" or "header"
548
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
549
	 * @param string[] $prefixes List of prefixes of all parameters that are relevant for generating the output
550
	 * @param string $confkey Configuration key prefix that matches all relevant settings for the component
551
	 * @return string|null Cached entry or null if not available
552
	 */
553
	protected function getCached( string $type, string $uid, array $prefixes, string $confkey ) : ?string
554
	{
555
		$context = $this->getContext();
556
		$config = $context->getConfig();
557
558
		/** client/html/common/cache/force
559
		 * Enforces content caching regardless of user logins
560
		 *
561
		 * Caching the component output is normally disabled as soon as the
562
		 * user has logged in. This enables displaying user or user group
563
		 * specific content without mixing standard and user specific output.
564
		 *
565
		 * If you don't have any user or user group specific content
566
		 * (products, categories, attributes, media, prices, texts, etc.),
567
		 * you can enforce content caching nevertheless to keep response
568
		 * times as low as possible.
569
		 *
570
		 * @param boolean True to cache output regardless of login, false for no caching
571
		 * @since 2015.08
572
		 * @category Developer
573
		 * @category User
574
		 * @see client/html/common/cache/tag-all
575
		 */
576
		$force = $config->get( 'client/html/common/cache/force', false );
577
		$enable = $config->get( $confkey . '/cache', true );
578
579
		if( $enable == false || $force == false && $context->getUserId() !== null ) {
580
			return null;
581
		}
582
583
		$cfg = array_merge( $config->get( 'client/html', [] ), $this->getSubClientNames() );
584
585
		$keys = array(
586
			'body' => $this->getParamHash( $prefixes, $uid . ':' . $confkey . ':body', $cfg ),
587
			'header' => $this->getParamHash( $prefixes, $uid . ':' . $confkey . ':header', $cfg ),
588
		);
589
590
		if( !isset( $this->cache[$keys[$type]] ) ) {
591
			$this->cache = $context->getCache()->getMultiple( $keys );
592
		}
593
594
		return ( isset( $this->cache[$keys[$type]] ) ? $this->cache[$keys[$type]] : null );
595
	}
596
597
598
	/**
599
	 * Returns the cache entry for the given type and unique ID.
600
	 *
601
	 * @param string $type Type of the cache entry, i.e. "body" or "header"
602
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
603
	 * @param string[] $prefixes List of prefixes of all parameters that are relevant for generating the output
604
	 * @param string $confkey Configuration key prefix that matches all relevant settings for the component
605
	 * @param string $value Value string that should be stored for the given key
606
	 * @param array $tags List of tag strings that should be assoicated to the given value in the cache
607
	 * @param string|null $expire Date/time string in "YYYY-MM-DD HH:mm:ss"	format when the cache entry expires
608
	 */
609
	protected function setCached( string $type, string $uid, array $prefixes, string $confkey, string $value, array $tags, string $expire = null )
610
	{
611
		$context = $this->getContext();
612
		$config = $context->getConfig();
613
614
		$force = $config->get( 'client/html/common/cache/force', false );
615
		$enable = $config->get( $confkey . '/cache', true );
616
617
		if( $enable == false || $force == false && $context->getUserId() !== null ) {
618
			return;
619
		}
620
621
		try
622
		{
623
			$cfg = array_merge( $config->get( 'client/html', [] ), $this->getSubClientNames() );
624
			$key = $this->getParamHash( $prefixes, $uid . ':' . $confkey . ':' . $type, $cfg );
625
626
			$context->getCache()->set( $key, $value, $expire, array_unique( $tags ) );
627
		}
628
		catch( \Exception $e )
629
		{
630
			$msg = sprintf( 'Unable to set cache entry: %1$s', $e->getMessage() );
631
			$context->getLogger()->log( $msg, Log::NOTICE, 'client/html' );
632
		}
633
	}
634
635
636
	/**
637
	 * Writes the exception details to the log
638
	 *
639
	 * @param \Exception $e Exception object
640
	 */
641
	protected function logException( \Exception $e )
642
	{
643
		$msg = $e->getMessage() . PHP_EOL . $e->getTraceAsString();
644
		$this->context->getLogger()->log( $msg, Log::WARN, 'client/html' );
645
	}
646
647
648
	/**
649
	 * Replaces the section in the content that is enclosed by the marker.
650
	 *
651
	 * @param string $content Cached content
652
	 * @param string $section New section content
653
	 * @param string $marker Name of the section marker without "<!-- " and " -->" parts
654
	 */
655
	protected function replaceSection( string $content, string $section, string $marker ) : string
656
	{
657
		$start = 0;
658
		$len = strlen( $section );
659
		$clen = strlen( $content );
660
		$marker = '<!-- ' . $marker . ' -->';
661
662
		while( $start < $clen && ( $start = @strpos( $content, $marker, $start ) ) !== false )
663
		{
664
			if( ( $end = strpos( $content, $marker, $start + 1 ) ) !== false ) {
665
				$content = substr_replace( $content, $section, $start, $end - $start + strlen( $marker ) );
666
			}
667
668
			$start += 2 * strlen( $marker ) + $len;
669
		}
670
671
		return $content;
672
	}
673
}
674