source: trunk/lib/Prefs.inc.php @ 480

Last change on this file since 480 was 480, checked in by anonymous, 10 years ago

Removed some legacy files. Improved use of array_key_exists.

File size: 22.7 KB
Line 
1<?php
2/**
3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
5 * Copyright 2001-2012 Strangecode, LLC
6 *
7 * This file is part of The Strangecode Codebase.
8 *
9 * The Strangecode Codebase is free software: you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License as published by the
11 * Free Software Foundation, either version 3 of the License, or (at your option)
12 * any later version.
13 *
14 * The Strangecode Codebase is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
18 *
19 * You should have received a copy of the GNU General Public License along with
20 * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23/**
24 * Prefs.inc.php
25 *
26 * Prefs provides an API for saving arbitrary values in a user's session, in cookies, and in the database.
27 * Prefs can be stored into a database with the optional save() and load() methods.
28 *
29 * @author  Quinn Comendant <quinn@strangecode.com>
30 * @version 3.0
31 *
32 * Example of use (database storagetype):
33---------------------------------------------------------------------
34// Load preferences for the user's session.
35require_once 'codebase/lib/Prefs.inc.php';
36$prefs = new Prefs('my-namespace');
37$prefs->setParam(array(
38    'storagetype' => ($auth->isLoggedIn() ? 'database' : 'session'),
39    'user_id' => $auth->get('user_id'),
40));
41$prefs->setDefaults(array(
42    'search_num_results' => 25,
43    'datalog_num_entries' => 25,
44));
45$prefs->load();
46
47// Update preferences. Make sure to validate this input first!
48$prefs->set('search_num_results', getFormData('search_num_results'));
49$prefs->set('datalog_num_entries', getFormData('datalog_num_entries'));
50$prefs->save();
51---------------------------------------------------------------------
52 */
53class Prefs {
54
55    // Namespace of this instance of Prefs.
56    private $_ns;
57
58    // Configuration parameters for this object.
59    private $_params = array(
60
61        // Legacy parameter, superceeded by the 'storagetype' setting.
62        // Enable database storage. If this is false, all prefs will live only as long as the session.
63        'persistent' => null,
64
65        // Store preferences in one of the available storage mechanisms: session, cookie, database
66        'storagetype' => 'session',
67
68        // ----------------------------------------------------------
69        // Cookie-type settings.
70
71        // Lifespan of the cookie. If set to an integer, interpreted as a timestamp (0 for 'when user closes browser'), otherwise as a strtotime-compatible value ('tomorrow', etc).
72        'cookie_expire' => '+10 years',
73
74        // The path on the server in which the cookie will be available on.
75        'cookie_path' => null,
76
77        // The domain that the cookie is available to.
78        'cookie_domain' => null,
79
80        // ----------------------------------------------------------
81        // Database-type settings.
82
83        // The current user_id for which to load/save database-backed preferences.
84        'user_id' => null,
85
86        // How long before we force a reload of the persistent prefs data? 3600 = once every hour.
87        'load_timeout' => 3600,
88
89        // Name of database table to store persistent prefs.
90        'db_table' => 'pref_tbl',
91
92        // Automatically create table and verify columns. Better set to false after site launch.
93        // This value is overwritten by the $app->getParam('db_create_tables') setting if it is available.
94        'create_table' => true,
95    );
96
97    /**
98     * Prefs constructor.
99     */
100    public function __construct($namespace='', array $params=null)
101    {
102        $app =& App::getInstance();
103
104        $this->_ns = $namespace;
105
106        // Get create tables config from global context.
107        if (!is_null($app->getParam('db_create_tables'))) {
108            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
109        }
110
111        // Optional initial params.
112        $this->setParam($params);
113
114        // Run Prefs->save() upon script completion if we're using the database storagetype.
115        // This only works if 'storagetype' is provided as a parameter to the constructor rather than via setParam() later.
116        if ('database' == $this->getParam('storagetype')) {
117            register_shutdown_function(array($this, 'save'));
118        }
119    }
120
121    /**
122     * Setup the database table for this class.
123     *
124     * @access  public
125     * @author  Quinn Comendant <quinn@strangecode.com>
126     * @since   04 Jun 2006 16:41:42
127     */
128    public function initDB($recreate_db=false)
129    {
130        $app =& App::getInstance();
131        $db =& DB::getInstance();
132
133        static $_db_tested = false;
134
135        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
136            if ($recreate_db) {
137                $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table'));
138                $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_INFO, __FILE__, __LINE__);
139            }
140            $db->query("CREATE TABLE IF NOT EXISTS " . $db->escapeString($this->getParam('db_table')) . " (
141                user_id VARCHAR(32) NOT NULL DEFAULT '',
142                pref_namespace VARCHAR(32) NOT NULL DEFAULT '',
143                pref_key VARCHAR(64) NOT NULL DEFAULT '',
144                pref_value TEXT,
145                PRIMARY KEY (user_id, pref_namespace, pref_key)
146            )");
147
148            if (!$db->columnExists($this->getParam('db_table'), array(
149                'user_id',
150                'pref_namespace',
151                'pref_key',
152                'pref_value',
153            ), false, false)) {
154                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
155                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
156            }
157        }
158        $_db_tested = true;
159    }
160
161    /**
162     * Set the params of this object.
163     *
164     * @param  array $params   Array of param keys and values to set.
165     */
166    public function setParam(array $params=null)
167    {
168        // CLI scripts can't use prefs stored in HTTP-based protocols.
169        if (defined('_CLI')
170        && isset($params['storagetype'])
171        && in_array($params['storagetype'], array('cookie', 'session'))) {
172            $app->logMsg(sprintf('Storage type %s not available for CLI', $params['storagetype']), LOG_NOTICE, __FILE__, __LINE__);
173        }
174
175        // Convert the legacy param 'persistent' to 'storagetype=database'.
176        if (isset($params['persistent']) && $params['persistent'] && !isset($params['storagetype'])) {
177            $params['storagetype'] = 'database';
178        }
179
180        if (isset($params) && is_array($params)) {
181            // Merge new parameters with old overriding only those passed.
182            $this->_params = array_merge($this->_params, $params);
183        }
184    }
185
186    /**
187     * Return the value of a parameter, if it exists.
188     *
189     * @access public
190     * @param string $param        Which parameter to return.
191     * @return mixed               Configured parameter value.
192     */
193    public function getParam($param)
194    {
195        $app =& App::getInstance();
196
197        if (array_key_exists($param, $this->_params)) {
198            return $this->_params[$param];
199        } else {
200            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
201            return null;
202        }
203    }
204
205    /**
206     * Sets the default values for preferences. If a preference is not explicitly
207     * set, the value set here will be used. Can be called multiple times to merge additional
208     * defaults together. This is mostly only useful for the database storetype, when you have
209     * values you want to use as default, and those are not stored to the database (so the defaults
210     * can be changed later and apply to all users who haven't make s specific setting).
211     * For the cookie storetype, using setDefaults just sets cookies but only if a cookie with
212     * the same name is not already set.
213     *
214     * @param  array $defaults  Array of key-value pairs
215     */
216    public function setDefaults($defaults)
217    {
218        $app =& App::getInstance();
219
220        if (isset($defaults) && is_array($defaults)) {
221            switch ($this->getParam('storagetype')) {
222            case 'session':
223            case 'database':
224                $_SESSION['_prefs'][$this->_ns]['defaults'] = array_merge($_SESSION['_prefs'][$this->_ns]['defaults'], $defaults);
225                break;
226
227            case 'cookie':
228                foreach ($defaults as $key => $val) {
229                    if (!$this->exists($key)) {
230                        $this->set($key, $val);
231                    }
232                }
233                unset($key, $val);
234                break;
235            }
236        } else {
237            $app->logMsg(sprintf('Wrong data-type passed to Prefs->setDefaults().', null), LOG_NOTICE, __FILE__, __LINE__);
238        }
239    }
240
241    /**
242     * Store a key-value pair.
243     * When using the database storagetype, if the value is different than what is set by setDefaults the value will be scheduled to be saved in the database.
244     *
245     * @param  string $key          The name of the preference to modify.
246     * @param  string $val          The new value for this preference.
247     */
248    public function set($key, $val)
249    {
250        $app =& App::getInstance();
251
252        if (!is_string($key)) {
253            $app->logMsg(sprintf('Key is not a string-compatible type (%s)', getDump($key)), LOG_NOTICE, __FILE__, __LINE__);
254            return false;
255        }
256        if ('' == trim($key)) {
257            $app->logMsg(sprintf('Key is empty (along with value: %s)', $val), LOG_NOTICE, __FILE__, __LINE__);
258            return false;
259        }
260        if (!is_scalar($val) && !is_array($val) && !is_object($val)) {
261            $app->logMsg(sprintf('Value is not a compatible data type (%s=%s)', $key, getDump($val)), LOG_WARNING, __FILE__, __LINE__);
262            return false;
263        }
264
265        switch ($this->getParam('storagetype')) {
266        // Both session and database prefs are saved in the session (for database, only temporarily until they are saved).
267        case 'session':
268        case 'database':
269            // Initialized the prefs array.
270            if (!isset($_SESSION['_prefs'][$this->_ns]['saved'])) {
271                $this->clear();
272            }
273            // Set a saved preference if...
274            // - there isn't a default.
275            // - or the new value is different than the default
276            // - or there is a previously existing saved key.
277            if (!(isset($_SESSION['_prefs'][$this->_ns]['defaults']) && array_key_exists($key, $_SESSION['_prefs'][$this->_ns]['defaults']))
278            || $_SESSION['_prefs'][$this->_ns]['defaults'][$key] != $val
279            || (isset($_SESSION['_prefs'][$this->_ns]['saved']) && array_key_exists($key, $_SESSION['_prefs'][$this->_ns]['saved']))) {
280                $_SESSION['_prefs'][$this->_ns]['saved'][$key] = $val;
281                $app->logMsg(sprintf('Setting session preference %s => %s', $key, getDump($val, true)), LOG_DEBUG, __FILE__, __LINE__);
282            } else {
283                $app->logMsg(sprintf('Not setting session preference %s => %s', $key, getDump($val, true)), LOG_DEBUG, __FILE__, __LINE__);
284            }
285            break;
286
287        case 'cookie':
288            $name = $this->_getCookieName($key);
289            $val = json_encode($val);
290            $app->setCookie($name, $val, $this->getParam('cookie_expire'), $this->getParam('cookie_path'), $this->getParam('cookie_domain'));
291            $_COOKIE[$name] = $val;
292            $app->logMsg(sprintf('Setting cookie preference %s => %s', $key, $val), LOG_DEBUG, __FILE__, __LINE__);
293            break;
294        }
295
296    }
297
298    /**
299     * Returns the value of the requested preference. Saved values take precedence, but if none is set
300     * a default value is returned, or if not that, null.
301     *
302     * @param string $key       The name of the preference to retrieve.
303     *
304     * @return string           The value of the preference.
305     */
306    public function get($key)
307    {
308        $app =& App::getInstance();
309
310        switch ($this->getParam('storagetype')) {
311        case 'session':
312        case 'database':
313            if (isset($_SESSION['_prefs'][$this->_ns]['saved']) && array_key_exists($key, $_SESSION['_prefs'][$this->_ns]['saved'])) {
314                $app->logMsg(sprintf('Found %s in saved', $key), LOG_DEBUG, __FILE__, __LINE__);
315                return $_SESSION['_prefs'][$this->_ns]['saved'][$key];
316            } else if (isset($_SESSION['_prefs'][$this->_ns]['defaults']) && array_key_exists($key, $_SESSION['_prefs'][$this->_ns]['defaults'])) {
317                $app->logMsg(sprintf('Found %s in defaults', $key), LOG_DEBUG, __FILE__, __LINE__);
318                return $_SESSION['_prefs'][$this->_ns]['defaults'][$key];
319            } else {
320                $app->logMsg(sprintf('Key not found in prefs cache: %s', $key), LOG_DEBUG, __FILE__, __LINE__);
321                return null;
322            }
323            break;
324
325        case 'cookie':
326            $name = $this->_getCookieName($key);
327            if ($this->exists($key)) {
328                $val = json_decode($_COOKIE[$name]);
329                $app->logMsg(sprintf('Found %s in cookie: %s', $key, getDump($val)), LOG_DEBUG, __FILE__, __LINE__);
330                return $val;
331            } else {
332                $app->logMsg(sprintf('Key not found in cookie: %s', $key), LOG_DEBUG, __FILE__, __LINE__);
333                return null;
334            }
335            break;
336        }
337    }
338
339    /**
340     * To see if a preference has been set.
341     *
342     * @param string $key       The name of the preference to check.
343     * @return boolean          True if the preference isset and not empty false otherwise.
344     */
345    public function exists($key)
346    {
347        switch ($this->getParam('storagetype')) {
348        case 'session':
349        case 'database':
350            return (isset($_SESSION['_prefs'][$this->_ns]['saved']) && array_key_exists($key, $_SESSION['_prefs'][$this->_ns]['saved']));
351
352        case 'cookie':
353            $name = $this->_getCookieName($key);
354            return (isset($_COOKIE) && array_key_exists($name, $_COOKIE));
355        }
356
357    }
358
359    /**
360     * Delete an existing preference value. This will also remove the value from the database, once save() is called.
361     *
362     * @param string $key       The name of the preference to delete.
363     */
364    public function delete($key)
365    {
366        $app =& App::getInstance();
367
368        switch ($this->getParam('storagetype')) {
369        case 'session':
370        case 'database':
371            unset($_SESSION['_prefs'][$this->_ns]['saved'][$key]);
372            break;
373
374        case 'cookie':
375            if ($this->exists($key)) {
376                // Just set the existing value to an empty string, which expires in the past.
377                $name = $this->_getCookieName($key);
378                $app->setCookie($name, '', time() - 86400);
379                // Also unset the received cookie value, so it is unavailable.
380                unset($_COOKIE[$name]);
381            }
382            break;
383        }
384
385    }
386
387    /**
388     * Resets all existing values under this namespace. This should be executed with the same consideration as $auth->clear(), such as when logging out.
389     */
390    public function clear($scope='all')
391    {
392        switch ($scope) {
393        case 'all' :
394            switch ($this->getParam('storagetype')) {
395            case 'session':
396            case 'database':
397                $_SESSION['_prefs'][$this->_ns] = array(
398                    'loaded' => false,
399                    'load_datetime' => '1970-01-01',
400                    'defaults' => array(),
401                    'saved' => array(),
402                );
403                break;
404            case 'cookie':
405                foreach ($_COOKIE as $key => $value) {
406                    // All cookie keys with our internal prefix. Use only the last part as the key.
407                    if (preg_match('/^' . preg_quote(sprintf('_prefs-%s-', $this->_ns)) . '(.+)$/i', $key, $match)) {
408                        $this->delete($match[1]);
409                    }
410                }
411                break;
412            }
413            break;
414
415        case 'defaults' :
416            $_SESSION['_prefs'][$this->_ns]['defaults'] = array();
417            break;
418
419        case 'saved' :
420            $_SESSION['_prefs'][$this->_ns]['saved'] = array();
421            break;
422        }
423    }
424
425    /*
426    * Retrieves all prefs from the database and stores them in the $_SESSION.
427    *
428    * @access   public
429    * @param    bool    $force  Set to always load from database, regardless if _isLoaded() or not.
430    * @return   bool    True if loading succeeded.
431    * @author   Quinn Comendant <quinn@strangecode.com>
432    * @version  1.0
433    * @since    04 Jun 2006 16:56:53
434    */
435    public function load($force=false)
436    {
437        $app =& App::getInstance();
438        $db =& DB::getInstance();
439
440        // Skip this method if not using the db.
441        if ('database' != $this->getParam('storagetype')) {
442            $app->logMsg('Prefs->load() does nothing unless using a database storagetype.', LOG_NOTICE, __FILE__, __LINE__);
443            return true;
444        }
445
446        $this->initDB();
447
448        // Prefs already loaded for this session.
449        if (!$force && $this->_isLoaded()) {
450            return true;
451        }
452
453        // User_id must not be empty.
454        if ('' == $this->getParam('user_id')) {
455            $app->logMsg(sprintf('Cannot save prefs because user_id not set.', null), LOG_WARNING, __FILE__, __LINE__);
456            return false;
457        }
458
459        // Clear existing cache.
460        $this->clear('saved');
461
462        // Retrieve all prefs for this user and namespace.
463        $qid = $db->query("
464            SELECT pref_key, pref_value
465            FROM " . $db->escapeString($this->getParam('db_table')) . "
466            WHERE user_id = '" . $db->escapeString($this->getParam('user_id')) . "'
467            AND pref_namespace = '" . $db->escapeString($this->_ns) . "'
468            LIMIT 10000
469        ");
470        while (list($key, $val) = mysql_fetch_row($qid)) {
471            $_SESSION['_prefs'][$this->_ns]['saved'][$key] = unserialize($val);
472        }
473
474        $app->logMsg(sprintf('Loaded %s prefs from database.', mysql_num_rows($qid)), LOG_DEBUG, __FILE__, __LINE__);
475
476        // Data loaded only once per session.
477        $_SESSION['_prefs'][$this->_ns]['loaded'] = true;
478        $_SESSION['_prefs'][$this->_ns]['load_datetime'] = date('Y-m-d H:i:s');
479
480        return true;
481    }
482
483    /*
484    * Returns true if the prefs had been loaded from the database into the $_SESSION recently.
485    * This function is simply a check so the database isn't access every page load.
486    *
487    * @access   private
488    * @return   bool    True if prefs are loaded.
489    * @author   Quinn Comendant <quinn@strangecode.com>
490    * @version  1.0
491    * @since    04 Jun 2006 17:12:44
492    */
493    private function _isLoaded()
494    {
495        if ('database' != $this->getParam('storagetype')) {
496            $app->logMsg('Prefs->_isLoaded() does nothing unless using a database storagetype.', LOG_NOTICE, __FILE__, __LINE__);
497            return true;
498        }
499
500        if (isset($_SESSION['_prefs'][$this->_ns]['load_datetime'])
501        && strtotime($_SESSION['_prefs'][$this->_ns]['load_datetime']) > time() - $this->getParam('load_timeout')
502        && isset($_SESSION['_prefs'][$this->_ns]['loaded'])
503        && true === $_SESSION['_prefs'][$this->_ns]['loaded']) {
504            return true;
505        } else {
506            return false;
507        }
508    }
509
510    /*
511    * Saves all prefs stored in the $_SESSION into the database.
512    *
513    * @access   public
514    * @return   bool    True if prefs exist and were saved.
515    * @author   Quinn Comendant <quinn@strangecode.com>
516    * @version  1.0
517    * @since    04 Jun 2006 17:19:56
518    */
519    public function save()
520    {
521        $app =& App::getInstance();
522        $db =& DB::getInstance();
523
524        // Skip this method if not using the db.
525        if ('database' != $this->getParam('storagetype')) {
526            $app->logMsg('Prefs->save() does nothing unless using a database storagetype.', LOG_NOTICE, __FILE__, __LINE__);
527            return true;
528        }
529
530        // User_id must not be empty.
531        if ('' == $this->getParam('user_id')) {
532            $app->logMsg(sprintf('Cannot save prefs because user_id not set.', null), LOG_WARNING, __FILE__, __LINE__);
533            return false;
534        }
535
536        $this->initDB();
537
538        if (isset($_SESSION['_prefs'][$this->_ns]['saved']) && is_array($_SESSION['_prefs'][$this->_ns]['saved']) && !empty($_SESSION['_prefs'][$this->_ns]['saved'])) {
539            // Delete old prefs from database.
540            $db->query("
541                DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
542                WHERE user_id = '" . $db->escapeString($this->getParam('user_id')) . "'
543                AND pref_namespace = '" . $db->escapeString($this->_ns) . "'
544            ");
545
546            // Insert new prefs.
547            $insert_values = array();
548            foreach ($_SESSION['_prefs'][$this->_ns]['saved'] as $key => $val) {
549                $insert_values[] = sprintf("('%s', '%s', '%s', '%s')",
550                    $db->escapeString($this->getParam('user_id')),
551                    $db->escapeString($this->_ns),
552                    $db->escapeString($key),
553                    $db->escapeString(serialize($val))
554                );
555            }
556            // TODO: after MySQL 5.0.23 is released this query could benefit from INSERT DELAYED.
557            $db->query("
558                INSERT INTO " . $db->escapeString($this->getParam('db_table')) . "
559                (user_id, pref_namespace, pref_key, pref_value)
560                VALUES " . join(', ', $insert_values) . "
561            ");
562
563            $app->logMsg(sprintf('Saved %s prefs to database.', sizeof($insert_values)), LOG_DEBUG, __FILE__, __LINE__);
564            return true;
565        }
566
567        return false;
568    }
569
570    /*
571    *
572    *
573    * @access   public
574    * @param
575    * @return
576    * @author   Quinn Comendant <quinn@strangecode.com>
577    * @version  1.0
578    * @since    02 May 2014 18:17:04
579    */
580    private function _getCookieName($key)
581    {
582        $app =& App::getInstance();
583
584        if (mb_strpos($key, sprintf('_prefs-%s', $this->_ns)) === 0) {
585            $app->logMsg(sprintf('Invalid key name (%s). Leave off "_prefs-%s-" and it should work.', $key, $this->_ns), LOG_NOTICE, __FILE__, __LINE__);
586        }
587        // Use standardized class data names: _ + classname + namespace + variablekey
588        return sprintf('_prefs-%s-%s', $this->_ns, $key);
589    }
590}
591
Note: See TracBrowser for help on using the repository browser.