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