diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index b30a221b6..1bd78a0f2 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -44,7 +44,7 @@ jobs: php-version: '8.1' - name: Update composer.lock file - run: composer update --no-install + run: composer update --no-dev --no-install - name: Install Composer dependencies run: composer install --no-dev --no-progress --prefer-dist --optimize-autoloader diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8d53cc4a..0a23691a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: php-version: '8.1' - name: Update composer.lock file - run: composer update --no-install + run: composer update --no-dev --no-install - name: Install Composer dependencies 🪚 run: composer install --no-dev --no-progress --prefer-dist --optimize-autoloader @@ -60,7 +60,7 @@ jobs: php-version: '8.1' - name: Update composer.lock file - run: composer update --no-install + run: composer update --no-dev --no-install - name: 🖇 Install Composer dependencies run: composer install --no-dev --no-progress --prefer-dist --optimize-autoloader diff --git a/admin/stats/tr_stats.php b/admin/stats/tr_stats.php index 4cd8733c3..db1fc444d 100644 --- a/admin/stats/tr_stats.php +++ b/admin/stats/tr_stats.php @@ -31,7 +31,8 @@ echo '
'; echo '{$lang['TR_STATS'][$i]} | $row | "; } diff --git a/install.php b/install.php index 0db88cba8..4064798fb 100644 --- a/install.php +++ b/install.php @@ -160,14 +160,14 @@ if (!is_file(BB_ROOT . 'vendor/autoload.php')) { out("- Installing production dependencies only...\n", 'info'); } - runProcess('php ' . BB_ROOT . 'composer.phar update --no-install'); - sleep(3); - $composerFlags = '--no-interaction --no-ansi'; if (!$isDeveloper) { $composerFlags .= ' --no-dev'; } + runProcess('php ' . BB_ROOT . 'composer.phar update --no-install ' . $composerFlags); + sleep(3); + runProcess('php ' . BB_ROOT . 'composer.phar install ' . $composerFlags); define('COMPOSER_COMPLETED', true); } else { 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 = '
|