1<?php /** @noinspection DuplicatedCode */ 2/** 3 * Mikio CSS/LESS Engine (hardened) 4 * @link http://dokuwiki.org/template:mikio 5 * @license GPLv2 6 * @author James Collins 7 */ 8 9require(__DIR__ . '/inc/polyfill-ctype.php'); 10 11if (!class_exists('lessc')) { 12 require(__DIR__ . '/inc/stemmechanics/lesserphp/lessc.inc.php'); 13} 14 15function logInvalidRequest($reason, $input) { 16 error_log("[mikio css.php] $reason | input: $input | IP: " . ($_SERVER['REMOTE_ADDR'] ?? 'unknown')); 17} 18 19function arrayDeepMerge($arr1, $arr2) { 20 foreach ($arr2 as $key => $value) { 21 if (isset($arr1[$key]) && is_array($arr1[$key]) && is_array($value)) { 22 $arr1[$key] = arrayDeepMerge($arr1[$key], $value); 23 } else { 24 $arr1[$key] = $value; 25 } 26 } 27 return $arr1; 28} 29 30try { 31 if (!isset($_GET['css'])) { 32 http_response_code(404); 33 echo "The requested file could not be found"; 34 exit; 35 } 36 37 $themeRoot = realpath(__DIR__ . '/'); 38 if ($themeRoot === false) { 39 throw new RuntimeException('Theme root resolution failed'); 40 } 41 42 // Only allow CSS/LESS inside these theme subdirectories 43 $allowedDirs = [ 44 realpath($themeRoot . '/assets'), 45 realpath($themeRoot . '/styles'), 46 realpath($themeRoot . '/css'), 47 ]; 48 $allowedExtensions = ['css', 'less']; 49 50 $css = ''; 51 $failed = false; 52 53 // Support a comma-separated list like your plugin fix 54 $cssFileList = explode(',', $_GET['css']); 55 56 foreach ($cssFileList as $rawInput) { 57 // Strip query/hash and basic traversal chars 58 $clean = explode('?', $rawInput, 2)[0]; 59 $clean = trim(str_replace(['..', '\\'], '', $clean)); 60 61 if ($clean === '') { 62 $failed = true; 63 logInvalidRequest('Empty or invalid path', $rawInput); 64 continue; 65 } 66 67 $resolved = realpath($themeRoot . '/' . ltrim($clean, '/')); 68 if (!$resolved || !is_file($resolved)) { 69 $failed = true; 70 logInvalidRequest('Invalid file path', $rawInput); 71 continue; 72 } 73 74 $ext = strtolower(pathinfo($resolved, PATHINFO_EXTENSION)); 75 if (!in_array($ext, $allowedExtensions, true)) { 76 $failed = true; 77 logInvalidRequest('Disallowed extension', $rawInput); 78 continue; 79 } 80 81 // Enforce allowed directories 82 $inside = false; 83 foreach ($allowedDirs as $dir) { 84 if ($dir && strpos($resolved, $dir) === 0) { 85 $inside = true; 86 break; 87 } 88 } 89 if (!$inside) { 90 $failed = true; 91 logInvalidRequest('File outside allowed directories', $rawInput); 92 continue; 93 } 94 95 $css .= file_get_contents($resolved); 96 } 97 98 if ($failed) { 99 http_response_code(404); 100 echo "The requested file could not be found"; 101 exit; 102 } 103 104 // Load style.ini replacements from trusted locations 105 $rawVars = []; 106 $iniCandidates = [ 107 $themeRoot . '/style.ini', 108 dirname($themeRoot, 3) . '/conf/tpl/mikio/style.ini' ?: null, 109 (isset($_SERVER['DOCUMENT_ROOT']) ? ($_SERVER['DOCUMENT_ROOT'] . '/conf/tpl/mikio/style.ini') : null), 110 ]; 111 112 foreach ($iniCandidates as $ini) { 113 if ($ini && is_file($ini)) { 114 $parsed = @parse_ini_file($ini, true); 115 if (is_array($parsed)) { 116 $rawVars = arrayDeepMerge($rawVars, $parsed); 117 } 118 } 119 } 120 121 header('Content-Type: text/css; charset=utf-8'); 122 123 $less = new lessc(); 124 $less->setPreserveComments(false); 125 126 // Map __FOO__ => ini_FOO variables just like before 127 $vars = []; 128 if (isset($rawVars['replacements']) && is_array($rawVars['replacements'])) { 129 foreach ($rawVars['replacements'] as $k => $v) { 130 if (strpos($k, '__') === 0 && substr($k, -2) === '__') { 131 $vars['ini_' . substr($k, 2, -2)] = $v; 132 } 133 } 134 } 135 if ($vars) { 136 $less->setVariables($vars); 137 } 138 139 echo $less->compile($css); 140 141} catch (Throwable $e) { 142 // Log server-side; no path/stack to client 143 error_log('[mikio css.php] Exception: ' . $e->getMessage()); 144 http_response_code(500); 145 header('Content-Type: text/css; charset=utf-8'); 146 echo "/* An error occurred while processing the CSS. */"; 147}