1<?php 2/** 3 * Matomo - free/libre analytics platform 4 * 5 * @link https://matomo.org 6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later 7 * 8 */ 9namespace Piwik; 10 11use Piwik\Container\StaticContainer; 12 13/** 14 * Convenient key-value storage for user specified options and temporary 15 * data that needs to be persisted beyond one request. 16 * 17 * ### Examples 18 * 19 * **Setting and getting options** 20 * 21 * $optionValue = Option::get('MyPlugin.MyOptionName'); 22 * if ($optionValue === false) { 23 * // if not set, set it 24 * Option::set('MyPlugin.MyOptionName', 'my option value'); 25 * } 26 * 27 * **Storing user specific options** 28 * 29 * $userName = // ... 30 * Option::set('MyPlugin.MyOptionName.' . $userName, 'my option value'); 31 * 32 * **Clearing user specific options** 33 * 34 * Option::deleteLike('MyPlugin.MyOptionName.%'); 35 * 36 * @api 37 */ 38class Option 39{ 40 /** 41 * Returns the option value for the requested option `$name`. 42 * 43 * @param string $name The option name. 44 * @return string|false The value or `false`, if not found. 45 */ 46 public static function get($name) 47 { 48 return self::getInstance()->getValue($name); 49 } 50 51 /** 52 * Returns option values for options whose names are like a given pattern. Only `%` is supported as part of the 53 * pattern. 54 * 55 * @param string $namePattern The pattern used in the SQL `LIKE` expression 56 * used to SELECT options.`'%'` characters should be used as wildcard. Underscore match is not supported. 57 * @return array Array mapping option names with option values. 58 */ 59 public static function getLike($namePattern) 60 { 61 return self::getInstance()->getNameLike($namePattern); 62 } 63 64 /** 65 * Sets an option value by name. 66 * 67 * @param string $name The option name. 68 * @param string $value The value to set the option to. 69 * @param int $autoLoad If set to 1, this option value will be automatically loaded when Piwik is initialized; 70 * should be set to 1 for options that will be used in every Piwik request. 71 */ 72 public static function set($name, $value, $autoload = 0) 73 { 74 self::getInstance()->setValue($name, $value, $autoload); 75 } 76 77 /** 78 * Deletes an option. 79 * 80 * @param string $name Option name to match exactly. 81 * @param string $value If supplied the option will be deleted only if its value matches this value. 82 */ 83 public static function delete($name, $value = null) 84 { 85 self::getInstance()->deleteValue($name, $value); 86 } 87 88 /** 89 * Deletes all options that match the supplied pattern. Only `%` is supported as part of the 90 * pattern. 91 * 92 * @param string $namePattern Pattern of key to match. `'%'` characters should be used as wildcard. Underscore match is not supported. 93 * @param string $value If supplied, options will be deleted only if their value matches this value. 94 */ 95 public static function deleteLike($namePattern, $value = null) 96 { 97 self::getInstance()->deleteNameLike($namePattern, $value); 98 } 99 100 public static function clearCachedOption($name) 101 { 102 self::getInstance()->clearCachedOptionByName($name); 103 } 104 105 /** 106 * Clears the option value cache and forces a reload from the Database. 107 * Used in unit tests to reset the state of the object between tests. 108 * 109 * @return void 110 * @ignore 111 */ 112 public static function clearCache() 113 { 114 $option = self::getInstance(); 115 $option->loaded = false; 116 $option->all = array(); 117 } 118 119 /** 120 * @var array 121 */ 122 private $all = array(); 123 124 /** 125 * @var bool 126 */ 127 private $loaded = false; 128 129 /** 130 * Singleton instance 131 * @var \Piwik\Option 132 */ 133 private static $instance = null; 134 135 /** 136 * Returns Singleton instance 137 * 138 * @return \Piwik\Option 139 */ 140 private static function getInstance() 141 { 142 if (self::$instance == null) { 143 self::$instance = new self; 144 } 145 146 return self::$instance; 147 } 148 149 /** 150 * Sets the singleton instance. For testing purposes. 151 * 152 * @param mixed 153 * @ignore 154 */ 155 public static function setSingletonInstance($instance) 156 { 157 self::$instance = $instance; 158 } 159 160 /** 161 * Private Constructor 162 */ 163 private function __construct() 164 { 165 } 166 167 protected function clearCachedOptionByName($name) 168 { 169 $name = $this->trimOptionNameIfNeeded($name); 170 if (isset($this->all[$name])) { 171 unset($this->all[$name]); 172 } 173 } 174 175 protected function getValue($name) 176 { 177 $name = $this->trimOptionNameIfNeeded($name); 178 $this->autoload(); 179 if (isset($this->all[$name])) { 180 return $this->all[$name]; 181 } 182 183 $value = Db::fetchOne('SELECT option_value FROM `' . Common::prefixTable('option') . '` ' . 184 'WHERE option_name = ?', [$name]); 185 186 $this->all[$name] = $value; 187 return $value; 188 } 189 190 protected function setValue($name, $value, $autoLoad = 0) 191 { 192 $autoLoad = (int)$autoLoad; 193 $name = $this->trimOptionNameIfNeeded($name); 194 195 $sql = 'UPDATE `' . Common::prefixTable('option') . '` SET option_value = ?, autoload = ? WHERE option_name = ?'; 196 $bind = array($value, $autoLoad, $name); 197 198 $result = Db::query($sql, $bind); 199 200 $rowsUpdated = Db::get()->rowCount($result); 201 202 if (! $rowsUpdated) { 203 try { 204 $sql = 'INSERT IGNORE INTO `' . Common::prefixTable('option') . '` (option_name, option_value, autoload) ' . 205 'VALUES (?, ?, ?) '; 206 $bind = array($name, $value, $autoLoad); 207 208 Db::query($sql, $bind); 209 } catch (\Exception $e) { 210 } 211 } 212 213 $this->all[$name] = $value; 214 } 215 216 protected function deleteValue($name, $value) 217 { 218 $name = $this->trimOptionNameIfNeeded($name); 219 $sql = 'DELETE FROM `' . Common::prefixTable('option') . '` WHERE option_name = ?'; 220 $bind[] = $name; 221 222 if (isset($value)) { 223 $sql .= ' AND option_value = ?'; 224 $bind[] = $value; 225 } 226 227 Db::query($sql, $bind); 228 229 $this->clearCache(); 230 } 231 232 protected function deleteNameLike($name, $value = null) 233 { 234 $name = $this->trimOptionNameIfNeeded($name); 235 $name = $this->getNameForLike($name); 236 237 $sql = 'DELETE FROM `' . Common::prefixTable('option') . '` WHERE option_name LIKE ?'; 238 $bind[] = $name; 239 240 if (isset($value)) { 241 $sql .= ' AND option_value = ?'; 242 $bind[] = $value; 243 } 244 245 Db::query($sql, $bind); 246 247 $this->clearCache(); 248 } 249 250 private function getNameForLike($name) 251 { 252 $name = str_replace('\_', '###NOREPLACE###', $name); 253 $name = str_replace('_', '\_', $name); 254 $name = str_replace( '###NOREPLACE###', '\_', $name); 255 return $name; 256 } 257 258 protected function getNameLike($name) 259 { 260 $name = $this->trimOptionNameIfNeeded($name); 261 $name = $this->getNameForLike($name); 262 263 $sql = 'SELECT option_name, option_value FROM `' . Common::prefixTable('option') . '` WHERE option_name LIKE ?'; 264 $bind = array($name); 265 $rows = Db::fetchAll($sql, $bind); 266 267 $result = array(); 268 foreach ($rows as $row) { 269 $result[$row['option_name']] = $row['option_value']; 270 } 271 272 return $result; 273 } 274 275 /** 276 * Initialize cache with autoload settings. 277 * 278 * @return void 279 */ 280 protected function autoload() 281 { 282 if ($this->loaded) { 283 return; 284 } 285 286 $table = Common::prefixTable('option'); 287 $sql = 'SELECT option_value, option_name FROM `' . $table . '` WHERE autoload = 1'; 288 $all = Db::fetchAll($sql); 289 290 foreach ($all as $option) { 291 $this->all[$option['option_name']] = $option['option_value']; 292 } 293 294 $this->loaded = true; 295 } 296 297 private function trimOptionNameIfNeeded($name) 298 { 299 if (strlen($name) > 191) { 300 StaticContainer::get('Psr\Log\LoggerInterface')->debug("Option name '$name' is too long and was trimmed to 191 chars"); 301 $name = substr($name, 0, 191); 302 } 303 304 return $name; 305 } 306} 307