diff --git a/library/language/source/main.php b/library/language/source/main.php
index 5228ede0b..aed625dad 100644
--- a/library/language/source/main.php
+++ b/library/language/source/main.php
@@ -1962,6 +1962,7 @@ $lang['MIGRATIONS_APPLIED'] = 'Applied Migrations';
$lang['MIGRATIONS_PENDING'] = 'Pending Migrations';
$lang['MIGRATIONS_VERSION'] = 'Version';
$lang['MIGRATIONS_NAME'] = 'Migration Name';
+$lang['MIGRATIONS_FILE'] = 'Migration File';
$lang['MIGRATIONS_APPLIED_AT'] = 'Applied At';
$lang['MIGRATIONS_COMPLETED_AT'] = 'Completed At';
$lang['MIGRATIONS_CURRENT_VERSION'] = 'Current Version';
diff --git a/src/Legacy/Template.php b/src/Legacy/Template.php
index a65e3b81a..83ec25199 100644
--- a/src/Legacy/Template.php
+++ b/src/Legacy/Template.php
@@ -421,7 +421,7 @@ class Template
// Append the variable reference.
$varref .= "['$varname']";
- $varref = "";
+ $varref = "";
return $varref;
}
@@ -766,7 +766,7 @@ class Template
$code = str_replace($search, $replace, $code);
}
// This will handle the remaining root-level varrefs
- $code = preg_replace('#\{(L_([a-z0-9\-_]+?))\}#i', '', $code);
+ $code = preg_replace('#\{(L_([a-z0-9\-_]+?))\}#i', '', $code);
$code = preg_replace('#\{(\$[a-z_][a-z0-9_$\->\'\"\.\[\]]*?)\}#i', '', $code);
$code = preg_replace('#\{(\#([a-z_][a-z0-9_]*?)\#)\}#i', '', $code);
$code = preg_replace('#\{([a-z0-9\-_]+?)\}#i', '', $code);
diff --git a/tests/Unit/Legacy/TemplateGracefulFallbackTest.php b/tests/Unit/Legacy/TemplateGracefulFallbackTest.php
new file mode 100644
index 000000000..ff57a55b3
--- /dev/null
+++ b/tests/Unit/Legacy/TemplateGracefulFallbackTest.php
@@ -0,0 +1,501 @@
+', '|', ' '];
+ return str_replace($s, '_', trim($fname));
+ }
+ }
+
+ if (!function_exists('config')) {
+ function config()
+ {
+ return new class {
+ public function get($key, $default = null)
+ {
+ // Return sensible defaults for template configuration
+ return match ($key) {
+ 'xs_use_cache' => 0,
+ 'default_lang' => 'en',
+ default => $default
+ };
+ }
+ };
+ }
+ }
+
+ // Create a temporary directory for templates and cache
+ $this->tempDir = createTempDirectory();
+ $this->templateDir = $this->tempDir . '/templates';
+ $this->cacheDir = $this->tempDir . '/cache';
+
+ mkdir($this->templateDir, 0755, true);
+ mkdir($this->cacheDir, 0755, true);
+
+ // Set up global language array for testing
+ global $lang;
+ $lang = [
+ 'EXISTING_KEY' => 'This key exists',
+ 'ANOTHER_KEY' => 'Another existing key'
+ ];
+
+ // Create template instance
+ $this->template = new Template($this->templateDir);
+ $this->template->cachedir = $this->cacheDir . '/';
+ $this->template->use_cache = 0; // Disable caching for tests
+});
+
+afterEach(function () {
+ // Clean up
+ if (isset($this->tempDir)) {
+ removeTempDirectory($this->tempDir);
+ }
+
+ // Reset global state
+ resetGlobalState();
+});
+
+/**
+ * Execute a compiled template and return its output
+ *
+ * @param string $compiled The compiled template code
+ * @param array $variables Optional variables to set in scope (V array)
+ * @param array $additionalVars Optional additional variables to set in scope
+ * @return string The template output
+ */
+function executeTemplate(string $compiled, array $variables = [], array $additionalVars = []): string
+{
+ ob_start();
+ global $lang;
+ $L = &$lang;
+ $V = $variables;
+
+ // Set any additional variables in scope
+ foreach ($additionalVars as $name => $value) {
+ $$name = $value;
+ }
+
+ // SECURITY NOTE: eval() is used intentionally here to execute compiled template code
+ // within a controlled test environment. While eval() poses security risks in production,
+ // its use is justified in this specific unit test scenario because:
+ // 1. We're testing the legacy template compilation system that generates PHP code
+ // 2. The input is controlled and comes from our own template compiler
+ // 3. This runs in an isolated test environment, not production
+ // 4. Testing the actual execution is necessary to verify template output correctness
+ // Future maintainers: Use extreme caution with eval() and avoid it in production code
+ eval('?>' . $compiled);
+ return ob_get_clean();
+}
+
+describe('Template Text Compilation - Graceful Fallback', function () {
+
+ it('shows missing language variables as original syntax', function () {
+ $template = '{L_MISSING_KEY}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled);
+
+ expect($output)->toBe('L_MISSING_KEY');
+ });
+
+ it('shows existing language variables correctly', function () {
+ $template = '{L_EXISTING_KEY}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled);
+
+ expect($output)->toBe('This key exists');
+ });
+
+ it('shows missing regular variables as original syntax', function () {
+ $template = '{MISSING_VAR}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled);
+
+ expect($output)->toBe('');
+ });
+
+ it('shows existing regular variables correctly', function () {
+ $template = '{EXISTING_VAR}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, ['EXISTING_VAR' => 'This variable exists']);
+
+ expect($output)->toBe('This variable exists');
+ });
+
+ it('shows missing constants as original syntax', function () {
+ $template = '{#MISSING_CONSTANT#}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled);
+
+ expect($output)->toBe('');
+ });
+
+ it('shows existing constants correctly', function () {
+ // Define a test constant
+ if (!defined('TEST_CONSTANT')) {
+ define('TEST_CONSTANT', 'This constant exists');
+ }
+
+ $template = '{#TEST_CONSTANT#}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled);
+
+ expect($output)->toBe('This constant exists');
+ });
+
+ it('handles mixed existing and missing variables correctly', function () {
+ $template = '{L_EXISTING_KEY} - {L_MISSING_KEY} - {EXISTING_VAR} - {MISSING_VAR}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, ['EXISTING_VAR' => 'Variable exists']);
+
+ expect($output)->toBe('This key exists - L_MISSING_KEY - Variable exists - ');
+ });
+
+ it('handles PHP variables correctly without fallback', function () {
+ $template = '{$test_var}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, [], ['test_var' => 'PHP variable value']);
+
+ expect($output)->toBe('PHP variable value');
+ });
+
+ it('handles undefined PHP variables gracefully', function () {
+ $template = '{$undefined_var}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled);
+
+ // PHP variables that don't exist should show empty string (original behavior)
+ expect($output)->toBe('');
+ });
+
+});
+
+describe('Template Block Variable Fallback', function () {
+
+ it('shows missing block variables as original syntax', function () {
+ $namespace = 'testblock';
+ $varname = 'MISSING_VAR';
+
+ $result = $this->template->generate_block_varref($namespace . '.', $varname);
+
+ // Verify the exact expected fallback output format string
+ $expectedFormat = "";
+ expect($result)->toBe($expectedFormat);
+ });
+
+ it('generates correct PHP code for block variable fallback', function () {
+ $namespace = 'news';
+ $varname = 'TITLE';
+
+ $result = $this->template->generate_block_varref($namespace . '.', $varname);
+
+ // Verify the exact expected fallback output format string
+ $expectedFormat = "";
+ expect($result)->toBe($expectedFormat);
+ });
+
+});
+
+describe('Compiled Code Verification', function () {
+
+ it('compiles language variables with proper fallback code', function () {
+ $template = '{L_MISSING_KEY}';
+ $compiled = $this->template->_compile_text($template);
+
+ // Verify the compiled PHP code contains the expected fallback logic
+ expect($compiled)->toContain("isset(\$L['MISSING_KEY'])");
+ expect($compiled)->toContain("'L_MISSING_KEY'");
+ });
+
+ it('compiles regular variables with proper fallback code', function () {
+ $template = '{MISSING_VAR}';
+ $compiled = $this->template->_compile_text($template);
+
+ // Verify the compiled PHP code contains the expected fallback logic
+ expect($compiled)->toContain("isset(\$V['MISSING_VAR'])");
+ expect($compiled)->toContain("''");
+ });
+
+ it('compiles constants with proper fallback code', function () {
+ $template = '{#MISSING_CONSTANT#}';
+ $compiled = $this->template->_compile_text($template);
+
+ // Verify the compiled PHP code contains the expected fallback logic
+ expect($compiled)->toContain("defined('MISSING_CONSTANT')");
+ expect($compiled)->toContain("''");
+ });
+
+});
+
+describe('Real-world Example - Admin Migrations', function () {
+
+ it('handles the original L_MIGRATIONS_FILE error gracefully', function () {
+ // The exact template that was causing the error
+ $template = '
{L_MIGRATIONS_FILE} | ';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled);
+
+ // Should show the fallback without braces instead of throwing an error
+ expect($output)->toContain('L_MIGRATIONS_FILE');
+ expect($output)->toContain('template->_compile_text($template);
+ $output = executeTemplate($compiled);
+
+ // Empty braces should remain as literal text
+ expect($output)->toBe('{}');
+ });
+
+ it('handles variables with special characters in names', function () {
+ $template = '{VAR_WITH_UNDERSCORES} {VAR-WITH-DASHES} {VAR123NUMBERS}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, [
+ 'VAR_WITH_UNDERSCORES' => 'underscore value',
+ 'VAR123NUMBERS' => 'number value'
+ ]);
+
+ // Verify the compiled code contains proper fallback logic for special chars
+ expect($compiled)->toContain("isset(\$V['VAR_WITH_UNDERSCORES'])");
+ expect($compiled)->toContain("isset(\$V['VAR123NUMBERS'])");
+
+ // Underscores and numbers should work, dashes might not be valid variable names
+ expect($output)->toContain('underscore value');
+ expect($output)->toContain('number value');
+ });
+
+ it('handles HTML entities and special characters in template content', function () {
+ $template = ' & {TEST_VAR} <script> ';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, ['TEST_VAR' => 'safe content']);
+
+ // HTML entities should be preserved, variable should be substituted
+ expect($output)->toBe('& safe content <script> ');
+
+ // Verify fallback logic is present
+ expect($compiled)->toContain("isset(\$V['TEST_VAR'])");
+ });
+
+ it('handles quotes and escaping in variable values', function () {
+ $template = 'Value: {QUOTED_VAR}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, [
+ 'QUOTED_VAR' => 'Contains "quotes" and \'apostrophes\''
+ ]);
+
+ expect($output)->toBe('Value: Contains "quotes" and \'apostrophes\'');
+ expect($compiled)->toContain("isset(\$V['QUOTED_VAR'])");
+ });
+
+ it('handles very long variable names', function () {
+ $longVarName = 'VERY_LONG_VARIABLE_NAME_THAT_TESTS_BUFFER_LIMITS_AND_PARSING_' . str_repeat('X', 100);
+ $template = '{' . $longVarName . '}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, [$longVarName => 'long var value']);
+
+ expect($output)->toBe('long var value');
+ expect($compiled)->toContain("isset(\$V['$longVarName'])");
+ });
+
+ it('handles nested braces and malformed syntax', function () {
+ $template = '{{NESTED}} {UNCLOSED {NORMAL_VAR} }EXTRA}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, ['NORMAL_VAR' => 'works']);
+
+ // Should handle the valid variable and leave malformed parts as literals
+ expect($output)->toContain('works');
+ expect($compiled)->toContain("isset(\$V['NORMAL_VAR'])");
+ });
+
+ it('handles empty string values with proper fallback', function () {
+ $template = 'Before:{EMPTY_VAR}:After';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, ['EMPTY_VAR' => '']);
+
+ expect($output)->toBe('Before::After');
+ expect($compiled)->toContain("isset(\$V['EMPTY_VAR'])");
+ });
+
+ it('handles null and false values correctly', function () {
+ $template = 'Null:{NULL_VAR} False:{FALSE_VAR} Zero:{ZERO_VAR}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, [
+ 'NULL_VAR' => null,
+ 'FALSE_VAR' => false,
+ 'ZERO_VAR' => 0
+ ]);
+
+ // PHP's string conversion: null='', false='', 0='0'
+ expect($output)->toBe('Null: False: Zero:0');
+ expect($compiled)->toContain("isset(\$V['NULL_VAR'])");
+ expect($compiled)->toContain("isset(\$V['FALSE_VAR'])");
+ expect($compiled)->toContain("isset(\$V['ZERO_VAR'])");
+ });
+
+ it('handles whitespace around variable names', function () {
+ $template = '{ SPACED_VAR } {NORMAL_VAR}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, [
+ 'SPACED_VAR' => 'should not work',
+ 'NORMAL_VAR' => 'should work'
+ ]);
+
+ // Spaces inside braces should make it not match as a variable pattern
+ expect($output)->toContain('should work');
+ expect($compiled)->toContain("isset(\$V['NORMAL_VAR'])");
+ });
+
+ it('handles multiple consecutive variables', function () {
+ $template = '{VAR1}{VAR2}{VAR3}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, [
+ 'VAR1' => 'A',
+ 'VAR2' => 'B',
+ 'VAR3' => 'C'
+ ]);
+
+ expect($output)->toBe('ABC');
+ expect($compiled)->toContain("isset(\$V['VAR1'])");
+ expect($compiled)->toContain("isset(\$V['VAR2'])");
+ expect($compiled)->toContain("isset(\$V['VAR3'])");
+ });
+
+ it('handles variables with numeric suffixes', function () {
+ $template = '{VAR1} {VAR2} {VAR10} {VAR100}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, [
+ 'VAR1' => 'one',
+ 'VAR2' => 'two',
+ 'VAR10' => 'ten',
+ 'VAR100' => 'hundred'
+ ]);
+
+ expect($output)->toBe('one two ten hundred');
+ expect($compiled)->toContain("isset(\$V['VAR1'])");
+ expect($compiled)->toContain("isset(\$V['VAR2'])");
+ expect($compiled)->toContain("isset(\$V['VAR10'])");
+ expect($compiled)->toContain("isset(\$V['VAR100'])");
+ });
+
+ it('handles mixed case sensitivity correctly', function () {
+ $template = '{lowercase} {UPPERCASE} {MixedCase}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, [
+ 'lowercase' => 'lower',
+ 'UPPERCASE' => 'upper',
+ 'MixedCase' => 'mixed'
+ ]);
+
+ expect($output)->toBe('lower upper mixed');
+ expect($compiled)->toContain("isset(\$V['lowercase'])");
+ expect($compiled)->toContain("isset(\$V['UPPERCASE'])");
+ expect($compiled)->toContain("isset(\$V['MixedCase'])");
+ });
+
+ it('handles language variables with special prefixes', function () {
+ global $lang;
+ $originalLang = $lang;
+
+ // Add some special test language variables
+ $lang['TEST_SPECIAL_CHARS'] = 'Special: &<>"\'';
+ $lang['TEST_UNICODE'] = 'Unicode: ñáéíóú';
+
+ $template = '{L_TEST_SPECIAL_CHARS} | {L_TEST_UNICODE} | {L_MISSING_SPECIAL}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled);
+
+ expect($output)->toBe('Special: &<>"\' | Unicode: ñáéíóú | L_MISSING_SPECIAL');
+ expect($compiled)->toContain("isset(\$L['TEST_SPECIAL_CHARS'])");
+ expect($compiled)->toContain("isset(\$L['TEST_UNICODE'])");
+ expect($compiled)->toContain("'L_MISSING_SPECIAL'");
+
+ // Restore original language array
+ $lang = $originalLang;
+ });
+
+ it('handles constants with edge case names', function () {
+ // Define some test constants with edge case names
+ if (!defined('TEST_CONST_123')) {
+ define('TEST_CONST_123', 'numeric suffix');
+ }
+ if (!defined('TEST_CONST_UNDERSCORE_')) {
+ define('TEST_CONST_UNDERSCORE_', 'trailing underscore');
+ }
+
+ $template = '{#TEST_CONST_123#} {#TEST_CONST_UNDERSCORE_#} {#UNDEFINED_CONST_EDGE#}';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled);
+
+ expect($output)->toBe('numeric suffix trailing underscore ');
+ expect($compiled)->toContain("defined('TEST_CONST_123')");
+ expect($compiled)->toContain("defined('TEST_CONST_UNDERSCORE_')");
+ expect($compiled)->toContain("defined('UNDEFINED_CONST_EDGE')");
+ });
+
+ it('handles complex nested HTML with variables', function () {
+ $template = '';
+ $compiled = $this->template->_compile_text($template);
+ $output = executeTemplate($compiled, [
+ 'CELL1' => 'First Cell',
+ 'CSS_CLASS' => 'highlight',
+ 'CELL2' => 'Second Cell'
+ ]);
+
+ expect($output)->toBe('');
+ expect($compiled)->toContain("isset(\$V['CELL1'])");
+ expect($compiled)->toContain("isset(\$V['CSS_CLASS'])");
+ expect($compiled)->toContain("isset(\$V['CELL2'])");
+ });
+
+});
|