SSViewer   F
last analyzed

Complexity

Total Complexity 85

Size/Duplication

Total Lines 824
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 215
dl 0
loc 824
rs 2
c 0
b 0
f 0
wmc 85

35 Methods

Rating   Name   Duplication   Size   Complexity  
A includeRequirements() 0 3 1
A setRewriteHashLinks() 0 4 1
A chooseTemplate() 0 3 1
A getTemplateFileByType() 0 3 1
A set_theme() 0 4 1
A hasTemplate() 0 3 1
A exists() 0 3 1
A dontRewriteHashlinks() 0 3 1
A setParser() 0 3 1
A flush() 0 4 1
A setPartialCacheStore() 0 3 1
A set_themes() 0 3 1
A setRewriteHashLinksDefault() 0 3 1
A templates() 0 3 1
B process() 0 80 10
A __construct() 0 20 4
A getSubtemplateFor() 0 26 4
A getParser() 0 6 2
A flush_cacheblock_cache() 0 8 3
A includeGeneratedTemplate() 0 21 5
A add_themes() 0 6 1
A topLevel() 0 6 2
A getPartialCacheStore() 0 7 2
A flush_template_cache() 0 10 5
A getRewriteHashLinksDefault() 0 7 2
A get_themes() 0 23 5
B get_templates_by_class() 0 29 8
A fromString() 0 7 2
A getRewriteHashLinks() 0 6 2
A setTemplate() 0 5 1
A parseTemplateContent() 0 6 2
A get_base_tag() 0 9 2
A setTemplateFile() 0 6 3
A execute_template() 0 21 3
A execute_string() 0 16 3

How to fix   Complexity   

Complex Class

Complex classes like SSViewer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SSViewer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\View;
4
5
use SilverStripe\Core\Config\Config;
6
use SilverStripe\Core\Config\Configurable;
7
use SilverStripe\Core\ClassInfo;
8
use Psr\SimpleCache\CacheInterface;
9
use SilverStripe\Core\Convert;
10
use SilverStripe\Core\Flushable;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\Core\Injector\Injectable;
13
use SilverStripe\Control\Director;
14
use SilverStripe\Dev\Deprecation;
15
use SilverStripe\ORM\FieldType\DBField;
16
use SilverStripe\ORM\FieldType\DBHTMLText;
17
use SilverStripe\Security\Permission;
18
use InvalidArgumentException;
19
20
/**
21
 * Parses a template file with an *.ss file extension.
22
 *
23
 * In addition to a full template in the templates/ folder, a template in
24
 * templates/Content or templates/Layout will be rendered into $Content and
25
 * $Layout, respectively.
26
 *
27
 * A single template can be parsed by multiple nested {@link SSViewer} instances
28
 * through $Layout/$Content placeholders, as well as <% include MyTemplateFile %> template commands.
29
 *
30
 * <b>Themes</b>
31
 *
32
 * See http://doc.silverstripe.org/themes and http://doc.silverstripe.org/themes:developing
33
 *
34
 * <b>Caching</b>
35
 *
36
 * Compiled templates are cached via {@link Cache}, usually on the filesystem.
37
 * If you put ?flush=1 on your URL, it will force the template to be recompiled.
38
 *
39
 * @see http://doc.silverstripe.org/themes
40
 * @see http://doc.silverstripe.org/themes:developing
41
 */
42
class SSViewer implements Flushable
43
{
44
    use Configurable;
45
    use Injectable;
46
47
    /**
48
     * Identifier for the default theme
49
     */
50
    const DEFAULT_THEME = '$default';
51
52
    /**
53
     * Identifier for the public theme
54
     */
55
    const PUBLIC_THEME = '$public';
56
57
    /**
58
     * A list (highest priority first) of themes to use
59
     * Only used when {@link $theme_enabled} is set to TRUE.
60
     *
61
     * @config
62
     * @var string
63
     */
64
    private static $themes = [];
0 ignored issues
show
introduced by
The private property $themes is not used, and could be removed.
Loading history...
65
66
    /**
67
     * Overridden value of $themes config
68
     *
69
     * @var array
70
     */
71
    protected static $current_themes = null;
72
73
    /**
74
     * The used "theme", which usually consists of templates, images and stylesheets.
75
     * Only used when {@link $theme_enabled} is set to TRUE, and $themes is empty
76
     *
77
     * @deprecated 4.0.0:5.0.0
78
     * @config
79
     * @var string
80
     */
81
    private static $theme = null;
0 ignored issues
show
introduced by
The private property $theme is not used, and could be removed.
Loading history...
82
83
    /**
84
     * Use the theme. Set to FALSE in order to disable themes,
85
     * which can be useful for scenarios where theme overrides are temporarily undesired,
86
     * such as an administrative interface separate from the website theme.
87
     * It retains the theme settings to be re-enabled, for example when a website content
88
     * needs to be rendered from within this administrative interface.
89
     *
90
     * @config
91
     * @var bool
92
     */
93
    private static $theme_enabled = true;
0 ignored issues
show
introduced by
The private property $theme_enabled is not used, and could be removed.
Loading history...
94
95
    /**
96
     * Default prepended cache key for partial caching
97
     *
98
     * @config
99
     * @var string
100
     */
101
    private static $global_key = '$CurrentReadingMode, $CurrentUser.ID';
0 ignored issues
show
introduced by
The private property $global_key is not used, and could be removed.
Loading history...
102
103
    /**
104
     * @config
105
     * @var bool
106
     */
107
    private static $source_file_comments = false;
0 ignored issues
show
introduced by
The private property $source_file_comments is not used, and could be removed.
Loading history...
108
109
    /**
110
     * Set if hash links should be rewritten
111
     *
112
     * @config
113
     * @var bool
114
     */
115
    private static $rewrite_hash_links = true;
0 ignored issues
show
introduced by
The private property $rewrite_hash_links is not used, and could be removed.
Loading history...
116
117
    /**
118
     * Overridden value of rewrite_hash_links config
119
     *
120
     * @var bool
121
     */
122
    protected static $current_rewrite_hash_links = null;
123
124
    /**
125
     * Instance variable to disable rewrite_hash_links (overrides global default)
126
     * Leave null to use global state.
127
     *
128
     * @var bool|null
129
     */
130
    protected $rewriteHashlinks = null;
131
132
    /**
133
     * @internal
134
     * @ignore
135
     */
136
    private static $template_cache_flushed = false;
137
138
    /**
139
     * @internal
140
     * @ignore
141
     */
142
    private static $cacheblock_cache_flushed = false;
143
144
    /**
145
     * List of items being processed
146
     *
147
     * @var array
148
     */
149
    protected static $topLevel = [];
150
151
    /**
152
     * List of templates to select from
153
     *
154
     * @var array
155
     */
156
    protected $templates = null;
157
158
    /**
159
     * Absolute path to chosen template file
160
     *
161
     * @var string
162
     */
163
    protected $chosen = null;
164
165
    /**
166
     * Templates to use when looking up 'Layout' or 'Content'
167
     *
168
     * @var array
169
     */
170
    protected $subTemplates = null;
171
172
    /**
173
     * @var bool
174
     */
175
    protected $includeRequirements = true;
176
177
    /**
178
     * @var TemplateParser
179
     */
180
    protected $parser;
181
182
    /**
183
     * @var CacheInterface
184
     */
185
    protected $partialCacheStore = null;
186
187
    /**
188
     * @param string|array $templates If passed as a string with .ss extension, used as the "main" template.
189
     *  If passed as an array, it can be used for template inheritance (first found template "wins").
190
     *  Usually the array values are PHP class names, which directly correlate to template names.
191
     *  <code>
192
     *  array('MySpecificPage', 'MyPage', 'Page')
193
     *  </code>
194
     * @param TemplateParser $parser
195
     */
196
    public function __construct($templates, TemplateParser $parser = null)
197
    {
198
        if ($parser) {
199
            $this->setParser($parser);
200
        }
201
202
        $this->setTemplate($templates);
203
204
        if (!$this->chosen) {
205
            $message = 'None of the following templates could be found: ';
206
            $message .= print_r($templates, true);
207
208
            $themes = self::get_themes();
209
            if (!$themes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $themes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
210
                $message .= ' (no theme in use)';
211
            } else {
212
                $message .= ' in themes "' . print_r($themes, true) . '"';
213
            }
214
215
            user_error($message, E_USER_WARNING);
216
        }
217
    }
218
219
    /**
220
     * Triggered early in the request when someone requests a flush.
221
     */
222
    public static function flush()
223
    {
224
        self::flush_template_cache(true);
225
        self::flush_cacheblock_cache(true);
226
    }
227
228
    /**
229
     * Create a template from a string instead of a .ss file
230
     *
231
     * @param string $content The template content
232
     * @param bool|void $cacheTemplate Whether or not to cache the template from string
233
     * @return SSViewer
234
     */
235
    public static function fromString($content, $cacheTemplate = null)
236
    {
237
        $viewer = SSViewer_FromString::create($content);
238
        if ($cacheTemplate !== null) {
239
            $viewer->setCacheTemplate($cacheTemplate);
240
        }
241
        return $viewer;
242
    }
243
244
    /**
245
     * Assign the list of active themes to apply.
246
     * If default themes should be included add $default as the last entry.
247
     *
248
     * @param array $themes
249
     */
250
    public static function set_themes($themes = [])
251
    {
252
        static::$current_themes = $themes;
253
    }
254
255
    /**
256
     * Add to the list of active themes to apply
257
     *
258
     * @param array $themes
259
     */
260
    public static function add_themes($themes = [])
261
    {
262
        $currentThemes = SSViewer::get_themes();
263
        $finalThemes = array_merge($themes, $currentThemes);
264
        // array_values is used to ensure sequential array keys as array_unique can leave gaps
265
        static::set_themes(array_values(array_unique($finalThemes)));
266
    }
267
268
    /**
269
     * Get the list of active themes
270
     *
271
     * @return array
272
     */
273
    public static function get_themes()
274
    {
275
        $default = [self::PUBLIC_THEME, self::DEFAULT_THEME];
276
277
        if (!SSViewer::config()->uninherited('theme_enabled')) {
278
            return $default;
279
        }
280
281
        // Explicit list is assigned
282
        $themes = static::$current_themes;
283
        if (!isset($themes)) {
284
            $themes = SSViewer::config()->uninherited('themes');
285
        }
286
        if ($themes) {
287
            return $themes;
288
        }
289
290
        // Support legacy behaviour
291
        if ($theme = SSViewer::config()->uninherited('theme')) {
292
            return [self::PUBLIC_THEME, $theme, self::DEFAULT_THEME];
293
        }
294
295
        return $default;
296
    }
297
298
    /**
299
     * @deprecated 4.0.0:5.0.0 Use the "SSViewer#set_themes" instead
300
     * @param string $theme The "base theme" name (without underscores).
301
     */
302
    public static function set_theme($theme)
303
    {
304
        Deprecation::notice('4.0', 'Use the "SSViewer#set_themes" instead');
305
        self::set_themes([$theme, self::DEFAULT_THEME]);
306
    }
307
308
    /**
309
     * Traverses the given the given class context looking for candidate template names
310
     * which match each item in the class hierarchy. The resulting list of template candidates
311
     * may or may not exist, but you can invoke {@see SSViewer::chooseTemplate} on any list
312
     * to determine the best candidate based on the current themes.
313
     *
314
     * @param string|object $classOrObject Valid class name, or object
315
     * @param string $suffix
316
     * @param string $baseClass Class to halt ancestry search at
317
     * @return array
318
     */
319
    public static function get_templates_by_class($classOrObject, $suffix = '', $baseClass = null)
320
    {
321
        // Figure out the class name from the supplied context.
322
        if (!is_object($classOrObject) && !(
323
            is_string($classOrObject) && class_exists($classOrObject)
324
        )) {
325
            throw new InvalidArgumentException(
326
                'SSViewer::get_templates_by_class() expects a valid class name as its first parameter.'
327
            );
328
        }
329
330
        $templates = [];
331
        $classes = array_reverse(ClassInfo::ancestry($classOrObject));
332
        foreach ($classes as $class) {
333
            $template = $class . $suffix;
334
            $templates[] = $template;
335
            $templates[] = ['type' => 'Includes', $template];
336
337
            // If the class is "PageController" (PSR-2 compatibility) or "Page_Controller" (legacy), look for Page.ss
338
            if (preg_match('/^(?<name>.+[^\\\\])_?Controller$/iU', $class, $matches)) {
339
                $templates[] = $matches['name'] . $suffix;
340
            }
341
342
            if ($baseClass && $class == $baseClass) {
343
                break;
344
            }
345
        }
346
347
        return $templates;
348
    }
349
350
    /**
351
     * Get the current item being processed
352
     *
353
     * @return ViewableData
354
     */
355
    public static function topLevel()
356
    {
357
        if (SSViewer::$topLevel) {
0 ignored issues
show
Bug Best Practice introduced by
The expression SilverStripe\View\SSViewer::topLevel of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
358
            return SSViewer::$topLevel[sizeof(SSViewer::$topLevel)-1];
359
        }
360
        return null;
361
    }
362
363
    /**
364
     * Check if rewrite hash links are enabled on this instance
365
     *
366
     * @return bool
367
     */
368
    public function getRewriteHashLinks()
369
    {
370
        if (isset($this->rewriteHashlinks)) {
371
            return $this->rewriteHashlinks;
372
        }
373
        return static::getRewriteHashLinksDefault();
374
    }
375
376
    /**
377
     * Set if hash links are rewritten for this instance
378
     *
379
     * @param bool $rewrite
380
     * @return $this
381
     */
382
    public function setRewriteHashLinks($rewrite)
383
    {
384
        $this->rewriteHashlinks = $rewrite;
385
        return $this;
386
    }
387
388
    /**
389
     * Get default value for rewrite hash links for all modules
390
     *
391
     * @return bool
392
     */
393
    public static function getRewriteHashLinksDefault()
394
    {
395
        // Check if config overridden
396
        if (isset(static::$current_rewrite_hash_links)) {
397
            return static::$current_rewrite_hash_links;
398
        }
399
        return Config::inst()->get(static::class, 'rewrite_hash_links');
400
    }
401
402
    /**
403
     * Set default rewrite hash links
404
     *
405
     * @param bool $rewrite
406
     */
407
    public static function setRewriteHashLinksDefault($rewrite)
408
    {
409
        static::$current_rewrite_hash_links = $rewrite;
410
    }
411
412
    /**
413
     * @param string|array $templates
414
     */
415
    public function setTemplate($templates)
416
    {
417
        $this->templates = $templates;
0 ignored issues
show
Documentation Bug introduced by
It seems like $templates can also be of type string. However, the property $templates is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
418
        $this->chosen = $this->chooseTemplate($templates);
419
        $this->subTemplates = [];
420
    }
421
422
    /**
423
     * Find the template to use for a given list
424
     *
425
     * @param array|string $templates
426
     * @return string
427
     */
428
    public static function chooseTemplate($templates)
429
    {
430
        return ThemeResourceLoader::inst()->findTemplate($templates, self::get_themes());
431
    }
432
433
    /**
434
     * Set the template parser that will be used in template generation
435
     *
436
     * @param TemplateParser $parser
437
     */
438
    public function setParser(TemplateParser $parser)
439
    {
440
        $this->parser = $parser;
441
    }
442
443
    /**
444
     * Returns the parser that is set for template generation
445
     *
446
     * @return TemplateParser
447
     */
448
    public function getParser()
449
    {
450
        if (!$this->parser) {
451
            $this->setParser(Injector::inst()->get('SilverStripe\\View\\SSTemplateParser'));
452
        }
453
        return $this->parser;
454
    }
455
456
    /**
457
     * Returns true if at least one of the listed templates exists.
458
     *
459
     * @param array|string $templates
460
     *
461
     * @return bool
462
     */
463
    public static function hasTemplate($templates)
464
    {
465
        return (bool)ThemeResourceLoader::inst()->findTemplate($templates, self::get_themes());
466
    }
467
468
    /**
469
     * Call this to disable rewriting of <a href="#xxx"> links.  This is useful in Ajax applications.
470
     * It returns the SSViewer objects, so that you can call new SSViewer("X")->dontRewriteHashlinks()->process();
471
     *
472
     * @return $this
473
     */
474
    public function dontRewriteHashlinks()
475
    {
476
        return $this->setRewriteHashLinks(false);
477
    }
478
479
    /**
480
     * @return string
481
     */
482
    public function exists()
483
    {
484
        return $this->chosen;
485
    }
486
487
    /**
488
     * @param string $identifier A template name without '.ss' extension or path
489
     * @param string $type The template type, either "main", "Includes" or "Layout"
490
     * @return string Full system path to a template file
491
     */
492
    public static function getTemplateFileByType($identifier, $type = null)
493
    {
494
        return ThemeResourceLoader::inst()->findTemplate(['type' => $type, $identifier], self::get_themes());
495
    }
496
497
    /**
498
     * Clears all parsed template files in the cache folder.
499
     *
500
     * Can only be called once per request (there may be multiple SSViewer instances).
501
     *
502
     * @param bool $force Set this to true to force a re-flush. If left to false, flushing
503
     * may only be performed once a request.
504
     */
505
    public static function flush_template_cache($force = false)
506
    {
507
        if (!self::$template_cache_flushed || $force) {
508
            $dir = dir(TEMP_PATH);
509
            while (false !== ($file = $dir->read())) {
510
                if (strstr($file, '.cache')) {
511
                    unlink(TEMP_PATH . DIRECTORY_SEPARATOR . $file);
512
                }
513
            }
514
            self::$template_cache_flushed = true;
515
        }
516
    }
517
518
    /**
519
     * Clears all partial cache blocks.
520
     *
521
     * Can only be called once per request (there may be multiple SSViewer instances).
522
     *
523
     * @param bool $force Set this to true to force a re-flush. If left to false, flushing
524
     * may only be performed once a request.
525
     */
526
    public static function flush_cacheblock_cache($force = false)
527
    {
528
        if (!self::$cacheblock_cache_flushed || $force) {
529
            $cache = Injector::inst()->get(CacheInterface::class . '.cacheblock');
530
            $cache->clear();
531
532
533
            self::$cacheblock_cache_flushed = true;
534
        }
535
    }
536
537
    /**
538
     * Set the cache object to use when storing / retrieving partial cache blocks.
539
     *
540
     * @param CacheInterface $cache
541
     */
542
    public function setPartialCacheStore($cache)
543
    {
544
        $this->partialCacheStore = $cache;
545
    }
546
547
    /**
548
     * Get the cache object to use when storing / retrieving partial cache blocks.
549
     *
550
     * @return CacheInterface
551
     */
552
    public function getPartialCacheStore()
553
    {
554
        if ($this->partialCacheStore) {
555
            return $this->partialCacheStore;
556
        }
557
558
        return Injector::inst()->get(CacheInterface::class . '.cacheblock');
559
    }
560
561
    /**
562
     * Flag whether to include the requirements in this response.
563
     *
564
     * @param bool
565
     */
566
    public function includeRequirements($incl = true)
567
    {
568
        $this->includeRequirements = $incl;
569
    }
570
571
    /**
572
     * An internal utility function to set up variables in preparation for including a compiled
573
     * template, then do the include
574
     *
575
     * Effectively this is the common code that both SSViewer#process and SSViewer_FromString#process call
576
     *
577
     * @param string $cacheFile The path to the file that contains the template compiled to PHP
578
     * @param ViewableData $item The item to use as the root scope for the template
579
     * @param array $overlay Any variables to layer on top of the scope
580
     * @param array $underlay Any variables to layer underneath the scope
581
     * @param ViewableData $inheritedScope The current scope of a parent template including a sub-template
582
     * @return string The result of executing the template
583
     */
584
    protected function includeGeneratedTemplate($cacheFile, $item, $overlay, $underlay, $inheritedScope = null)
585
    {
586
        if (isset($_GET['showtemplate']) && $_GET['showtemplate'] && Permission::check('ADMIN')) {
587
            $lines = file($cacheFile);
588
            echo "<h2>Template: $cacheFile</h2>";
589
            echo "<pre>";
590
            foreach ($lines as $num => $line) {
591
                echo str_pad($num+1, 5) . htmlentities($line, ENT_COMPAT, 'UTF-8');
592
            }
593
            echo "</pre>";
594
        }
595
596
        $cache = $this->getPartialCacheStore();
597
        $scope = new SSViewer_DataPresenter($item, $overlay, $underlay, $inheritedScope);
598
        $val = '';
599
600
        // Placeholder for values exposed to $cacheFile
601
        [$cache, $scope, $val];
602
        include($cacheFile);
603
604
        return $val;
605
    }
606
607
    /**
608
     * The process() method handles the "meat" of the template processing.
609
     *
610
     * It takes care of caching the output (via {@link Cache}), as well as
611
     * replacing the special "$Content" and "$Layout" placeholders with their
612
     * respective subtemplates.
613
     *
614
     * The method injects extra HTML in the header via {@link Requirements::includeInHTML()}.
615
     *
616
     * Note: You can call this method indirectly by {@link ViewableData->renderWith()}.
617
     *
618
     * @param ViewableData $item
619
     * @param array|null $arguments Arguments to an included template
620
     * @param ViewableData $inheritedScope The current scope of a parent template including a sub-template
621
     * @return DBHTMLText Parsed template output.
622
     */
623
    public function process($item, $arguments = null, $inheritedScope = null)
624
    {
625
        // Set hashlinks and temporarily modify global state
626
        $rewrite = $this->getRewriteHashLinks();
627
        $origRewriteDefault = static::getRewriteHashLinksDefault();
628
        static::setRewriteHashLinksDefault($rewrite);
629
630
        SSViewer::$topLevel[] = $item;
631
632
        $template = $this->chosen;
633
634
        $cacheFile = TEMP_PATH . DIRECTORY_SEPARATOR . '.cache'
635
            . str_replace(['\\','/',':'], '.', Director::makeRelative(realpath($template)));
636
        $lastEdited = filemtime($template);
637
638
        if (!file_exists($cacheFile) || filemtime($cacheFile) < $lastEdited) {
639
            $content = file_get_contents($template);
640
            $content = $this->parseTemplateContent($content, $template);
641
642
            $fh = fopen($cacheFile, 'w');
643
            fwrite($fh, $content);
0 ignored issues
show
Bug introduced by
It seems like $fh can also be of type false; however, parameter $handle of fwrite() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

643
            fwrite(/** @scrutinizer ignore-type */ $fh, $content);
Loading history...
644
            fclose($fh);
0 ignored issues
show
Bug introduced by
It seems like $fh can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

644
            fclose(/** @scrutinizer ignore-type */ $fh);
Loading history...
645
        }
646
647
        $underlay = ['I18NNamespace' => basename($template)];
648
649
        // Makes the rendered sub-templates available on the parent item,
650
        // through $Content and $Layout placeholders.
651
        foreach (['Content', 'Layout'] as $subtemplate) {
652
            // Detect sub-template to use
653
            $sub = $this->getSubtemplateFor($subtemplate);
654
            if (!$sub) {
655
                continue;
656
            }
657
658
            // Create lazy-evaluated underlay for this subtemplate
659
            $underlay[$subtemplate] = function () use ($item, $arguments, $sub) {
660
                $subtemplateViewer = clone $this;
661
                // Disable requirements - this will be handled by the parent template
662
                $subtemplateViewer->includeRequirements(false);
663
                // Select the right template
664
                $subtemplateViewer->setTemplate($sub);
665
666
                // Render if available
667
                if ($subtemplateViewer->exists()) {
668
                    return $subtemplateViewer->process($item, $arguments);
669
                }
670
                return null;
671
            };
672
        }
673
674
        $output = $this->includeGeneratedTemplate($cacheFile, $item, $arguments, $underlay, $inheritedScope);
675
676
        if ($this->includeRequirements) {
677
            $output = Requirements::includeInHTML($output);
678
        }
679
680
        array_pop(SSViewer::$topLevel);
681
682
        // If we have our crazy base tag, then fix # links referencing the current page.
683
        if ($rewrite) {
684
            if (strpos($output, '<base') !== false) {
685
                if ($rewrite === 'php') {
0 ignored issues
show
introduced by
The condition $rewrite === 'php' is always false.
Loading history...
686
                    $thisURLRelativeToBase = <<<PHP
687
<?php echo \\SilverStripe\\Core\\Convert::raw2att(preg_replace("/^(\\\\/)+/", "/", \$_SERVER['REQUEST_URI'])); ?>
688
PHP;
689
                } else {
690
                    $thisURLRelativeToBase = Convert::raw2att(preg_replace("/^(\\/)+/", "/", $_SERVER['REQUEST_URI']));
691
                }
692
693
                $output = preg_replace('/(<a[^>]+href *= *)"#/i', '\\1"' . $thisURLRelativeToBase . '#', $output);
0 ignored issues
show
Bug introduced by
Are you sure $thisURLRelativeToBase of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

693
                $output = preg_replace('/(<a[^>]+href *= *)"#/i', '\\1"' . /** @scrutinizer ignore-type */ $thisURLRelativeToBase . '#', $output);
Loading history...
694
            }
695
        }
696
697
        /** @var DBHTMLText $html */
698
        $html = DBField::create_field('HTMLFragment', $output);
699
700
        // Reset global state
701
        static::setRewriteHashLinksDefault($origRewriteDefault);
702
        return $html;
703
    }
704
705
    /**
706
     * Get the appropriate template to use for the named sub-template, or null if none are appropriate
707
     *
708
     * @param string $subtemplate Sub-template to use
709
     *
710
     * @return array|null
711
     */
712
    protected function getSubtemplateFor($subtemplate)
713
    {
714
        // Get explicit subtemplate name
715
        if (isset($this->subTemplates[$subtemplate])) {
716
            return $this->subTemplates[$subtemplate];
717
        }
718
719
        // Don't apply sub-templates if type is already specified (e.g. 'Includes')
720
        if (isset($this->templates['type'])) {
721
            return null;
722
        }
723
724
        // Filter out any other typed templates as we can only add, not change type
725
        $templates = array_filter(
726
            (array)$this->templates,
727
            function ($template) {
728
                return !isset($template['type']);
729
            }
730
        );
731
        if (empty($templates)) {
732
            return null;
733
        }
734
735
        // Set type to subtemplate
736
        $templates['type'] = $subtemplate;
737
        return $templates;
738
    }
739
740
    /**
741
     * Execute the given template, passing it the given data.
742
     * Used by the <% include %> template tag to process templates.
743
     *
744
     * @param string $template Template name
745
     * @param mixed $data Data context
746
     * @param array $arguments Additional arguments
747
     * @param Object $scope
748
     * @param bool $globalRequirements
749
     *
750
     * @return string Evaluated result
751
     */
752
    public static function execute_template(
753
        $template,
754
        $data,
755
        $arguments = null,
756
        $scope = null,
757
        $globalRequirements = false
758
    ) {
759
        $v = SSViewer::create($template);
760
761
        if ($globalRequirements) {
762
            $v->includeRequirements(false);
763
        } else {
764
            //nest a requirements backend for our template rendering
765
            $origBackend = Requirements::backend();
766
            Requirements::set_backend(Requirements_Backend::create());
767
        }
768
        try {
769
            return $v->process($data, $arguments, $scope);
770
        } finally {
771
            if (!$globalRequirements) {
772
                Requirements::set_backend($origBackend);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $origBackend does not seem to be defined for all execution paths leading up to this point.
Loading history...
773
            }
774
        }
775
    }
776
777
    /**
778
     * Execute the evaluated string, passing it the given data.
779
     * Used by partial caching to evaluate custom cache keys expressed using
780
     * template expressions
781
     *
782
     * @param string $content Input string
783
     * @param mixed $data Data context
784
     * @param array $arguments Additional arguments
785
     * @param bool $globalRequirements
786
     *
787
     * @return string Evaluated result
788
     */
789
    public static function execute_string($content, $data, $arguments = null, $globalRequirements = false)
790
    {
791
        $v = SSViewer::fromString($content);
792
793
        if ($globalRequirements) {
794
            $v->includeRequirements(false);
795
        } else {
796
            //nest a requirements backend for our template rendering
797
            $origBackend = Requirements::backend();
798
            Requirements::set_backend(Requirements_Backend::create());
799
        }
800
        try {
801
            return $v->process($data, $arguments);
802
        } finally {
803
            if (!$globalRequirements) {
804
                Requirements::set_backend($origBackend);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $origBackend does not seem to be defined for all execution paths leading up to this point.
Loading history...
805
            }
806
        }
807
    }
808
809
    /**
810
     * Parse given template contents
811
     *
812
     * @param string $content The template contents
813
     * @param string $template The template file name
814
     * @return string
815
     */
816
    public function parseTemplateContent($content, $template = "")
817
    {
818
        return $this->getParser()->compileString(
819
            $content,
820
            $template,
821
            Director::isDev() && SSViewer::config()->uninherited('source_file_comments')
822
        );
823
    }
824
825
    /**
826
     * Returns the filenames of the template that will be rendered.  It is a map that may contain
827
     * 'Content' & 'Layout', and will have to contain 'main'
828
     *
829
     * @return array
830
     */
831
    public function templates()
832
    {
833
        return array_merge(['main' => $this->chosen], $this->subTemplates);
834
    }
835
836
    /**
837
     * @param string $type "Layout" or "main"
838
     * @param string $file Full system path to the template file
839
     */
840
    public function setTemplateFile($type, $file)
841
    {
842
        if (!$type || $type == 'main') {
843
            $this->chosen = $file;
844
        } else {
845
            $this->subTemplates[$type] = $file;
846
        }
847
    }
848
849
    /**
850
     * Return an appropriate base tag for the given template.
851
     * It will be closed on an XHTML document, and unclosed on an HTML document.
852
     *
853
     * @param string $contentGeneratedSoFar The content of the template generated so far; it should contain
854
     * the DOCTYPE declaration.
855
     * @return string
856
     */
857
    public static function get_base_tag($contentGeneratedSoFar)
858
    {
859
        $base = Director::absoluteBaseURL();
860
861
        // Is the document XHTML?
862
        if (preg_match('/<!DOCTYPE[^>]+xhtml/i', $contentGeneratedSoFar)) {
863
            return "<base href=\"$base\" />";
864
        } else {
865
            return "<base href=\"$base\"><!--[if lte IE 6]></base><![endif]-->";
866
        }
867
    }
868
}
869