• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TYPO3-Headless / headless / 29398229494

15 Jul 2026 07:42AM UTC coverage: 69.663% (-5.8%) from 75.459%
29398229494

Pull #893

github

web-flow
Merge ab993c965 into 79b7c5472
Pull Request #893: [TASK] Reintroduce missing features, extension cleanup

360 of 545 new or added lines in 33 files covered. (66.06%)

167 existing lines in 7 files now uncovered.

1364 of 1958 relevant lines covered (69.66%)

7.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.71
/Classes/View/HeadlessPhpView.php
1
<?php
2

3
/*
4
 * This file is part of the "headless" Extension for TYPO3 CMS.
5
 *
6
 * For the full copyright and license information, please read the
7
 * LICENSE.md file that was distributed with this source code.
8
 */
9

10
declare(strict_types=1);
11

12
namespace FriendsOfTYPO3\Headless\View;
13

14
use RuntimeException;
15
use Throwable;
16
use TYPO3\CMS\Core\Utility\GeneralUtility;
17
use TYPO3\CMS\Core\View\ViewFactoryData;
18
use TYPO3\CMS\Core\View\ViewInterface;
19

20
use function array_replace;
21
use function array_reverse;
22
use function extract;
23
use function is_file;
24
use function ltrim;
25
use function ob_end_clean;
26
use function ob_get_clean;
27
use function ob_get_level;
28
use function ob_start;
29
use function preg_match;
30
use function realpath;
31
use function rtrim;
32
use function str_starts_with;
33

34
final class HeadlessPhpView implements ViewInterface
35
{
36
    /** @var array<string, mixed> */
37
    private array $variables = [];
38

39
    /** @var list<string>|null */
40
    private ?array $resolvedRoots = null;
41

42
    public function __construct(private readonly ViewFactoryData $data) {}
43

44
    public function assign(string $key, mixed $value): self
45
    {
46
        $this->variables[$key] = $value;
3✔
47
        return $this;
3✔
48
    }
49

50
    /**
51
     * @param array<string, mixed> $values
52
     */
53
    public function assignMultiple(array $values): self
54
    {
55
        $this->variables = array_replace($this->variables, $values);
1✔
56
        return $this;
1✔
57
    }
58

59
    public function render(string $templateFileName = ''): string
60
    {
61
        $templateFile = $this->resolvePhpTemplate($templateFileName);
18✔
62
        if ($templateFile === null) {
18✔
63
            throw new RuntimeException(
11✔
64
                'Headless PHP template "' . $templateFileName . '" could not be resolved.',
11✔
65
                1747300000
11✔
66
            );
11✔
67
        }
68

69
        try {
70
            extract($this->variables, EXTR_SKIP);
7✔
71
            ob_start();
7✔
72
            include $templateFile;
7✔
73
            return (string)ob_get_clean();
5✔
74
        } catch (Throwable $e) {
2✔
75
            if (ob_get_level() > 0) {
2✔
76
                ob_end_clean();
2✔
77
            }
78
            throw $e;
2✔
79
        }
80
    }
81

82
    private function resolvePhpTemplate(string $name): ?string
83
    {
84
        if ($name === '') {
18✔
85
            return $this->resolveDirectFile();
1✔
86
        }
87

88
        if (!$this->isSafeTemplateName($name)) {
17✔
89
            return null;
8✔
90
        }
91

92
        $resolvedRoots = $this->resolvedTemplateRoots();
9✔
93
        if ($resolvedRoots === []) {
9✔
94
            return null;
1✔
95
        }
96

97
        $relative = ltrim($name, '/') . '.php';
8✔
98

99
        foreach (array_reverse($resolvedRoots) as $root) {
8✔
100
            $candidate = $root . '/' . $relative;
8✔
101
            if (!is_file($candidate)) {
8✔
102
                continue;
2✔
103
            }
104
            $real = realpath($candidate);
6✔
105
            if ($real === false) {
6✔
NEW
106
                continue;
×
107
            }
108
            if ($real === $root || str_starts_with($real, $root . '/')) {
6✔
109
                return $real;
6✔
110
            }
111
        }
112

113
        return null;
2✔
114
    }
115

116
    private function resolveDirectFile(): ?string
117
    {
118
        $direct = $this->data->templatePathAndFilename;
1✔
119
        if ($direct === null || $direct === '') {
1✔
NEW
120
            return null;
×
121
        }
122
        // getFileAbsFileName resolves EXT:, asserts allowed roots and runs validPathStr.
123
        $absolute = GeneralUtility::getFileAbsFileName($direct);
1✔
124
        if ($absolute === '' || !is_file($absolute)) {
1✔
NEW
125
            return null;
×
126
        }
127
        $real = realpath($absolute);
1✔
128
        return $real === false ? null : $real;
1✔
129
    }
130

131
    private function isSafeTemplateName(string $name): bool
132
    {
133
        if ($name[0] === '/'
17✔
134
            || preg_match('#^[a-zA-Z][a-zA-Z0-9+.\-]*:#', $name) === 1) {
17✔
135
            return false;
3✔
136
        }
137
        return GeneralUtility::validPathStr($name);
14✔
138
    }
139

140
    /**
141
     * @return list<string> canonical absolute roots without trailing slash
142
     */
143
    private function resolvedTemplateRoots(): array
144
    {
145
        if ($this->resolvedRoots !== null) {
9✔
NEW
146
            return $this->resolvedRoots;
×
147
        }
148
        $roots = [];
9✔
149
        foreach ($this->data->templateRootPaths ?? [] as $root) {
9✔
150
            if (!is_string($root) || $root === '') {
8✔
NEW
151
                continue;
×
152
            }
153
            $absolute = GeneralUtility::getFileAbsFileName($root);
8✔
154
            if ($absolute === '') {
8✔
NEW
155
                continue;
×
156
            }
157
            $real = realpath($absolute);
8✔
158
            if ($real === false) {
8✔
NEW
159
                continue;
×
160
            }
161
            $roots[] = rtrim($real, '/');
8✔
162
        }
163
        return $this->resolvedRoots = $roots;
9✔
164
    }
165
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc