xref: /template/mikio/css.php (revision 022c924719af4752f91f1a035611b996f5600a85)
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}