xref: /dokuwiki/inc/PrefCookie.php (revision 3f108b378b26a51a8deb0e3ec08ce3ed6342d5af)
1<?php
2
3namespace dokuwiki;
4
5/**
6 * The preference cookie is used to store small user preference data
7 *
8 * The cookie is written from PHP (using this class) and from JavaScript (using the DokuCookie object).
9 *
10 * Data is stored as key#value#key#value string, with all keys and values being urlencoded
11 */
12class PrefCookie
13{
14    const COOKIENAME = 'DOKU_PREFS';
15
16    /** @var string[] */
17    protected array $data = [];
18
19    /**
20     * Initialize the class from the cookie data
21     */
22    public function __construct()
23    {
24        $this->data = $this->decodeData($_COOKIE[self::COOKIENAME] ?? '');
25    }
26
27    /**
28     * Get a preference from the cookie
29     *
30     * @param string $pref The preference to read
31     * @param mixed $default The default to return if no preference is set
32     * @return mixed
33     */
34    public function get(string $pref, $default = null)
35    {
36        return $this->data[$pref] ?? $default;
37    }
38
39    /**
40     * Set a preference
41     *
42     * This will trigger a setCookie header and needs to be called before any output is sent
43     *
44     * @param string $pref The preference to set
45     * @param string|null $value The value to set. Null to delete a value
46     * @return void
47     */
48    public function set(string $pref, ?string $value): void
49    {
50        if ($value === null) {
51            if (isset($this->data[$pref])) {
52                unset($this->data[$pref]);
53            }
54        } else {
55            $this->data[$pref] = $value;
56        }
57
58        $this->sendCookie();
59    }
60
61    /**
62     * Set the cookie header
63     *
64     * @return void
65     */
66    protected function sendCookie(): void
67    {
68        global $conf;
69
70        ksort($this->data); // sort by key
71        $olddata = $_COOKIE[self::COOKIENAME] ?? '';
72        $newdata = self::encodeData($this->data);
73
74        // no need to set a cookie when it's the same as before
75        if ($olddata == $newdata) return;
76
77        // update the cookie data for the current request
78        $_COOKIE[self::COOKIENAME] = $newdata;
79
80        // no cookies to set when running on CLI
81        if (PHP_SAPI === 'cli') return;
82
83        // set the cookie header
84        setcookie(self::COOKIENAME, $newdata, [
85            'expires' => time() + 365 * 24 * 3600,
86            'path' => empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'],
87            'secure' => ($conf['securecookie'] && Ip::isSsl()),
88            'samesite' => 'Lax'
89        ]);
90    }
91
92    /**
93     * Decode the cookie data (if any)
94     *
95     * @return array the cookie data as associative array
96     */
97    protected function decodeData(string $rawdata): array
98    {
99        $data = [];
100        if($rawdata === '') return $data;
101        $parts = explode('#', $rawdata);
102        $count = count($parts);
103
104        for ($i = 0; $i < $count; $i += 2) {
105            if (!isset($parts[$i + 1])) {
106                Logger::error('Odd entries in user\'s pref cookie', $rawdata);
107                continue;
108            }
109
110            // if the entry was duplicated, it will be overwritten. Takes care of #2721
111            $data[urldecode($parts[$i])] = urldecode($parts[$i + 1]);
112        }
113
114        return $data;
115    }
116
117    /**
118     * Encode the given cookie data
119     *
120     * @param array $data the cookie data as associative array
121     * @return string the raw string to save in the cookie
122     */
123    protected function encodeData(array $data): string
124    {
125        $parts = [];
126
127        foreach ($data as $key => $val) {
128            $val = (string)$val; // we only store strings
129            $parts[] = join('#', [rawurlencode($key), rawurlencode($val)]);
130        }
131
132        return join('#', $parts);
133    }
134}
135