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