1*022c9247SJames Collins<?php /** @noinspection DuplicatedCode */ 2ab45ba71SJames Collins/** 3*022c9247SJames Collins * Mikio CSS/LESS Engine (hardened) 4ab45ba71SJames Collins * @link http://dokuwiki.org/template:mikio 57261b213SJames Collins * @license GPLv2 67261b213SJames Collins * @author James Collins 7ab45ba71SJames Collins */ 8d24f6ec2SJames Collins 97261b213SJames Collinsrequire(__DIR__ . '/inc/polyfill-ctype.php'); 10692c64c6SJames Collins 117a37170aSJames Collinsif (!class_exists('lessc')) { 127261b213SJames Collins require(__DIR__ . '/inc/stemmechanics/lesserphp/lessc.inc.php'); 137a37170aSJames Collins} 147a37170aSJames Collins 157261b213SJames Collinsfunction logInvalidRequest($reason, $input) { 16*022c9247SJames Collins error_log("[mikio css.php] $reason | input: $input | IP: " . ($_SERVER['REMOTE_ADDR'] ?? 'unknown')); 17*022c9247SJames Collins} 18*022c9247SJames Collins 19*022c9247SJames Collinsfunction arrayDeepMerge($arr1, $arr2) { 20*022c9247SJames Collins foreach ($arr2 as $key => $value) { 21*022c9247SJames Collins if (isset($arr1[$key]) && is_array($arr1[$key]) && is_array($value)) { 22*022c9247SJames Collins $arr1[$key] = arrayDeepMerge($arr1[$key], $value); 23*022c9247SJames Collins } else { 24*022c9247SJames Collins $arr1[$key] = $value; 25*022c9247SJames Collins } 26*022c9247SJames Collins } 27*022c9247SJames Collins return $arr1; 28fd3ef33aSJames Collins} 29fd3ef33aSJames Collins 30ab45ba71SJames Collinstry { 317261b213SJames Collins if (!isset($_GET['css'])) { 327261b213SJames Collins http_response_code(404); 337261b213SJames Collins echo "The requested file could not be found"; 347261b213SJames Collins exit; 358ddabb4eSJames Collins } 36ab45ba71SJames Collins 37*022c9247SJames Collins $themeRoot = realpath(__DIR__ . '/'); 38*022c9247SJames Collins if ($themeRoot === false) { 39*022c9247SJames Collins throw new RuntimeException('Theme root resolution failed'); 40*022c9247SJames Collins } 41*022c9247SJames Collins 42*022c9247SJames Collins // Only allow CSS/LESS inside these theme subdirectories 437261b213SJames Collins $allowedDirs = [ 44*022c9247SJames Collins realpath($themeRoot . '/assets'), 45*022c9247SJames Collins realpath($themeRoot . '/styles'), 46*022c9247SJames Collins realpath($themeRoot . '/css'), 477261b213SJames Collins ]; 487261b213SJames Collins $allowedExtensions = ['css', 'less']; 49*022c9247SJames Collins 507261b213SJames Collins $css = ''; 517261b213SJames Collins $failed = false; 527261b213SJames Collins 53*022c9247SJames Collins // Support a comma-separated list like your plugin fix 54*022c9247SJames Collins $cssFileList = explode(',', $_GET['css']); 55*022c9247SJames Collins 567261b213SJames Collins foreach ($cssFileList as $rawInput) { 57*022c9247SJames Collins // Strip query/hash and basic traversal chars 58*022c9247SJames Collins $clean = explode('?', $rawInput, 2)[0]; 59*022c9247SJames Collins $clean = trim(str_replace(['..', '\\'], '', $clean)); 60*022c9247SJames Collins 61*022c9247SJames Collins if ($clean === '') { 627261b213SJames Collins $failed = true; 63*022c9247SJames Collins logInvalidRequest('Empty or invalid path', $rawInput); 647261b213SJames Collins continue; 658ddabb4eSJames Collins } 66ab45ba71SJames Collins 67*022c9247SJames Collins $resolved = realpath($themeRoot . '/' . ltrim($clean, '/')); 68*022c9247SJames Collins if (!$resolved || !is_file($resolved)) { 697261b213SJames Collins $failed = true; 70*022c9247SJames Collins logInvalidRequest('Invalid file path', $rawInput); 717261b213SJames Collins continue; 728ddabb4eSJames Collins } 73692c64c6SJames Collins 74*022c9247SJames Collins $ext = strtolower(pathinfo($resolved, PATHINFO_EXTENSION)); 75*022c9247SJames Collins if (!in_array($ext, $allowedExtensions, true)) { 76*022c9247SJames Collins $failed = true; 77*022c9247SJames Collins logInvalidRequest('Disallowed extension', $rawInput); 78*022c9247SJames Collins continue; 79*022c9247SJames Collins } 80*022c9247SJames Collins 81*022c9247SJames Collins // Enforce allowed directories 82*022c9247SJames Collins $inside = false; 837261b213SJames Collins foreach ($allowedDirs as $dir) { 84*022c9247SJames Collins if ($dir && strpos($resolved, $dir) === 0) { 85*022c9247SJames Collins $inside = true; 867261b213SJames Collins break; 877261b213SJames Collins } 887261b213SJames Collins } 89*022c9247SJames Collins if (!$inside) { 907261b213SJames Collins $failed = true; 91*022c9247SJames Collins logInvalidRequest('File outside allowed directories', $rawInput); 927261b213SJames Collins continue; 937261b213SJames Collins } 947261b213SJames Collins 95*022c9247SJames Collins $css .= file_get_contents($resolved); 967261b213SJames Collins } 977261b213SJames Collins 987261b213SJames Collins if ($failed) { 997261b213SJames Collins http_response_code(404); 1007261b213SJames Collins echo "The requested file could not be found"; 1017261b213SJames Collins exit; 1027261b213SJames Collins } 103ab45ba71SJames Collins 104*022c9247SJames Collins // Load style.ini replacements from trusted locations 105*022c9247SJames Collins $rawVars = []; 106*022c9247SJames Collins $iniCandidates = [ 107*022c9247SJames Collins $themeRoot . '/style.ini', 108*022c9247SJames Collins dirname($themeRoot, 3) . '/conf/tpl/mikio/style.ini' ?: null, 109*022c9247SJames Collins (isset($_SERVER['DOCUMENT_ROOT']) ? ($_SERVER['DOCUMENT_ROOT'] . '/conf/tpl/mikio/style.ini') : null), 110*022c9247SJames Collins ]; 111*022c9247SJames Collins 112*022c9247SJames Collins foreach ($iniCandidates as $ini) { 113*022c9247SJames Collins if ($ini && is_file($ini)) { 114*022c9247SJames Collins $parsed = @parse_ini_file($ini, true); 115*022c9247SJames Collins if (is_array($parsed)) { 116*022c9247SJames Collins $rawVars = arrayDeepMerge($rawVars, $parsed); 117*022c9247SJames Collins } 118*022c9247SJames Collins } 119*022c9247SJames Collins } 120*022c9247SJames Collins 121e876e764SJames Collins header('Content-Type: text/css; charset=utf-8'); 122e876e764SJames Collins 123ab45ba71SJames Collins $less = new lessc(); 124ab45ba71SJames Collins $less->setPreserveComments(false); 125a8eebd82SJames Collins 126*022c9247SJames Collins // Map __FOO__ => ini_FOO variables just like before 1277261b213SJames Collins $vars = []; 128*022c9247SJames Collins if (isset($rawVars['replacements']) && is_array($rawVars['replacements'])) { 129*022c9247SJames Collins foreach ($rawVars['replacements'] as $k => $v) { 130*022c9247SJames Collins if (strpos($k, '__') === 0 && substr($k, -2) === '__') { 131*022c9247SJames Collins $vars['ini_' . substr($k, 2, -2)] = $v; 132a8eebd82SJames Collins } 133a8eebd82SJames Collins } 134a8eebd82SJames Collins } 135*022c9247SJames Collins if ($vars) { 136a8eebd82SJames Collins $less->setVariables($vars); 137a8eebd82SJames Collins } 138a8eebd82SJames Collins 1397261b213SJames Collins echo $less->compile($css); 1407261b213SJames Collins 141*022c9247SJames Collins} catch (Throwable $e) { 142*022c9247SJames Collins // Log server-side; no path/stack to client 143*022c9247SJames Collins error_log('[mikio css.php] Exception: ' . $e->getMessage()); 1447261b213SJames Collins http_response_code(500); 145692c64c6SJames Collins header('Content-Type: text/css; charset=utf-8'); 1467261b213SJames Collins echo "/* An error occurred while processing the CSS. */"; 1478ddabb4eSJames Collins}