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