/
Bootstrap.php
259 lines (238 loc) · 10.6 KB
/
Bootstrap.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Core;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Extbase\Mvc\Dispatcher;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Mvc\Web\RequestBuilder;
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
use TYPO3\CMS\Extbase\Service\CacheService;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* Creates a request and dispatches it to the controller which was specified
* by TS Setup and returns the content.
*
* This class is the main entry point for extbase extensions.
*
* @todo: Please note that this class will become internal in TYPO3 v13.0
*/
class Bootstrap
{
/**
* Set by UserContentObject (USER) via setContentObjectRenderer() in frontend
*/
protected ?ContentObjectRenderer $cObj = null;
protected ContainerInterface $container;
protected ConfigurationManagerInterface $configurationManager;
protected PersistenceManagerInterface $persistenceManager;
protected CacheService $cacheService;
protected Dispatcher $dispatcher;
protected RequestBuilder $extbaseRequestBuilder;
public function __construct(
ContainerInterface $container,
ConfigurationManagerInterface $configurationManager,
PersistenceManagerInterface $persistenceManager,
CacheService $cacheService,
Dispatcher $dispatcher,
RequestBuilder $extbaseRequestBuilder
) {
$this->container = $container;
$this->configurationManager = $configurationManager;
$this->persistenceManager = $persistenceManager;
$this->cacheService = $cacheService;
$this->dispatcher = $dispatcher;
$this->extbaseRequestBuilder = $extbaseRequestBuilder;
}
/**
* Called for frontend plugins from UserContentObject via ContentObjectRenderer->callUserFunction().
*/
public function setContentObjectRenderer(ContentObjectRenderer $cObj)
{
$this->cObj = $cObj;
}
/**
* Explicitly initializes all necessary Extbase objects by invoking the various initialize* methods.
*
* Usually this method is only called from unit tests or other applications which need a more fine grained control over
* the initialization and request handling process. Most other applications just call the run() method.
*
* @param array $configuration The TS configuration array
* @throws \RuntimeException
* @see run()
*/
public function initialize(array $configuration): void
{
if (!Environment::isCli()) {
if (!isset($configuration['extensionName']) || $configuration['extensionName'] === '') {
throw new \RuntimeException('Invalid configuration: "extensionName" is not set', 1290623020);
}
if (!isset($configuration['pluginName']) || $configuration['pluginName'] === '') {
throw new \RuntimeException('Invalid configuration: "pluginName" is not set', 1290623027);
}
}
$this->initializeConfiguration($configuration);
}
/**
* Initializes the Object framework.
*
* @see initialize()
* @internal
*/
public function initializeConfiguration(array $configuration): void
{
$this->cObj ??= $this->container->get(ContentObjectRenderer::class);
$this->configurationManager->setContentObject($this->cObj);
$this->configurationManager->setConfiguration($configuration);
// todo: Shouldn't the configuration manager object – which is a singleton – be stateless?
// todo: At this point we give the configuration manager a state, while we could directly pass the
// todo: configuration (i.e. controllerName, actionName and such), directly to the request
// todo: handler, which then creates stateful request objects.
// todo: Once this has changed, \TYPO3\CMS\Extbase\Mvc\Web\RequestBuilder::loadDefaultValues does not need
// todo: to fetch this configuration from the configuration manager.
}
/**
* Runs the Extbase Framework by resolving an appropriate Request Handler and passing control to it.
* If the Framework is not initialized yet, it will be initialized.
*
* This is usually used in Frontend plugins.
* This method will be marked as internal in the future, use EXTBASEPLUGIN in TypoScript to execute a Extbase plugin
* instead.
*
* @param string $content The content. Not used
* @param array $configuration The TS configuration array
* @param ServerRequestInterface $request the incoming server request
* @return string $content The processed content
*/
public function run(string $content, array $configuration, ServerRequestInterface $request): string
{
$this->initialize($configuration);
return $this->handleFrontendRequest($request);
}
/**
* Used for any Extbase Plugin in the Frontend, be sure to run $this->initialize() before.
*
* @internal
*/
public function handleFrontendRequest(ServerRequestInterface $request): string
{
$extbaseRequest = $this->extbaseRequestBuilder->build($request);
if (!$this->isExtbaseRequestCacheable($extbaseRequest)) {
if ($this->cObj->getUserObjectType() === ContentObjectRenderer::OBJECTTYPE_USER) {
// ContentObjectRenderer::convertToUserIntObject() will recreate the object,
// so we have to stop the request here before the action is actually called
$this->cObj->convertToUserIntObject();
return '';
}
}
// Dispatch the extbase request
$response = $this->dispatcher->dispatch($extbaseRequest);
if ($response->getStatusCode() >= 300) {
// Avoid caching the plugin when we issue a redirect or error response
// This means that even when an action is configured as cachable
// we avoid the plugin to be cached, but keep the page cache untouched
if ($this->cObj->getUserObjectType() === ContentObjectRenderer::OBJECTTYPE_USER) {
$this->cObj->convertToUserIntObject();
}
}
// Usually coming from an error action, ensure all caches are cleared
if ($response->getStatusCode() === 400) {
$this->clearCacheOnError();
}
// In case TSFE is available and this is a json response, we have to let TSFE know we have a specific Content-Type
if (($typoScriptFrontendController = ($GLOBALS['TSFE'] ?? null)) instanceof TypoScriptFrontendController
&& $response->hasHeader('Content-Type')
) {
$typoScriptFrontendController->setContentType($response->getHeaderLine('Content-Type'));
// Do not send the header directly (see below)
$response = $response->withoutHeader('Content-Type');
}
if (headers_sent() === false) {
foreach ($response->getHeaders() as $name => $values) {
foreach ($values as $value) {
header(sprintf('%s: %s', $name, $value));
}
}
// Set status code from extbase response
// @todo: Remove when ContentObjectRenderer is response aware
if ($response->getStatusCode() >= 300) {
header('HTTP/' . $response->getProtocolVersion() . ' ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase());
}
}
$body = $response->getBody();
$body->rewind();
$content = $body->getContents();
$this->resetSingletons();
$this->cacheService->clearCachesOfRegisteredPageIds();
return $content;
}
/**
* Entrypoint for backend modules, handling PSR-7 requests/responses.
*
* Creates an Extbase Request, dispatches it and then returns the Response
*
* @param ServerRequestInterface $request
* @internal
*/
public function handleBackendRequest(ServerRequestInterface $request): ResponseInterface
{
// build the configuration from the module, included in the current request
$module = $request->getAttribute('module');
$configuration = [
'extensionName' => $module?->getExtensionName(),
'pluginName' => $module?->getIdentifier(),
];
$this->initialize($configuration);
$extbaseRequest = $this->extbaseRequestBuilder->build($request);
$response = $this->dispatcher->dispatch($extbaseRequest);
$this->resetSingletons();
$this->cacheService->clearCachesOfRegisteredPageIds();
return $response;
}
/**
* Clear cache of current page on error. Needed because we want a re-evaluation of the data.
*/
protected function clearCacheOnError(): void
{
$extbaseSettings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
if (isset($extbaseSettings['persistence']['enableAutomaticCacheClearing']) && $extbaseSettings['persistence']['enableAutomaticCacheClearing'] === '1') {
if (isset($GLOBALS['TSFE'])) {
$this->cacheService->clearPageCache([$GLOBALS['TSFE']->id]);
}
}
}
/**
* Resets global singletons for the next plugin
*/
protected function resetSingletons(): void
{
$this->persistenceManager->persistAll();
}
protected function isExtbaseRequestCacheable(RequestInterface $extbaseRequest): bool
{
$controllerClassName = $extbaseRequest->getControllerObjectName();
$actionName = $extbaseRequest->getControllerActionName();
$frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
$nonCacheableActions = $frameworkConfiguration['controllerConfiguration'][$controllerClassName]['nonCacheableActions'] ?? null;
if (!is_array($nonCacheableActions)) {
return true;
}
return !in_array($actionName, $nonCacheableActions, true);
}
}