Skip to content

Commit 29dba65

Browse files
Merge pull request #7394 from christianbeeznest/fixes-updates226
Internal: Fix migrated tracking header/footer extra content settings
2 parents 6e05529 + 00fee8b commit 29dba65

2 files changed

Lines changed: 202 additions & 1 deletion

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?php
2+
3+
/* For licensing terms, see /license.txt */
4+
5+
declare(strict_types=1);
6+
7+
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
8+
9+
use Chamilo\CoreBundle\Entity\AccessUrl;
10+
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
11+
use Doctrine\DBAL\Schema\Schema;
12+
13+
final class Version20260118180100 extends AbstractMigrationChamilo
14+
{
15+
public function getDescription(): string
16+
{
17+
return 'Fix migrated tracking header/footer extra content settings: replace legacy .txt path with file content.';
18+
}
19+
20+
public function up(Schema $schema): void
21+
{
22+
$rows = $this->connection->fetchAllAssociative(
23+
"
24+
SELECT id, access_url, variable, selected_value
25+
FROM settings
26+
WHERE category = 'tracking'
27+
AND variable IN ('header_extra_content', 'footer_extra_content')
28+
"
29+
);
30+
31+
if (!$rows) {
32+
error_log('[MIGRATION] No tracking extra content settings found. Nothing to do.');
33+
return;
34+
}
35+
36+
$updateRoot = rtrim((string) $this->getUpdateRootPath(), DIRECTORY_SEPARATOR);
37+
error_log('[MIGRATION] Tracking extra content fix started.');
38+
error_log('[MIGRATION] Update root path: '.$updateRoot);
39+
40+
$accessUrlRepo = $this->entityManager->getRepository(AccessUrl::class);
41+
42+
foreach ($rows as $row) {
43+
$id = (int) ($row['id'] ?? 0);
44+
$accessUrlId = (int) ($row['access_url'] ?? 0);
45+
$variable = (string) ($row['variable'] ?? '');
46+
$value = trim((string) ($row['selected_value'] ?? ''));
47+
48+
if ($id <= 0 || $variable === '' || $value === '') {
49+
continue;
50+
}
51+
52+
// If it already looks like HTML, keep it.
53+
if (str_contains($value, '<')) {
54+
error_log("[MIGRATION] settings#$id ($variable): already HTML, skipped.");
55+
continue;
56+
}
57+
58+
// Only fix legacy file paths (typically "app/home/*.txt").
59+
if (!$this->looksLikeLegacyTxtPath($value)) {
60+
error_log("[MIGRATION] settings#$id ($variable): not a legacy .txt path, skipped.");
61+
continue;
62+
}
63+
64+
$host = '';
65+
if ($accessUrlId > 0) {
66+
$accessUrl = $accessUrlRepo->find($accessUrlId);
67+
if ($accessUrl && method_exists($accessUrl, 'getUrl')) {
68+
$host = (string) (parse_url((string) $accessUrl->getUrl(), PHP_URL_HOST) ?: '');
69+
}
70+
}
71+
72+
$fileContent = $this->readLegacyExtraContentFile($updateRoot, $value, $host);
73+
74+
if ($fileContent === null) {
75+
error_log("[MIGRATION] settings#$id ($variable): file not found for '$value'. Clearing setting.");
76+
$this->updateSettingValue($id, '');
77+
continue;
78+
}
79+
80+
$fileContent = trim($fileContent);
81+
82+
// Be conservative: header/footer snippets are injected as raw HTML in Twig.
83+
if ($fileContent === '' || !str_contains($fileContent, '<')) {
84+
error_log("[MIGRATION] settings#$id ($variable): file content is empty or not HTML. Clearing setting.");
85+
$this->updateSettingValue($id, '');
86+
continue;
87+
}
88+
89+
error_log("[MIGRATION] settings#$id ($variable): content loaded, updating setting.");
90+
$this->updateSettingValue($id, $fileContent);
91+
}
92+
93+
error_log('[MIGRATION] Tracking extra content fix completed.');
94+
}
95+
96+
public function down(Schema $schema): void
97+
{
98+
// Not reversible: we can't safely restore the original legacy file paths.
99+
}
100+
101+
private function updateSettingValue(int $id, string $value): void
102+
{
103+
$this->connection->update('settings', ['selected_value' => $value], ['id' => $id]);
104+
}
105+
106+
private function looksLikeLegacyTxtPath(string $value): bool
107+
{
108+
$v = strtolower(trim($value));
109+
110+
// Typical migrated values:
111+
// - app/home/header_extra_content.txt
112+
// - app/home/footer_extra_content.txt
113+
return str_ends_with($v, '.txt') && str_contains($v, 'app/home');
114+
}
115+
116+
private function readLegacyExtraContentFile(string $updateRoot, string $storedValue, string $host): ?string
117+
{
118+
$storedValue = trim($storedValue);
119+
120+
$basename = basename(str_replace('\\', '/', $storedValue));
121+
$relative = ltrim($storedValue, "/\\");
122+
$relative = str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $relative);
123+
124+
$candidates = [];
125+
126+
// 1) updateRoot + stored relative path (most common)
127+
$candidates[] = $updateRoot.DIRECTORY_SEPARATOR.$relative;
128+
129+
// 2) updateRoot/app/home/<host>/<basename> (some upgrades store files per URL)
130+
if ($host !== '') {
131+
$candidates[] = $updateRoot.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'home'.DIRECTORY_SEPARATOR.$host.DIRECTORY_SEPARATOR.$basename;
132+
}
133+
134+
// 3) updateRoot/app/home/<basename> (your local test case)
135+
$candidates[] = $updateRoot.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'home'.DIRECTORY_SEPARATOR.$basename;
136+
137+
foreach ($candidates as $path) {
138+
if (is_file($path) && is_readable($path)) {
139+
error_log('[MIGRATION] Reading extra content file: '.$path);
140+
$content = @file_get_contents($path);
141+
if ($content !== false) {
142+
return (string) $content;
143+
}
144+
}
145+
}
146+
147+
return null;
148+
}
149+
}

src/CoreBundle/Twig/Extension/ChamiloExtension.php

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,13 @@ public function getSettings($namespace): SettingsInterface
108108

109109
public function getSettingsParameter($name)
110110
{
111-
return $this->helper->getSettingsParameter($name);
111+
$value = $this->helper->getSettingsParameter($name);
112+
// We only want to inject valid HTML snippets here.
113+
if ($name === 'tracking.header_extra_content' || $name === 'tracking.footer_extra_content') {
114+
return $this->resolveTrackingExtraContentValue($name, $value);
115+
}
116+
117+
return $value;
112118
}
113119

114120
/**
@@ -276,4 +282,50 @@ public function getThemeAssetBase64Encoded(string $path): string
276282
{
277283
return $this->themeHelper->getAssetBase64Encoded($path);
278284
}
285+
286+
/**
287+
* Normalize legacy tracking extra content values.
288+
*
289+
* Expected value: an HTML snippet (e.g. <script>...</script>, <meta ...>, etc.)
290+
*
291+
* Problem on migrated portals:
292+
* - Legacy migration can store a filesystem path (e.g. "app/home/header_extra_content.txt")
293+
* - Rendering that value produces a visible "flash" of the path before navigation completes.
294+
*
295+
* Rule:
296+
* - Only allow HTML snippets (must contain "<").
297+
* - If the value looks like a file path / file reference, return empty string.
298+
* - If the value is plain text (no "<"), return empty string (do not inject as |raw).
299+
*/
300+
private function resolveTrackingExtraContentValue(string $settingName, mixed $value): string
301+
{
302+
if (!\in_array($settingName, ['tracking.header_extra_content', 'tracking.footer_extra_content'], true)) {
303+
return is_string($value) ? $value : '';
304+
}
305+
306+
if (!is_string($value)) {
307+
return '';
308+
}
309+
310+
$value = trim($value);
311+
if ($value === '') {
312+
return '';
313+
}
314+
315+
// Only allow HTML snippets. Anything else should not be injected with |raw.
316+
if (!str_contains($value, '<')) {
317+
return '';
318+
}
319+
320+
// Reject obvious file paths or filename references to avoid injecting legacy migrated values.
321+
// Examples: "app/home/header_extra_content.txt", "/var/www/...", "C:\path\file.txt"
322+
$looksLikePath = str_contains($value, '/') || str_contains($value, '\\');
323+
$looksLikeFile = (bool) preg_match('/\.[a-z0-9]{1,8}\b/i', $value);
324+
325+
if ($looksLikePath && $looksLikeFile) {
326+
return '';
327+
}
328+
329+
return $value;
330+
}
279331
}

0 commit comments

Comments
 (0)