dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxContentPageCache.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxContent;
3
4require_once __DIR__ . '/dbxContentLng.class.php';
5
7
8 private static bool $dirsReady = false;
9 private const FULL_PAGE_CACHE_VERSION = 'v3';
10 private const FULL_PAGE_GENERATION_FILE = '.generation';
11
12 public static function isConfigEnabled(): bool {
13 $configFile = '';
14 try {
15 $configFile = rtrim((string)dbx()->get_base_dir(), '/\\') . '/dbx/modules/dbx/cfg/config.php';
16 } catch (\Throwable $e) {
17 $configFile = dirname(__DIR__, 2) . '/dbx/cfg/config.php';
18 }
19 if (is_file($configFile) && is_readable($configFile)) {
20 $readConfig = static function(string $path): array {
21 $config = array();
22 include $path;
23 return is_array($config) ? $config : array();
24 };
25 $fileConfig = $readConfig($configFile);
26 if (array_key_exists('cache_content', $fileConfig)) {
27 return (int)$fileConfig['cache_content'] === 1;
28 }
29 }
30
31 $enabled = dbx()->get_config('dbx', 'cache_content');
32 if ($enabled === 'undef' || $enabled === '' || $enabled === null) {
33 return true;
34 }
35
36 return (int) $enabled === 1;
37 }
38
39 public static function setConfigEnabled(bool $enabled): bool {
40 $config = dbx()->get_config('dbx');
41 if (!is_array($config)) {
42 $config = array();
43 }
44
45 $config['cache_content'] = $enabled ? 1 : 0;
46 // Der Schalter steuert ausschliesslich neue Schreibvorgaenge. Bereits
47 // vorhandene Treffer bleiben lesbar und werden nicht geloescht.
48 return (int) dbx()->set_config('dbx', $config) > 0;
49 }
50
52 public static function isEnabled(): bool {
53 return self::isWriteEnabled();
54 }
55
57 public static function isReadEnabled(): bool {
58 if (!self::isGuestSession() || self::hasPersonalizedGuestState()) {
59 return false;
60 }
61
62 $method = strtoupper(trim((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')));
63 if ($method !== 'GET' && $method !== 'HEAD') {
64 return false;
65 }
66
67 if ((int) dbx()->get_request_var('dbx_sync', 1, 'int') !== 1) {
68 return false;
69 }
70
71 if ((int) dbx()->get_system_var('dbx_edit', 0, 'int') > 0) {
72 return false;
73 }
74
75 if ((int) dbx()->get_system_var('dbx_ajax', 0, 'int')) {
76 return false;
77 }
78
79 if ((int) dbx()->get_system_var('dbx_window', 0, 'int')) {
80 return false;
81 }
82
83 return true;
84 }
85
87 public static function isWriteEnabled(): bool {
88 return self::isReadEnabled() && self::isConfigEnabled();
89 }
90
92 private static function isGuestSession(): bool {
93 try {
94 // user() beruecksichtigt auch dbxRunAsAdmin. Ohne diese Abfrage konnte
95 // der Admin-Bypass als Gast HTML in den gemeinsamen Cache schreiben.
96 return (int)dbx()->user() <= 0;
97 } catch (\Throwable $e) {
98 return (int)($_SESSION['dbx']['current_user']['id'] ?? 0) <= 0;
99 }
100 }
101
103 private static function hasPersonalizedGuestState(): bool {
104 $cart = $_SESSION['dbxShop_cart'] ?? array();
105 if (!is_array($cart)) {
106 return false;
107 }
108
109 foreach ($cart as $quantity) {
110 if ((int)$quantity > 0) {
111 return true;
112 }
113 }
114
115 return false;
116 }
117
118 public static function isContentRoute(): bool {
119 if (dbx()->get_system_var('dbx_modul') !== 'dbxContent') {
120 return false;
121 }
122
123 $action = (string) dbx()->get_system_var('dbx_run1', '');
124 return $action === 'show';
125 }
126
127 public static function isPermalinkPageRequest(): bool {
128 if (!self::isEnabled()) {
129 return false;
130 }
131
132 if ((int) dbx()->get_system_var('dbx_content_permalink_request', 0, 'int') !== 1) {
133 return false;
134 }
135
136 return (int) dbx()->get_system_var('dbx_content_route_cid', 0, 'int') > 0;
137 }
138
140 private static function isRawPermalinkRequest(): bool {
141 // Lesen bleibt auch bei ausgeschaltetem Cache-Schreiben aktiv.
142 if (!self::isReadEnabled()) {
143 return false;
144 }
145
146 if (trim((string) dbx()->get_request_var('dbx_modul', '', 'parameter')) !== ''
147 || trim((string) dbx()->get_request_var('dbx_run1', '', 'parameter')) !== ''
148 || (int) dbx()->get_request_var('cid', 0, 'int') > 0
149 || (int) dbx()->get_request_var('dbx_cid', 0, 'int') > 0) {
150 return false;
151 }
152
153 $permalink = self::currentPermalink();
154 if ($permalink === '' || in_array($permalink, array('admin', 'sitemap', 'sitemap.xml', 'robots.txt'), true)) {
155 return false;
156 }
157
158 // Ein Dateiname pro Permalink/Sprache/Design/Skin ist nur fuer reine
159 // Seitenaufrufe eindeutig. Andere Parameter koennten den Inhalt aendern.
160 foreach (array_keys($_GET) as $name) {
161 if (!in_array((string)$name, array('dbx_lng', 'dbx_design', 'dbx_color'), true)) {
162 return false;
163 }
164 }
165
166 return true;
167 }
168
174 public static function prepareFullPageRequest(): bool {
175 dbx()->set_system_var('dbx_full_page_cache_prepared', 0);
176 dbx()->set_system_var('dbx_full_page_cache_path', '');
177 dbx()->set_system_var('dbx_full_page_cache_file', '');
178 dbx()->set_system_var('dbx_full_page_cache_generation', '');
179 dbx()->set_system_var('dbx_full_page_cache_cid', 0);
180 dbx()->set_system_var('dbx_full_page_cache_lng', '');
181
182 if (!self::isRawPermalinkRequest()) {
183 return false;
184 }
185
186 // Die leere Root-Route ist fachlich dieselbe Route wie /home. Dadurch
187 // erzeugt bereits der erste Root-MISS exakt dieselbe fertige Seite.
188 $rawPermalink = self::normalizePermalink((string)dbx()->get_system_var('dbx_permalink', ''));
189 if ($rawPermalink === '') {
190 dbx()->set_system_var('dbx_permalink', 'home');
191 dbx()->set_system_var('dbx_self_url', 'home');
192 }
193
194 $permalink = self::currentPermalink();
195 $lng = self::safeToken(self::currentLng(), 'de');
196 $design = self::currentDesign();
197 $skin = dbx()->normalize_skin((string) dbx()->get_system_var('dbx_color', 'blau'));
198 $generation = self::cacheGeneration();
199 if ($generation === '') {
200 return false;
201 }
202 $path = self::fullPagePathForGeneration($permalink, $lng, $design, $skin, $generation);
203
204 dbx()->set_system_var('dbx_full_page_cache_prepared', 1);
205 dbx()->set_system_var('dbx_full_page_cache_path', $path);
206 dbx()->set_system_var('dbx_full_page_cache_file', basename($path));
207 dbx()->set_system_var('dbx_full_page_cache_permalink', $permalink);
208 dbx()->set_system_var('dbx_full_page_cache_design', $design);
209 dbx()->set_system_var('dbx_full_page_cache_lng', $lng);
210 dbx()->set_system_var('dbx_full_page_cache_generation', $generation);
211
212 return true;
213 }
214
216 public static function attachResolvedContentRoute(): bool {
217 if (!self::isPreparedFullPageRequest()) {
218 return false;
219 }
220
221 // Ein sprachspezifischer Permalink kann die aktive Sprache erst bei der
222 // Content-Aufloesung eindeutig bestimmen. Den vorbereiteten Schreibpfad
223 // dann auf die erkannte Sprache umstellen, statt fremdsprachigen Inhalt
224 // unter dem vorherigen Session-Sprachschluessel abzulegen.
225 $preparedLng = (string)dbx()->get_system_var('dbx_full_page_cache_lng', '');
226 $currentLng = self::safeToken(self::currentLng(), 'de');
227 if ($preparedLng !== $currentLng && !self::prepareFullPageRequest()) {
228 return false;
229 }
230
231 // Ein ungueltiger Permalink kann die Home-Darstellung mit HTTP 404
232 // verwenden. Seine Antwort darf weder unter dem Tippfehler noch als
233 // Home-Cache geschrieben werden.
234 $preparedPermalink = (string)dbx()->get_system_var('dbx_full_page_cache_permalink', '');
235 if ((int)dbx()->get_system_var('dbx_content_not_found', 0, 'int') === 1
236 || $preparedPermalink !== self::currentPermalink()) {
237 dbx()->set_system_var('dbx_full_page_cache_prepared', 0);
238 dbx()->set_system_var('dbx_full_page_cache_path', '');
239 dbx()->set_system_var('dbx_full_page_cache_file', '');
240 return false;
241 }
242
243 $cid = (int) dbx()->get_system_var('dbx_content_route_cid', 0, 'int');
244 if ((int) dbx()->get_system_var('dbx_content_permalink_request', 0, 'int') !== 1 || $cid <= 0) {
245 dbx()->set_system_var('dbx_full_page_cache_prepared', 0);
246 dbx()->set_system_var('dbx_full_page_cache_path', '');
247 dbx()->set_system_var('dbx_full_page_cache_file', '');
248 return false;
249 }
250
251 dbx()->set_system_var('dbx_full_page_cache_cid', $cid);
252 return true;
253 }
254
255 public static function isPreparedFullPageRequest(): bool {
256 return (int) dbx()->get_system_var('dbx_full_page_cache_prepared', 0, 'int') === 1
257 && self::isGuestSession()
258 && !self::hasPersonalizedGuestState()
259 && self::currentPermalink() !== ''
260 && trim((string) dbx()->get_system_var('dbx_full_page_cache_path', '')) !== '';
261 }
262
264 public static function readFullPage(): ?string {
265 if (!self::isPreparedFullPageRequest()) {
266 return null;
267 }
268
269 $preparedGeneration = (string)dbx()->get_system_var('dbx_full_page_cache_generation', '');
270 if ($preparedGeneration === '' || !hash_equals($preparedGeneration, self::cacheGeneration())) {
271 return null;
272 }
273
274 $path = (string) dbx()->get_system_var('dbx_full_page_cache_path', '');
275 if (!is_file($path) || !is_readable($path)) {
276 return null;
277 }
278
279 $html = file_get_contents($path);
280 if (!is_string($html) || !self::isCompleteHtml($html) || !self::hasCurrentBaseHref($html)) {
281 @unlink($path);
282 @unlink(self::fullPageMetaPath($path));
283 return null;
284 }
285
286 // Eine Invalidierung waehrend des Lesens macht auch bereits gelesene
287 // Bytes ungueltig. So wird nach einem Speichern kein alter Stand bedient.
288 if (!hash_equals($preparedGeneration, self::cacheGeneration())) {
289 return null;
290 }
291
292 // file_get_contents liefert die unveraenderten Bytes. Kein Escaping,
293 // keine Interpretation und keine Session-abhaengige Nachbearbeitung.
294 return $html;
295 }
296
298 public static function writeFullPage(string $html): bool {
299 if (!self::isWriteEnabled()
300 || !self::isPreparedFullPageRequest()
301 || http_response_code() !== 200
302 || (string) dbx()->get_system_var('dbx_master_modul', '') !== 'dbxContent'
303 || (int) dbx()->get_system_var('dbx_full_page_cache_cid', 0, 'int') <= 0
304 || !self::isCompleteHtml($html)
305 || !self::hasCurrentBaseHref($html)
306 || preg_match(self::secureInputPattern(), $html)) {
307 return false;
308 }
309
310 $preparedGeneration = (string)dbx()->get_system_var('dbx_full_page_cache_generation', '');
311 if ($preparedGeneration === '' || !hash_equals($preparedGeneration, self::cacheGeneration())) {
312 // Ein paralleler Speichervorgang hat den Cache inzwischen verworfen.
313 // Der alte Request darf seinen inzwischen veralteten Stand nicht in
314 // die neue Cache-Generation schreiben.
315 return false;
316 }
317
318 self::ensureDirs();
319 $path = (string) dbx()->get_system_var('dbx_full_page_cache_path', '');
320 return self::atomicWrite($path, $html);
321 }
322
323 public static function baseDir(): string {
324 $dir = rtrim(dbx()->get_file_dir(), '/\\') . '/cache/';
325 return dbx()->os_path($dir);
326 }
327
328 public static function menuVariantFlat(int $flat = 1): string {
329 return 'flat-' . ((int) $flat === 0 ? 0 : 1);
330 }
331
332 public static function menuVariantLoad(int $deep = 9, string $mode = '', string $label = ''): string {
333 $deep = max(1, (int) $deep);
334 $mode = strtolower(trim($mode));
335 if ($mode === '') {
336 $mode = 'default';
337 }
338 $mode = self::safeToken($mode, 'default');
339 $label = trim((string) $label);
340 $labelKey = $label !== '' ? substr(sha1($label), 0, 8) : 'menu';
341
342 return 'load-' . $deep . '-' . $mode . '-' . $labelKey;
343 }
344
345 public static function currentLng(): string {
346 $lng = trim((string) dbx()->get_system_var('dbx_lng', 'de'));
347 return $lng !== '' ? $lng : 'de';
348 }
349
351 public static function currentDesign(): string {
352 $config = dbx()->get_config('dbx');
353 if (!is_array($config)) {
354 $config = array();
355 }
356
357 $userDefault = trim((string)($config['default_design_user'] ?? 'dbxapp'));
358 if ($userDefault === '') {
359 $userDefault = 'dbxapp';
360 }
361
362 $design = trim((string)dbx()->get_system_var('dbx_design', $userDefault));
363 // Der Full-Page-Cache gilt nur fuer Gaeste. Die Aliase user/admin werden
364 // daher genauso wie in check_design auf das Frontend-Design aufgeloest.
365 if ($design === '' || $design === 'user' || $design === 'admin' || !dbx()->is_design($design)) {
366 $design = $userDefault;
367 }
368 if (!dbx()->is_design($design)) {
369 $design = 'dbxapp';
370 }
371
372 return self::safeToken($design, 'dbxapp');
373 }
374
375 public static function ensureDirs(): void {
376 if (self::$dirsReady) {
377 return;
378 }
379
380 foreach (array('content', 'content/full-page', 'meta') as $sub) {
381 $dir = self::baseDir() . $sub;
382 if (!is_dir($dir)) {
383 @mkdir($dir, 0755, true);
384 }
385 }
386
387 self::purgeLegacyMenuCache();
388 self::purgeLegacyPageCache();
389 self::purgeStaleFullPages();
390 self::$dirsReady = true;
391 }
392
394 private static function purgeLegacyPageCache(): void {
395 foreach (array(
396 self::baseDir() . 'meta/pages/*.json',
397 self::baseDir() . 'meta/permalinks_*.json',
398 self::baseDir() . 'meta/home_*.json',
399 ) as $pattern) {
400 foreach (glob($pattern) ?: array() as $file) {
401 @unlink($file);
402 }
403 }
404
405 $contentDir = self::baseDir() . 'content/';
406 foreach (array_merge(glob($contentDir . '*.htm') ?: array(), glob($contentDir . '*.html') ?: array()) as $file) {
407 @unlink($file);
408 @unlink(self::fullPageMetaPath($file));
409 }
410 foreach (glob($contentDir . '*.html.meta.json') ?: array() as $metaFile) {
411 $htmlFile = preg_replace('/\.meta\.json$/', '', $metaFile);
412 if (!is_string($htmlFile) || !is_file($htmlFile)) {
413 @unlink($metaFile);
414 }
415 }
416 // Hash-Unterordner und Meta-Dateien des alten Full-Page-Caches werden
417 // nicht mehr verwendet. Neue Dateien liegen direkt in full-page/.
418 foreach (glob($contentDir . 'full-page/*.htm.meta.json') ?: array() as $metaFile) {
419 @unlink($metaFile);
420 }
421 // V1 enthielt kein Design, V2 keinen kollisionsfreien Permalink-/Host-Key
422 // und keine Generation. Nur die aktuelle V3 darf weiterverwendet werden.
423 foreach (glob($contentDir . 'full-page/*.htm') ?: array() as $fullPageFile) {
424 if (!str_ends_with(strtolower(basename($fullPageFile)), '_' . self::FULL_PAGE_CACHE_VERSION . '.htm')) {
425 @unlink($fullPageFile);
426 }
427 }
428 foreach (glob($contentDir . 'full-page/*', GLOB_ONLYDIR) ?: array() as $legacyDir) {
429 foreach (glob($legacyDir . '/*') ?: array() as $legacyFile) {
430 if (is_file($legacyFile)) {
431 @unlink($legacyFile);
432 }
433 }
434 @rmdir($legacyDir);
435 }
436 }
437
439 public static function purgeLegacyMenuCache(): int {
440 $dir = self::baseDir() . 'menu/';
441 if (!is_dir($dir)) {
442 return 0;
443 }
444
445 $removed = 0;
446 foreach (glob($dir . '*.html') ?: array() as $path) {
447 if (@unlink($path)) {
448 $removed++;
449 }
450 }
451
452 return $removed;
453 }
454
455 public static function contentPath(int $cid, string $lng): string {
456 $cid = (int) $cid;
457 $lng = self::safeToken($lng, 'de');
458 return self::baseDir() . 'content/cid-' . $cid . '.' . $lng . '.htm';
459 }
460
461 public static function fullPagePath(string $permalink, string $lng, string $design = '', string $skin = ''): string {
462 return self::fullPagePathForGeneration($permalink, $lng, $design, $skin, self::cacheGeneration());
463 }
464
465 private static function fullPagePathForGeneration(string $permalink, string $lng, string $design, string $skin, string $generation): string {
466 $normalizedPermalink = self::normalizePermalink($permalink);
467 $permalink = self::permalinkFileToken($normalizedPermalink);
468 $lng = self::safeToken($lng, 'de');
469 $design = self::safeToken($design !== '' ? $design : self::currentDesign(), 'dbxapp');
470 $skin = dbx()->normalize_skin($skin !== '' ? $skin : (string)dbx()->get_system_var('dbx_color', 'blau'));
471 $skin = self::safeToken($skin, 'blau');
472 $generation = self::safeToken($generation, 'invalid');
473 $origin = self::requestOriginToken();
474
475 return self::baseDir() . 'content/full-page/'
476 . $permalink . '_' . $lng . '_' . $design . '_' . $skin . '_'
477 . $origin . '_' . $generation . '_' . self::FULL_PAGE_CACHE_VERSION . '.htm';
478 }
479
480 public static function permalinkContentPath(string $permalink, string $lng): string {
481 $permalink = self::permalinkFileToken($permalink);
482 $lng = self::safeToken($lng, 'de');
483
484 return self::baseDir() . 'content/' . $permalink . '.' . $lng . '.htm';
485 }
486
487 public static function menuPath(int $root, string $lng, string $variant = 'flat-1'): string {
488 $root = (int) $root;
489 $lng = self::safeToken($lng, 'de');
490 $variant = self::safeToken($variant, 'flat-1');
491
492 return self::baseDir() . 'menu/' . $root . '_' . $lng . '_' . $variant . '.html';
493 }
494
495 public static function pageMetaPath(int $cid): string {
496 return self::baseDir() . 'meta/pages/' . (int) $cid . '.json';
497 }
498
499 public static function readContent(int $cid, string $lng = ''): ?string {
500 // Der alte dbxContent-Zwischencache ist bewusst deaktiviert.
501 return null;
502 }
503
504 public static function readPermalinkContent(string $permalink, string $lng = ''): ?string {
505 // Ein separater Permalink-Cache existiert nicht mehr.
506 return null;
507 }
508
509 public static function writeContent(int $cid, string $html, array $meta = array(), string $lng = ''): bool {
510 // Der alte dbxContent-Zwischencache ist bewusst deaktiviert.
511 return false;
512 }
513
514 public static function readMenu(int $root, string $variant = 'flat-1', string $lng = ''): ?string {
515 return null;
516 }
517
518 public static function writeMenu(string $html, int $root, string $variant = 'flat-1', string $lng = ''): bool {
519 return false;
520 }
521
522 public static function readPageMeta(int $cid): ?array {
523 $meta = self::buildContentMeta($cid, self::currentLng());
524 return count($meta) ? $meta : null;
525 }
526
527 private static function readPageMetaFile(int $cid): ?array {
528 $path = self::pageMetaPath($cid);
529 if (!is_file($path) || !is_readable($path)) {
530 return null;
531 }
532
533 $json = file_get_contents($path);
534 if (!is_string($json) || $json === '') {
535 return null;
536 }
537
538 $data = json_decode($json, true);
539 return is_array($data) ? $data : null;
540 }
541
542 public static function writePageMeta(int $cid, array $meta): bool {
543 // Seiten-Metadaten werden live aus dbxContent gelesen und nicht gecacht.
544 return true;
545 }
546
547 public static function invalidateContent(int $cid): void {
548 $cid = (int) $cid;
549 if ($cid <= 0) {
550 return;
551 }
552
553 // Der Full-Page-Dateiname enthaelt bewusst keine Content-ID. Bei einer
554 // Inhaltsaenderung werden deshalb alle fertigen Gastseiten verworfen.
555 self::invalidateAllFullPages();
556
557 $permalinks = array();
558 $meta = self::readPageMetaFile($cid);
559 if (is_array($meta)) {
560 $permalink = trim((string) ($meta['permalink'] ?? ''));
561 $lng = trim((string) ($meta['lng'] ?? ''));
562 if ($permalink !== '') {
563 $permalinks[] = array('permalink' => $permalink, 'lng' => $lng);
564 }
565 }
566
567 foreach (glob(self::baseDir() . 'meta/permalinks_*.json') ?: array() as $indexFile) {
568 $json = file_get_contents($indexFile);
569 if (!is_string($json) || $json === '') {
570 continue;
571 }
572 $index = json_decode($json, true);
573 if (!is_array($index)) {
574 continue;
575 }
576 $lng = '';
577 if (preg_match('/^permalinks_([a-z]{2,3})\.json$/i', basename($indexFile), $match)) {
578 $lng = strtolower($match[1]);
579 }
580 foreach ($index as $permalink => $row) {
581 if (is_array($row) && (int) ($row['cid'] ?? 0) === $cid) {
582 $permalinks[] = array('permalink' => (string) $permalink, 'lng' => $lng);
583 }
584 }
585 }
586
587 $seen = array();
588 foreach ($permalinks as $item) {
589 $permalink = trim((string) ($item['permalink'] ?? ''));
590 if ($permalink === '') {
591 continue;
592 }
593 $lng = trim((string) ($item['lng'] ?? ''));
594 $token = self::permalinkFileToken($permalink);
595 $key = $token . '|' . $lng;
596 if (isset($seen[$key])) {
597 continue;
598 }
599 $seen[$key] = 1;
600 $legacyToken = self::legacyPermalinkFileToken($permalink);
601 $pattern = $lng !== ''
602 ? self::baseDir() . 'content/' . $token . '.' . self::safeToken($lng, 'de') . '.htm'
603 : self::baseDir() . 'content/' . $token . '.*.htm';
604 foreach (glob($pattern) ?: array() as $file) {
605 @unlink($file);
606 }
607 foreach (array(
608 self::baseDir() . 'content/' . $legacyToken . '.*.htm',
609 self::baseDir() . 'content/' . $legacyToken . '__*.html',
610 ) as $legacyPattern) {
611 foreach (glob($legacyPattern) ?: array() as $file) {
612 @unlink($file);
613 }
614 }
615 }
616
617 foreach (glob(self::baseDir() . 'content/cid-' . $cid . '.*.htm') ?: array() as $file) {
618 @unlink($file);
619 }
620 foreach (glob(self::baseDir() . 'content/cid-' . $cid . '.*.full-*.html') ?: array() as $file) {
621 @unlink($file);
622 }
623 foreach (glob(self::baseDir() . 'content/full-page/*/*.htm.meta.json') ?: array() as $metaFile) {
624 $cacheMeta = self::readJsonFile($metaFile);
625 if (!is_array($cacheMeta) || (int)($cacheMeta['cid'] ?? 0) !== $cid) {
626 continue;
627 }
628 $htmlFile = preg_replace('/\.meta\.json$/', '', $metaFile);
629 if (is_string($htmlFile)) {
630 @unlink($htmlFile);
631 }
632 @unlink($metaFile);
633 }
634 foreach (glob(self::baseDir() . 'content/' . $cid . '_*.html') ?: array() as $file) {
635 @unlink($file);
636 }
637
638 @unlink(self::pageMetaPath($cid));
639 if (class_exists(__NAMESPACE__ . '\\dbxContentSitemap')) {
640 dbxContentSitemap::invalidate();
641 }
642 }
643
644 public static function invalidateMenu(int $root): void {
645 $root = (int) $root;
646 $pattern = self::baseDir() . 'menu/' . $root . '_*.html';
647 foreach (glob($pattern) ?: array() as $file) {
648 @unlink($file);
649 }
650 self::invalidateAllFullPages();
651 }
652
653 public static function invalidateAllMenus(): void {
654 foreach (glob(self::baseDir() . 'menu/*.html') ?: array() as $file) {
655 @unlink($file);
656 }
657 self::invalidateAllFullPages();
658 self::invalidateSitemap();
659 }
660
662 public static function invalidateAllFullPages(): int {
663 // Zuerst die Generation wechseln: Ab diesem Moment koennen neue Requests
664 // weder alte Dateien lesen noch unter deren Namen schreiben. Das schliesst
665 // die Race Condition "Speichern waehrend ein alter Request rendert".
666 $generation = self::cacheGeneration(true);
667
668 $removed = self::purgeStaleFullPages($generation);
669 foreach (array('permalink-*.*.full-*.html', 'cid-*.*.full-*.html') as $pattern) {
670 foreach (glob(self::baseDir() . 'content/' . $pattern) ?: array() as $file) {
671 if (@unlink($file)) {
672 $removed++;
673 }
674 @unlink(self::fullPageMetaPath($file));
675 }
676 }
677 foreach (glob(self::baseDir() . 'content/full-page/*/*.htm') ?: array() as $file) {
678 if (@unlink($file)) {
679 $removed++;
680 }
681 @unlink(self::fullPageMetaPath($file));
682 @rmdir(dirname($file));
683 }
684 foreach (glob(self::baseDir() . 'content/full-page/*.htm.meta.json') ?: array() as $metaFile) {
685 @unlink($metaFile);
686 }
687 foreach (glob(self::baseDir() . 'content/*.html.meta.json') ?: array() as $metaFile) {
688 @unlink($metaFile);
689 }
690 foreach (glob(self::baseDir() . 'content/full-page/*/*.htm.meta.json') ?: array() as $metaFile) {
691 @unlink($metaFile);
692 @rmdir(dirname($metaFile));
693 }
694 return $removed;
695 }
696
702 public static function purgeStaleFullPages(string $generation = ''): int {
703 if ($generation === '') {
704 $generation = self::cacheGeneration();
705 }
706 if (!preg_match('/^[a-f0-9]{24}$/', $generation)) {
707 return 0;
708 }
709
710 $removed = 0;
711 $currentSuffix = '_' . $generation . '_' . self::FULL_PAGE_CACHE_VERSION . '.htm';
712 foreach (glob(self::baseDir() . 'content/full-page/*.htm') ?: array() as $file) {
713 if (str_ends_with(strtolower(basename($file)), $currentSuffix)) {
714 continue;
715 }
716 if (@unlink($file)) {
717 $removed++;
718 }
719 @unlink(self::fullPageMetaPath($file));
720 }
721 foreach (glob(self::baseDir() . 'content/full-page/*.tmp-*') ?: array() as $temporary) {
722 if (is_file($temporary) && (int)@filemtime($temporary) < time() - 300) {
723 @unlink($temporary);
724 }
725 }
726 return $removed;
727 }
728
729 public static function invalidateSitemap(): void {
730 if (class_exists(__NAMESPACE__ . '\\dbxContentSitemap')) {
732 return;
733 }
734
735 @unlink(self::baseDir() . 'meta/sitemap.xml');
736 }
737
738 public static function invalidateAll(): array {
739 $stats = array(
740 'content' => 0,
741 'menu' => 0,
742 'meta' => 0,
743 );
744
745 foreach (array('*.htm', '*.html') as $pattern) {
746 foreach (glob(self::baseDir() . 'content/' . $pattern) ?: array() as $file) {
747 if (@unlink($file)) {
748 $stats['content']++;
749 }
750 }
751 }
752 foreach (glob(self::baseDir() . 'content/*.html.meta.json') ?: array() as $file) {
753 if (@unlink($file)) {
754 $stats['meta']++;
755 }
756 }
757 $stats['content'] += self::invalidateAllFullPages();
758
759 foreach (glob(self::baseDir() . 'menu/*.html') ?: array() as $file) {
760 if (@unlink($file)) {
761 $stats['menu']++;
762 }
763 }
764
765 foreach (glob(self::baseDir() . 'meta/pages/*.json') ?: array() as $file) {
766 if (@unlink($file)) {
767 $stats['meta']++;
768 }
769 }
770
771 foreach (glob(self::baseDir() . 'meta/permalinks_*.json') ?: array() as $file) {
772 if (@unlink($file)) {
773 $stats['meta']++;
774 }
775 }
776
777 foreach (glob(self::baseDir() . 'meta/home_*.json') ?: array() as $file) {
778 if (@unlink($file)) {
779 $stats['meta']++;
780 }
781 }
782
783 return $stats;
784 }
785
786 public static function cacheStats(): array {
787 self::ensureDirs();
788 self::purgeStaleFullPages();
789 $content = array_values(array_filter(
790 glob(self::baseDir() . 'content/full-page/*.htm') ?: array(),
791 static fn(string $path): bool => str_ends_with(strtolower(basename($path)), '_' . self::FULL_PAGE_CACHE_VERSION . '.htm')
792 ));
793 $sitemapPath = self::baseDir() . 'meta/sitemap.xml';
794
795 return array(
796 'content' => count($content),
797 'menu' => 0,
798 'meta' => 0,
799 'permalinks' => 0,
800 'home' => 0,
801 'sitemap' => is_file($sitemapPath) ? 1 : 0,
802 'base_dir' => self::baseDir(),
803 );
804 }
805
806 public static function invalidateFolderTree($db, int $folderId): void {
807 if (!is_object($db)) {
808 return;
809 }
810
811 $folderId = (int) $folderId;
812 self::invalidateMenu($folderId);
813 self::invalidateMenu(0);
814
815 $folderIds = self::collectFolderIds($db, $folderId);
816 foreach ($folderIds as $id) {
817 self::invalidateMenu((int) $id);
818 }
819
820 $pages = $db->select(dbxContentLng::ddContent(), 'folder IN (' . implode(',', array_map('intval', $folderIds)) . ')', 'id', 'id', 'ASC', '', 0, 0, 0);
821 if (is_array($pages)) {
822 foreach ($pages as $page) {
823 self::invalidateContent((int) ($page['id'] ?? 0));
824 }
825 }
826 }
827
828 private static function collectFolderIds($db, int $folderId): array {
829 $folderId = (int) $folderId;
830 $ids = array($folderId);
831 $queue = array($folderId);
832
833 while (count($queue)) {
834 $current = (int) array_shift($queue);
835 $rows = $db->select(dbxContentLng::ddFolder(), 'parent_id = ' . $current, 'id', 'id', 'ASC', '', 0, 0, 0);
836 if (!is_array($rows)) {
837 continue;
838 }
839 foreach ($rows as $row) {
840 $id = (int) ($row['id'] ?? 0);
841 if ($id > 0 && !in_array($id, $ids, true)) {
842 $ids[] = $id;
843 $queue[] = $id;
844 }
845 }
846 }
847
848 return $ids;
849 }
850
851 private static function buildContentMeta(int $cid, string $lng = ''): array {
852 $cid = (int) $cid;
853 if ($cid <= 0) {
854 return array();
855 }
856
857 $prevLng = null;
858 if ($lng !== '') {
859 $currentLng = (string) dbx()->get_system_var('dbx_lng', 'de');
860 if ($lng !== $currentLng) {
861 $prevLng = $currentLng;
862 dbx()->set_system_var('dbx_lng', $lng);
863 }
864 }
865
866 require_once __DIR__ . '/dbxContentRenderer.class.php';
867 $db = dbx()->get_system_obj('dbxDB');
868 if (!is_object($db)) {
869 if ($prevLng !== null) {
870 dbx()->set_system_var('dbx_lng', $prevLng);
871 }
872 return array();
873 }
874
875 $rec = $db->select1(dbxContentLng::ddContent(), $cid, 'permalink,activ,folder,title,seo_title,description,keywords,meta_robots,seo_image_id,update_date,lng_uid', 0);
876 if (!is_array($rec)) {
877 if ($prevLng !== null) {
878 dbx()->set_system_var('dbx_lng', $prevLng);
879 }
880 return array();
881 }
882
883 $renderer = new dbxContentRenderer();
884 $rights = $renderer->getPublicFolderRights((int)($rec['folder'] ?? 0));
885 $meta = array(
886 'cid' => $cid,
887 'permalink' => (string)($rec['permalink'] ?? ''),
888 'rights' => $rights,
889 'activ' => (int)($rec['activ'] ?? 1),
890 'seo' => dbxContentRenderer::seoMetaFromRecord($rec),
891 );
892 if ($prevLng !== null) {
893 dbx()->set_system_var('dbx_lng', $prevLng);
894 }
895 return $meta;
896 }
897
898 private static function currentPermalink(): string {
899 $permalink = self::normalizePermalink((string) dbx()->get_system_var('dbx_permalink', ''));
900 return $permalink === '' ? 'home' : $permalink;
901 }
902
903 private static function normalizePermalink(string $permalink): string {
904 $permalink = trim(str_replace('\\', '/', $permalink));
905 if ($permalink === 'undef' || $permalink === '/') {
906 return '';
907 }
908
909 return trim($permalink, '/');
910 }
911
912 private static function fullPageMetaPath(string $htmlPath): string {
913 return $htmlPath . '.meta.json';
914 }
915
916 private static function readJsonFile(string $path): ?array {
917 if (!is_file($path) || !is_readable($path)) {
918 return null;
919 }
920
921 $json = file_get_contents($path);
922 if (!is_string($json) || $json === '') {
923 return null;
924 }
925
926 $data = json_decode($json, true);
927 return is_array($data) ? $data : null;
928 }
929
930 private static function isCompleteHtml(string $html): bool {
931 if (trim($html) === '') {
932 return false;
933 }
934
935 return (bool) preg_match('/^\s*<!doctype\s+html\b/i', $html)
936 && stripos($html, '</html>') !== false;
937 }
938
945 private static function hasCurrentBaseHref(string $html): bool {
946 $expected = self::normalizeBaseHref((string)dbx()->get_base_url());
947 if ($expected === '') {
948 return false;
949 }
950
951 if (!preg_match('~<head\b[^>]*>(.*?)</head>~is', $html, $headMatch)) {
952 return false;
953 }
954 $head = preg_replace('~<!--.*?-->|<script\b.*?</script>|<style\b.*?</style>~is', '', (string)$headMatch[1]) ?? '';
955
956 if (!preg_match(
957 '~<base\b[^>]*\bhref\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s>]+))~i',
958 $head,
959 $baseMatch
960 )) {
961 return false;
962 }
963
964 $actual = (string)($baseMatch[1] !== ''
965 ? $baseMatch[1]
966 : (($baseMatch[2] ?? '') !== '' ? $baseMatch[2] : ($baseMatch[3] ?? '')));
967
968 return self::normalizeBaseHref($actual) === $expected;
969 }
970
972 private static function normalizeBaseHref(string $href): string {
973 $href = html_entity_decode(trim($href), ENT_QUOTES | ENT_HTML5, 'UTF-8');
974 $href = str_replace('\\', '/', preg_replace('/[\x00-\x1F\x7F]/', '', $href) ?? '');
975 if ($href === '') {
976 return '';
977 }
978
979 $parts = parse_url($href);
980 if (!is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) {
981 return '';
982 }
983
984 $scheme = strtolower((string)$parts['scheme']);
985 $host = strtolower((string)$parts['host']);
986 $port = isset($parts['port']) ? (int)$parts['port'] : 0;
987 $defaultPort = ($scheme === 'https') ? 443 : (($scheme === 'http') ? 80 : 0);
988
989 $normalized = $scheme . '://' . $host;
990 if ($port > 0 && $port !== $defaultPort) {
991 $normalized .= ':' . $port;
992 }
993 $normalized .= (string)($parts['path'] ?? '');
994 if (isset($parts['query'])) {
995 $normalized .= '?' . $parts['query'];
996 }
997 if (isset($parts['fragment'])) {
998 $normalized .= '#' . $parts['fragment'];
999 }
1000
1001 return $normalized;
1002 }
1003
1005 private static function secureInputPattern(): string {
1006 return '/<input\b(?=[^>]*\bname\s*=\s*["\']?_[^"\'\s>]+)(?=[^>]*\bvalue\s*=\s*["\']?[a-f0-9]{64}["\']?)[^>]*>/i';
1007 }
1008
1009 private static function atomicWrite(string $path, string $html): bool {
1010 if ($path === '') {
1011 return false;
1012 }
1013
1014 $dir = dirname($path);
1015 if (!is_dir($dir) && !@mkdir($dir, 0755, true) && !is_dir($dir)) {
1016 return false;
1017 }
1018
1019 try {
1020 $suffix = bin2hex(random_bytes(6));
1021 } catch (\Throwable $e) {
1022 $suffix = str_replace('.', '', uniqid('', true));
1023 }
1024
1025 $tmp = $path . '.tmp-' . $suffix;
1026 $bytes = @file_put_contents($tmp, $html, LOCK_EX);
1027 if ($bytes === false || $bytes !== strlen($html)) {
1028 @unlink($tmp);
1029 return false;
1030 }
1031
1032 if (@rename($tmp, $path)) {
1033 return true;
1034 }
1035
1036 // Ein paralleler Request kann denselben vollstaendigen Cache bereits
1037 // gewonnen haben. Dessen Datei bleibt dann unangetastet.
1038 if (is_file($path) && is_readable($path)) {
1039 @unlink($tmp);
1040 return true;
1041 }
1042
1043 @unlink($tmp);
1044 return false;
1045 }
1046
1053 private static function cacheGeneration(bool $rotate = false): string {
1054 $dir = self::baseDir() . 'content/full-page/';
1055 if (!is_dir($dir) && !@mkdir($dir, 0755, true) && !is_dir($dir)) {
1056 return '';
1057 }
1058
1059 $handle = @fopen($dir . self::FULL_PAGE_GENERATION_FILE, 'c+b');
1060 if (!is_resource($handle)) {
1061 return '';
1062 }
1063 if (!@flock($handle, $rotate ? LOCK_EX : LOCK_SH)) {
1064 @fclose($handle);
1065 return '';
1066 }
1067
1068 @rewind($handle);
1069 $generation = trim((string)stream_get_contents($handle));
1070 if (!$rotate && !preg_match('/^[a-f0-9]{24}$/', $generation)) {
1071 // Upgrade ohne festgehaltene SH-Sperre, danach unter EX erneut lesen.
1072 @flock($handle, LOCK_UN);
1073 if (!@flock($handle, LOCK_EX)) {
1074 @fclose($handle);
1075 return '';
1076 }
1077 @rewind($handle);
1078 $generation = trim((string)stream_get_contents($handle));
1079 }
1080
1081 if ($rotate || !preg_match('/^[a-f0-9]{24}$/', $generation)) {
1082 try {
1083 $generation = bin2hex(random_bytes(12));
1084 } catch (\Throwable $e) {
1085 $generation = substr(hash('sha256', uniqid('', true) . microtime(true)), 0, 24);
1086 }
1087
1088 $written = @ftruncate($handle, 0)
1089 && @rewind($handle)
1090 && @fwrite($handle, $generation) === strlen($generation)
1091 && @fflush($handle);
1092 if (!$written) {
1093 $generation = '';
1094 }
1095 }
1096
1097 @flock($handle, LOCK_UN);
1098 @fclose($handle);
1099 return $generation;
1100 }
1101
1103 private static function requestOriginToken(): string {
1104 $forwardedProto = strtolower(trim(explode(',', (string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''))[0]));
1105 if ($forwardedProto === 'http' || $forwardedProto === 'https') {
1106 $scheme = $forwardedProto;
1107 } else {
1108 $https = strtolower((string)($_SERVER['HTTPS'] ?? ''));
1109 $scheme = ($https !== '' && $https !== 'off') || (int)($_SERVER['SERVER_PORT'] ?? 0) === 443
1110 ? 'https'
1111 : 'http';
1112 }
1113
1114 $host = strtolower(trim((string)($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'local')));
1115 $host = preg_replace('/[^a-z0-9.:[\‍]-]+/i', '', $host) ?: 'local';
1116 $script = str_replace('\\', '/', (string)($_SERVER['SCRIPT_NAME'] ?? ''));
1117 $installPath = trim(str_replace('\\', '/', dirname($script)), '/.');
1118
1119 return substr(hash('sha256', $scheme . '://' . $host . '/' . $installPath), 0, 16);
1120 }
1121
1122 private static function safeToken(string $value, string $fallback): string {
1123 $value = strtolower(trim($value));
1124 if ($value === '' || !preg_match('/^[a-z0-9_-]+$/', $value)) {
1125 return $fallback;
1126 }
1127 return $value;
1128 }
1129
1130 private static function permalinkFileToken(string $permalink): string {
1131 $permalink = strtolower(trim(str_replace('\\', '/', $permalink), '/'));
1132 if ($permalink === '') {
1133 $permalink = 'home';
1134 }
1135
1136 $token = preg_replace('/[^a-z0-9_-]+/i', '-', $permalink);
1137 $token = trim((string) $token, '-_');
1138 if ($token === '') {
1139 $token = 'page';
1140 }
1141
1142 if (strlen($token) > 72) {
1143 $token = substr($token, 0, 72);
1144 }
1145
1146 // Der lesbare Slug allein kollidiert z. B. bei abc/def und abc-def.
1147 // Der Hash wird aus dem vollstaendigen normalisierten Permalink gebildet.
1148 return strtolower($token . '-' . substr(hash('sha256', $permalink), 0, 24));
1149 }
1150
1151 private static function legacyPermalinkFileToken(string $permalink): string {
1152 $permalink = strtolower(trim(str_replace('\\', '/', $permalink), '/'));
1153 if ($permalink === '') {
1154 return 'home';
1155 }
1156
1157 $token = preg_replace('/[^a-z0-9_-]+/i', '-', $permalink);
1158 $token = trim((string) $token, '-_');
1159 if ($token === '') {
1160 $token = 'page';
1161 }
1162
1163 $hash = substr(sha1($permalink), 0, 8);
1164 if (strlen($token) > 80) {
1165 $token = substr($token, 0, 80);
1166 }
1167
1168 return strtolower($token . '-' . $hash);
1169 }
1170}
static attachResolvedContentRoute()
Bindet nach einem MISS die live aufgeloeste Content-ID an den Schreibvorgang.
static permalinkContentPath(string $permalink, string $lng)
static readPermalinkContent(string $permalink, string $lng='')
static writeFullPage(string $html)
Schreibt die finale Ausgabe nach Design, Modulen, Interpreter und Filtern.
static invalidateAllFullPages()
Menues sind Bestandteil jeder Full-Page-Ausgabe.
static currentDesign()
Das Design ist Bestandteil der vollstaendigen HTML-Ausgabe.
static prepareFullPageRequest()
Bereitet den Gast-Seiten-Cache nur aus dem bereits erkannten URL-Permalink vor.
static writeMenu(string $html, int $root, string $variant='flat-1', string $lng='')
static isWriteEnabled()
Darf der aktuelle Request eine neue Gastseite in den Cache schreiben?
static readMenu(int $root, string $variant='flat-1', string $lng='')
static writeContent(int $cid, string $html, array $meta=array(), string $lng='')
static isEnabled()
Abwaertskompatibel: "aktiv" bezeichnet das Schreiben neuer Seiten.
static readFullPage()
Liefert ausschliesslich eine bereits komplett gerenderte HTML-Seite.
static purgeStaleFullPages(string $generation='')
Entfernt V3-Dateien alter Generationen.
static menuVariantLoad(int $deep=9, string $mode='', string $label='')
static fullPagePath(string $permalink, string $lng, string $design='', string $skin='')
static readContent(int $cid, string $lng='')
static menuPath(int $root, string $lng, string $variant='flat-1')
static isReadEnabled()
Darf der aktuelle Request vorhandene Gastseiten aus dem Cache lesen?
static writePageMeta(int $cid, array $meta)
static purgeLegacyMenuCache()
Entfernt den nicht mehr verwendeten Menu-Cache.
if(dbx() ->check_action_token( 'missing.scope', $unknownToken)) if(isset($_SESSION['dbx']['session']['dbx']['security']['action_token_secret'])) $legacyToken
user($key='id')
Liest einen Wert des aktuellen Benutzers.
Definition dbxApi.php:1305
dbx()
Liefert die zentrale dbXapp-API als Singleton.
Definition dbxApi.php:2805
set_config(string $modul, array $config)
Speichert die Konfiguration eines Moduls.
Definition dbxApi.php:793
get_base_dir(int $cutData=0)
Liefert das Basisverzeichnis der Installation.
Definition dbxApi.php:1507
get_file_dir()
Liefert das files/-Verzeichnis der Installation.
Definition dbxApi.php:1524
is_design($design, $page='default')
Prueft, ob ein Design fuer die angegebene Seite existiert.
Definition dbxApi.php:2217
parse_url($data)
Zerlegt URL-/Parameterdaten in einen Array.
Definition dbxApi.php:2416
get_base_url()
Liefert die Basis-URL des aktuellen Requests.
Definition dbxApi.php:1488
$_SERVER['REQUEST_METHOD']
if($resolved !==$expectedBase . 'files/test/') $config
if(preg_match('/core\.js\? foreach[^"\']*v=(\d+)/', $designTemplate, $assetMatch) !== 1 || !str_contains($shopReference, 'dbxapp-Asset-Version ' . $assetMatch[1])) foreach (array( 'reference\\archive', 'provision_docs_content.php', 'dbxSelfTest') as $needle)(array('Installation'=> $installation, 'Installations-Tutorial'=> $installationTutorial, 'SelfTest'=> $selfTest) as $label=> $html)
foreach( $iterator as $file) if(! $groups) $expected
if(trim($second) !=='second') $design
if(!defined( 'IMG_WEBP')) define( 'IMG_WEBP'
$https
Definition index.php:20
DBX schema administration.
if($cinematic===''||!str_contains($cinematic, 'data-dbx-cinema')) $page