[BUGFIX] Incorrect cHash generation may cause 404 on any page
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Page / CacheHashCalculator.php
1 <?php
2 namespace TYPO3\CMS\Frontend\Page;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 /**
18 * Logic for cHash calculation
19 */
20 class CacheHashCalculator implements \TYPO3\CMS\Core\SingletonInterface
21 {
22 /**
23 * @var array Parameters that are relevant for cacheHash calculation. Optional.
24 */
25 protected $cachedParametersWhiteList = [];
26
27 /**
28 * @var array Parameters that are not relevant for cacheHash calculation.
29 */
30 protected $excludedParameters = [];
31
32 /**
33 * @var array Parameters that forces a presence of a valid cacheHash.
34 */
35 protected $requireCacheHashPresenceParameters = [];
36
37 /**
38 * @var array Parameters that need a value to be relevant for cacheHash calculation
39 */
40 protected $excludedParametersIfEmpty = [];
41
42 /**
43 * @var bool Whether to exclude all empty parameters for cacheHash calculation
44 */
45 protected $excludeAllEmptyParameters = false;
46
47 /**
48 * @var bool
49 */
50 protected $includePageId = false;
51
52 /**
53 * Initialise class properties by using the relevant TYPO3 configuration
54 */
55 public function __construct()
56 {
57 $this->setConfiguration($GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']);
58 }
59
60 /**
61 * Calculates the cHash based on the provided parameters
62 *
63 * @param array $params Array of cHash key-value pairs
64 * @return string Hash of all the values
65 */
66 public function calculateCacheHash(array $params)
67 {
68 return !empty($params) ? md5(serialize($params)) : '';
69 }
70
71 /**
72 * Returns the cHash based on provided query parameters and added values from internal call
73 *
74 * @param string $queryString Query-parameters: "&xxx=yyy&zzz=uuu
75 * @return string Hash of all the values
76 */
77 public function generateForParameters($queryString)
78 {
79 $cacheHashParams = $this->getRelevantParameters($queryString);
80 return $this->calculateCacheHash($cacheHashParams);
81 }
82
83 /**
84 * Checks whether a parameter of the given $queryString requires cHash calculation
85 *
86 * @param string $queryString
87 * @return bool
88 */
89 public function doParametersRequireCacheHash($queryString)
90 {
91 if (empty($this->requireCacheHashPresenceParameters)) {
92 return false;
93 }
94 $hasRequiredParameter = false;
95 $parameterNames = array_keys($this->splitQueryStringToArray($queryString));
96 foreach ($parameterNames as $parameterName) {
97 if (in_array($parameterName, $this->requireCacheHashPresenceParameters)) {
98 $hasRequiredParameter = true;
99 }
100 }
101 return $hasRequiredParameter;
102 }
103
104 /**
105 * Splits the input query-parameters into an array with certain parameters filtered out.
106 * Used to create the cHash value
107 *
108 * @param string $queryString Query-parameters: "&xxx=yyy&zzz=uuu
109 * @return array Array with key/value pairs of query-parameters WITHOUT a certain list of
110 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::makeCacheHash(), \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::typoLink()
111 */
112 public function getRelevantParameters($queryString)
113 {
114 $parameters = $this->splitQueryStringToArray($queryString);
115 $relevantParameters = [];
116 foreach ($parameters as $parameterName => $parameterValue) {
117 if ($this->isAdminPanelParameter($parameterName) || $this->isExcludedParameter($parameterName) || $this->isCoreParameter($parameterName)) {
118 continue;
119 }
120 if ($this->hasCachedParametersWhiteList() && !$this->isInCachedParametersWhiteList($parameterName)) {
121 continue;
122 }
123 if ((is_null($parameterValue) || $parameterValue === '') && !$this->isAllowedWithEmptyValue($parameterName)) {
124 continue;
125 }
126 $relevantParameters[$parameterName] = $parameterValue;
127 }
128 if (!empty($relevantParameters)) {
129 if ($this->includePageId) {
130 if (empty($parameters['id'])) {
131 throw new \RuntimeException('ID parameter needs to be passed for the cHash calculation! As a temporary not recommended workaround, you can set $GLOBALS[\'TYPO3_CONF_VARS\'][\'FE\'][\'cHashIncludePageId\'] to false to avoid this error.', 1467983513);
132 }
133 $relevantParameters['id'] = $parameters['id'];
134 }
135 // Finish and sort parameters array by keys:
136 $relevantParameters['encryptionKey'] = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'];
137 ksort($relevantParameters);
138 }
139 return $relevantParameters;
140 }
141
142 /**
143 * Parses the query string and converts it to an array.
144 * Unlike parse_str it only creates an array with one level.
145 *
146 * e.g. foo[bar]=baz will be array('foo[bar]' => 'baz')
147 *
148 * @param $queryString
149 * @return array
150 */
151 protected function splitQueryStringToArray($queryString)
152 {
153 $parameters = array_filter(explode('&', ltrim($queryString, '?')));
154 $parameterArray = [];
155 foreach ($parameters as $parameter) {
156 list($parameterName, $parameterValue) = explode('=', $parameter);
157 if (trim($parameterName) === '') {
158 // This parameter cannot appear in $_GET in PHP even if its value is not empty, so it should be ignored!
159 continue;
160 }
161 $parameterArray[rawurldecode($parameterName)] = rawurldecode($parameterValue);
162 }
163 return $parameterArray;
164 }
165
166 /**
167 * Checks whether the given parameter starts with TSFE_ADMIN_PANEL
168 * stristr check added to avoid bad performance
169 *
170 * @param string $key
171 * @return bool
172 */
173 protected function isAdminPanelParameter($key)
174 {
175 return stristr($key, 'TSFE_ADMIN_PANEL') !== false && preg_match('/TSFE_ADMIN_PANEL\\[.*?\\]/', $key);
176 }
177
178 /**
179 * Checks whether the given parameter is a core parameter
180 *
181 * @param string $key
182 * @return bool
183 */
184 protected function isCoreParameter($key)
185 {
186 return $key === 'id' || $key === 'type' || $key === 'no_cache' || $key === 'cHash' || $key === 'MP' || $key === 'ftu';
187 }
188
189 /**
190 * Checks whether the given parameter should be exluded from cHash calculation
191 *
192 * @param string $key
193 * @return bool
194 */
195 protected function isExcludedParameter($key)
196 {
197 return in_array($key, $this->excludedParameters);
198 }
199
200 /**
201 * Checks whether the given parameter is an exclusive parameter for cHash calculation
202 *
203 * @param string $key
204 * @return bool
205 */
206 protected function isInCachedParametersWhiteList($key)
207 {
208 return in_array($key, $this->cachedParametersWhiteList);
209 }
210
211 /**
212 * Checks whether cachedParametersWhiteList parameters are configured
213 *
214 * @return bool
215 */
216 protected function hasCachedParametersWhiteList()
217 {
218 return !empty($this->cachedParametersWhiteList);
219 }
220
221 /**
222 * Check whether the given parameter may be used even with an empty value
223 *
224 * @param $key
225 * @return bool
226 */
227 protected function isAllowedWithEmptyValue($key)
228 {
229 return !($this->excludeAllEmptyParameters || in_array($key, $this->excludedParametersIfEmpty));
230 }
231
232 /**
233 * Loops through the configuration array and calls the accordant
234 * getters with the value.
235 *
236 * @param array $configuration
237 */
238 public function setConfiguration(array $configuration)
239 {
240 foreach ($configuration as $name => $value) {
241 $setterName = 'set' . ucfirst($name);
242 if (method_exists($this, $setterName)) {
243 $this->{$setterName}($value);
244 }
245 }
246 }
247
248 /**
249 * @param array $cachedParametersWhiteList
250 */
251 protected function setCachedParametersWhiteList(array $cachedParametersWhiteList)
252 {
253 $this->cachedParametersWhiteList = $cachedParametersWhiteList;
254 }
255
256 /**
257 * @param bool $includePageId
258 */
259 protected function setIncludePageId($includePageId)
260 {
261 $this->includePageId = $includePageId;
262 }
263
264 /**
265 * @param bool $excludeAllEmptyParameters
266 */
267 protected function setExcludeAllEmptyParameters($excludeAllEmptyParameters)
268 {
269 $this->excludeAllEmptyParameters = $excludeAllEmptyParameters;
270 }
271
272 /**
273 * @param array $excludedParameters
274 */
275 protected function setExcludedParameters(array $excludedParameters)
276 {
277 $this->excludedParameters = $excludedParameters;
278 }
279
280 /**
281 * @param array $excludedParametersIfEmpty
282 */
283 protected function setExcludedParametersIfEmpty(array $excludedParametersIfEmpty)
284 {
285 $this->excludedParametersIfEmpty = $excludedParametersIfEmpty;
286 }
287
288 /**
289 * @param array $requireCacheHashPresenceParameters
290 */
291 protected function setRequireCacheHashPresenceParameters(array $requireCacheHashPresenceParameters)
292 {
293 $this->requireCacheHashPresenceParameters = $requireCacheHashPresenceParameters;
294 }
295 }