xref: /dokuwiki/inc/PrefCookie.php (revision 9399c87e10527bd9270ac34f45432bcff3a2b473)
13f108b37SAndreas Gohr<?php
23f108b37SAndreas Gohr
33f108b37SAndreas Gohrnamespace dokuwiki;
43f108b37SAndreas Gohr
53f108b37SAndreas Gohr/**
63f108b37SAndreas Gohr * The preference cookie is used to store small user preference data
73f108b37SAndreas Gohr *
83f108b37SAndreas Gohr * The cookie is written from PHP (using this class) and from JavaScript (using the DokuCookie object).
93f108b37SAndreas Gohr *
103f108b37SAndreas Gohr * Data is stored as key#value#key#value string, with all keys and values being urlencoded
113f108b37SAndreas Gohr */
123f108b37SAndreas Gohrclass PrefCookie
133f108b37SAndreas Gohr{
143f108b37SAndreas Gohr    const COOKIENAME = 'DOKU_PREFS';
153f108b37SAndreas Gohr
163f108b37SAndreas Gohr    /** @var string[] */
173f108b37SAndreas Gohr    protected array $data = [];
183f108b37SAndreas Gohr
193f108b37SAndreas Gohr    /**
203f108b37SAndreas Gohr     * Initialize the class from the cookie data
213f108b37SAndreas Gohr     */
223f108b37SAndreas Gohr    public function __construct()
233f108b37SAndreas Gohr    {
243f108b37SAndreas Gohr        $this->data = $this->decodeData($_COOKIE[self::COOKIENAME] ?? '');
253f108b37SAndreas Gohr    }
263f108b37SAndreas Gohr
273f108b37SAndreas Gohr    /**
283f108b37SAndreas Gohr     * Get a preference from the cookie
293f108b37SAndreas Gohr     *
303f108b37SAndreas Gohr     * @param string $pref The preference to read
313f108b37SAndreas Gohr     * @param mixed $default The default to return if no preference is set
323f108b37SAndreas Gohr     * @return mixed
333f108b37SAndreas Gohr     */
343f108b37SAndreas Gohr    public function get(string $pref, $default = null)
353f108b37SAndreas Gohr    {
363f108b37SAndreas Gohr        return $this->data[$pref] ?? $default;
373f108b37SAndreas Gohr    }
383f108b37SAndreas Gohr
393f108b37SAndreas Gohr    /**
403f108b37SAndreas Gohr     * Set a preference
413f108b37SAndreas Gohr     *
423f108b37SAndreas Gohr     * This will trigger a setCookie header and needs to be called before any output is sent
433f108b37SAndreas Gohr     *
443f108b37SAndreas Gohr     * @param string $pref The preference to set
453f108b37SAndreas Gohr     * @param string|null $value The value to set. Null to delete a value
463f108b37SAndreas Gohr     * @return void
473f108b37SAndreas Gohr     */
483f108b37SAndreas Gohr    public function set(string $pref, ?string $value): void
493f108b37SAndreas Gohr    {
503f108b37SAndreas Gohr        if ($value === null) {
513f108b37SAndreas Gohr            if (isset($this->data[$pref])) {
523f108b37SAndreas Gohr                unset($this->data[$pref]);
533f108b37SAndreas Gohr            }
543f108b37SAndreas Gohr        } else {
553f108b37SAndreas Gohr            $this->data[$pref] = $value;
563f108b37SAndreas Gohr        }
573f108b37SAndreas Gohr
583f108b37SAndreas Gohr        $this->sendCookie();
593f108b37SAndreas Gohr    }
603f108b37SAndreas Gohr
613f108b37SAndreas Gohr    /**
623f108b37SAndreas Gohr     * Set the cookie header
633f108b37SAndreas Gohr     *
643f108b37SAndreas Gohr     * @return void
653f108b37SAndreas Gohr     */
663f108b37SAndreas Gohr    protected function sendCookie(): void
673f108b37SAndreas Gohr    {
683f108b37SAndreas Gohr        global $conf;
693f108b37SAndreas Gohr
703f108b37SAndreas Gohr        ksort($this->data); // sort by key
713f108b37SAndreas Gohr        $olddata = $_COOKIE[self::COOKIENAME] ?? '';
723f108b37SAndreas Gohr        $newdata = self::encodeData($this->data);
733f108b37SAndreas Gohr
743f108b37SAndreas Gohr        // no need to set a cookie when it's the same as before
753f108b37SAndreas Gohr        if ($olddata == $newdata) return;
763f108b37SAndreas Gohr
773f108b37SAndreas Gohr        // update the cookie data for the current request
783f108b37SAndreas Gohr        $_COOKIE[self::COOKIENAME] = $newdata;
793f108b37SAndreas Gohr
803f108b37SAndreas Gohr        // no cookies to set when running on CLI
813f108b37SAndreas Gohr        if (PHP_SAPI === 'cli') return;
823f108b37SAndreas Gohr
833f108b37SAndreas Gohr        // set the cookie header
843f108b37SAndreas Gohr        setcookie(self::COOKIENAME, $newdata, [
853f108b37SAndreas Gohr            'expires' => time() + 365 * 24 * 3600,
863f108b37SAndreas Gohr            'path' => empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'],
873f108b37SAndreas Gohr            'secure' => ($conf['securecookie'] && Ip::isSsl()),
883f108b37SAndreas Gohr            'samesite' => 'Lax'
893f108b37SAndreas Gohr        ]);
903f108b37SAndreas Gohr    }
913f108b37SAndreas Gohr
923f108b37SAndreas Gohr    /**
933f108b37SAndreas Gohr     * Decode the cookie data (if any)
943f108b37SAndreas Gohr     *
953f108b37SAndreas Gohr     * @return array the cookie data as associative array
963f108b37SAndreas Gohr     */
973f108b37SAndreas Gohr    protected function decodeData(string $rawdata): array
983f108b37SAndreas Gohr    {
993f108b37SAndreas Gohr        $data = [];
1003f108b37SAndreas Gohr        if ($rawdata === '') return $data;
1013f108b37SAndreas Gohr        $parts = explode('#', $rawdata);
1023f108b37SAndreas Gohr        $count = count($parts);
1033f108b37SAndreas Gohr
1043f108b37SAndreas Gohr        for ($i = 0; $i < $count; $i += 2) {
1053f108b37SAndreas Gohr            if (!isset($parts[$i + 1])) {
1063f108b37SAndreas Gohr                Logger::error('Odd entries in user\'s pref cookie', $rawdata);
1073f108b37SAndreas Gohr                continue;
1083f108b37SAndreas Gohr            }
1093f108b37SAndreas Gohr
1103f108b37SAndreas Gohr            // if the entry was duplicated, it will be overwritten. Takes care of #2721
1113f108b37SAndreas Gohr            $data[urldecode($parts[$i])] = urldecode($parts[$i + 1]);
1123f108b37SAndreas Gohr        }
1133f108b37SAndreas Gohr
1143f108b37SAndreas Gohr        return $data;
1153f108b37SAndreas Gohr    }
1163f108b37SAndreas Gohr
1173f108b37SAndreas Gohr    /**
1183f108b37SAndreas Gohr     * Encode the given cookie data
1193f108b37SAndreas Gohr     *
1203f108b37SAndreas Gohr     * @param array $data the cookie data as associative array
1213f108b37SAndreas Gohr     * @return string the raw string to save in the cookie
1223f108b37SAndreas Gohr     */
1233f108b37SAndreas Gohr    protected function encodeData(array $data): string
1243f108b37SAndreas Gohr    {
1253f108b37SAndreas Gohr        $parts = [];
1263f108b37SAndreas Gohr
1273f108b37SAndreas Gohr        foreach ($data as $key => $val) {
1283f108b37SAndreas Gohr            $val = (string)$val; // we only store strings
129*9399c87eSsplitbrain            $parts[] = implode('#', [rawurlencode($key), rawurlencode($val)]);
1303f108b37SAndreas Gohr        }
1313f108b37SAndreas Gohr
132*9399c87eSsplitbrain        return implode('#', $parts);
1333f108b37SAndreas Gohr    }
1343f108b37SAndreas Gohr}
135