1<?php
2/**
3 * Mikio CSS/LESS Engine
4 *
5 * @link    http://dokuwiki.org/template:mikio
6 * @license GPLv2
7 * @author  James Collins
8 */
9
10require(__DIR__ . '/inc/polyfill-ctype.php');
11
12if (!class_exists('lessc')) {
13    require(__DIR__ . '/inc/stemmechanics/lesserphp/lessc.inc.php');
14}
15
16function logInvalidRequest($reason, $input) {
17    error_log("[css.php] $reason | input: $input | IP: " . ($_SERVER['REMOTE_ADDR'] ?? 'unknown'));
18}
19
20try {
21    if (!isset($_GET['css'])) {
22        http_response_code(404);
23        echo "The requested file could not be found";
24        exit;
25    }
26
27    $cssFileList = explode(',', $_GET['css']);
28    $pluginRoot = realpath(__DIR__ . '/');
29    $allowedDirs = [
30        realpath($pluginRoot . '/assets'),
31        realpath($pluginRoot . '/styles')
32    ];
33    $allowedExtensions = ['css', 'less'];
34    $css = '';
35    $failed = false;
36
37    foreach ($cssFileList as $rawInput) {
38        // Strip query/hash
39        $cleanInput = explode('?', $rawInput, 2)[0];
40        $cleanInput = trim(str_replace(['..', '\\'], '', $cleanInput));
41        if (empty($cleanInput)) {
42            $failed = true;
43            logInvalidRequest("Empty or invalid path", $rawInput);
44            continue;
45        }
46
47        $resolvedPath = realpath($pluginRoot . '/' . $cleanInput);
48        $ext = pathinfo($resolvedPath, PATHINFO_EXTENSION);
49
50        if (
51            !$resolvedPath ||
52            !file_exists($resolvedPath) ||
53            !in_array($ext, $allowedExtensions, true)
54        ) {
55            $failed = true;
56            logInvalidRequest("Invalid file or extension", $rawInput);
57            continue;
58        }
59
60        // Confirm file is within allowed directories
61        $allowed = false;
62        foreach ($allowedDirs as $dir) {
63            if (strpos($resolvedPath, $dir) === 0) {
64                $allowed = true;
65                break;
66            }
67        }
68
69        if (!$allowed) {
70            $failed = true;
71            logInvalidRequest("File outside allowed directory", $rawInput);
72            continue;
73        }
74
75        $css .= file_get_contents($resolvedPath);
76    }
77
78    if ($failed) {
79        http_response_code(404);
80        echo "The requested file could not be found";
81        exit;
82    }
83
84    header('Content-Type: text/css; charset=utf-8');
85
86    $less = new lessc();
87    $less->setPreserveComments(false);
88
89    // Optional variables (future-proofed)
90    $rawVars = [];
91    $vars = [];
92    if (isset($rawVars['replacements'])) {
93        foreach ($rawVars['replacements'] as $key => $val) {
94            if (strpos($key, '__') === 0 && substr($key, -2) === '__') {
95                $vars['ini_' . substr($key, 2, -2)] = $val;
96            }
97        }
98    }
99
100    if (!empty($vars)) {
101        $less->setVariables($vars);
102    }
103
104    echo $less->compile($css);
105
106} catch (Exception $e) {
107    error_log("[css.php] Exception: " . $e->getMessage());
108    http_response_code(500);
109    header('Content-Type: text/css; charset=utf-8');
110    echo "/* An error occurred while processing the CSS. */";
111}