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