Passed
Push — master ( 238462...8134c1 )
by Aimeos
04:27
created

Base::translatePluginErrorCodes()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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