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

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

Added cookie storage to Prefs(). Created App->addCookie method. Improved PHP version checks.

File size: 20.5 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.
27 * Session prefs can be stored into a database with the optional save() and load() methods.
28 *
29 * @author  Quinn Comendant <quinn@strangecode.com>
30 * @version 2.1
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='')
101    {
102        $app =& App::getInstance();
103
104        $this->_ns = $namespace;
105
106        // Initialized the prefs array.
107        if (!isset($_SESSION['_prefs'][$this->_ns]['saved'])) {
108            $this->clear();
109        }
110
111        // Get create tables config from global context.
112        if (!is_null($app->getParam('db_create_tables'))) {
113            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
114        }
115    }
116
117    /**
118     * Setup the database table for this class.
119     *
120     * @access  public
121     * @author  Quinn Comendant <quinn@strangecode.com>
122     * @since   04 Jun 2006 16:41:42
123     */
124    public function initDB($recreate_db=false)
125    {
126        $app =& App::getInstance();
127        $db =& DB::getInstance();
128
129        static $_db_tested = false;
130
131        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
132            if ($recreate_db) {
133                $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table'));
134                $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_INFO, __FILE__, __LINE__);
135            }
136            $db->query("CREATE TABLE IF NOT EXISTS " . $db->escapeString($this->getParam('db_table')) . " (
137                user_id VARCHAR(32) NOT NULL DEFAULT '',
138                pref_namespace VARCHAR(32) NOT NULL DEFAULT '',
139                pref_key VARCHAR(64) NOT NULL DEFAULT '',
140                pref_value TEXT,
141                PRIMARY KEY (user_id, pref_namespace, pref_key)
142            )");
143
144            if (!$db->columnExists($this->getParam('db_table'), array(
145                'user_id',
146                'pref_namespace',
147                'pref_key',
148                'pref_value',
149            ), false, false)) {
150                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
151                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
152            }
153        }
154        $_db_tested = true;
155    }
156
157    /**
158     * Set the params of this object.
159     *
160     * @param  array $params   Array of param keys and values to set.
161     */
162    public function setParam($params=null)
163    {
164        // CLI scripts can't use prefs stored in HTTP-based protocols.
165        if (defined('_CLI')
166        && isset($params['storagetype'])
167        && in_array($params['storagetype'], array('cookie', 'session'))) {
168            $app->logMsg(sprintf('Storage type %s not available for CLI', $params['storagetype']), LOG_NOTICE, __FILE__, __LINE__);
169        }
170
171        // Convert the legacy param 'persistent' to 'storagetype=database'.
172        if (isset($params['persistent']) && $params['persistent'] && !isset($params['storagetype'])) {
173            $params['storagetype'] = 'database';
174        }
175
176        if (isset($params) && is_array($params)) {
177            // Merge new parameters with old overriding only those passed.
178            $this->_params = array_merge($this->_params, $params);
179        }
180    }
181
182    /**
183     * Return the value of a parameter, if it exists.
184     *
185     * @access public
186     * @param string $param        Which parameter to return.
187     * @return mixed               Configured parameter value.
188     */
189    public function getParam($param)
190    {
191        $app =& App::getInstance();
192
193        if (isset($this->_params[$param])) {
194            return $this->_params[$param];
195        } else {
196            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
197            return null;
198        }
199    }
200
201    /**
202     * Sets the default values for preferences. If a preference is not explicitly
203     * set, the value set here will be used. Can be called multiple times to merge additional
204     * defaults together.
205     *
206     * @param  array $defaults  Array of key-value pairs
207     */
208    public function setDefaults($defaults)
209    {
210        if (isset($defaults) && is_array($defaults)) {
211            $_SESSION['_prefs'][$this->_ns]['defaults'] = array_merge($_SESSION['_prefs'][$this->_ns]['defaults'], $defaults);
212        }
213    }
214
215    /**
216     * Store a key-value pair in the session. If the value is different than what is set by setDefaults
217     * the value will be scheduled to be saved in the database.
218     * This function determines what data is saved to the database. Ensure clean values!
219     *
220     * @param  string $key          The name of the preference to modify.
221     * @param  string $val          The new value for this preference.
222     */
223    public function set($key, $val)
224    {
225        $app =& App::getInstance();
226
227        if (!is_string($key)) {
228            $app->logMsg(sprintf('Key is not a string-compatible type (%s)', getDump($key)), LOG_NOTICE, __FILE__, __LINE__);
229            return false;
230        }
231        if ('' == trim($key)) {
232            $app->logMsg(sprintf('Key is empty (along with value: %s)', $val), LOG_NOTICE, __FILE__, __LINE__);
233            return false;
234        }
235        if (!is_scalar($val) && !is_array($val) && !is_object($val)) {
236            $app->logMsg(sprintf('Value is not a string-compatible type (%s=%s)', $key, getDump($val)), LOG_WARNING, __FILE__, __LINE__);
237            return false;
238        }
239
240        // Set a saved preference if...
241        // - there isn't a default.
242        // - or the new value is different than the default
243        // - or there is a previously existing saved key.
244        switch ($this->getParam('storagetype')) {
245        case 'session':
246        case 'database': // DB prefs are saved in the session temporarily until they are saved.
247            if (!isset($_SESSION['_prefs'][$this->_ns]['defaults'][$key])
248            || $_SESSION['_prefs'][$this->_ns]['defaults'][$key] != $val
249            || isset($_SESSION['_prefs'][$this->_ns]['saved'][$key])) {
250                $_SESSION['_prefs'][$this->_ns]['saved'][$key] = $val;
251                $app->logMsg(sprintf('Setting session preference %s => %s', $key, getDump($val, true)), LOG_DEBUG, __FILE__, __LINE__);
252            } else {
253                $app->logMsg(sprintf('Not setting session preference %s => %s', $key, getDump($val, true)), LOG_DEBUG, __FILE__, __LINE__);
254            }
255            break;
256
257        case 'cookie':
258            $name = $this->_getCookieName($key);
259            $val = json_encode($val);
260            $app->setCookie($name, $val, $this->getParam('cookie_expire'), $this->getParam('cookie_path'), $this->getParam('cookie_domain'));
261            $app->logMsg(sprintf('Setting cookie preference %s => %s', $key, $val), LOG_DEBUG, __FILE__, __LINE__);
262            break;
263        }
264
265    }
266
267    /**
268     * Returns the value of the requested preference. Saved values take precedence, but if none is set
269     * a default value is returned, or if not that, null.
270     *
271     * @param string $key       The name of the preference to retrieve.
272     *
273     * @return string           The value of the preference.
274     */
275    public function get($key)
276    {
277        $app =& App::getInstance();
278
279        switch ($this->getParam('storagetype')) {
280        case 'session':
281        case 'database':
282            if (isset($_SESSION['_prefs'][$this->_ns]['saved']) && array_key_exists($key, $_SESSION['_prefs'][$this->_ns]['saved'])) {
283                $app->logMsg(sprintf('Found %s in saved', $key), LOG_DEBUG, __FILE__, __LINE__);
284                return $_SESSION['_prefs'][$this->_ns]['saved'][$key];
285            } else if (isset($_SESSION['_prefs'][$this->_ns]['defaults']) && array_key_exists($key, $_SESSION['_prefs'][$this->_ns]['defaults'])) {
286                $app->logMsg(sprintf('Found %s in defaults', $key), LOG_DEBUG, __FILE__, __LINE__);
287                return $_SESSION['_prefs'][$this->_ns]['defaults'][$key];
288            } else {
289                $app->logMsg(sprintf('Key not found in prefs cache: %s', $key), LOG_DEBUG, __FILE__, __LINE__);
290                return null;
291            }
292            break;
293
294        case 'cookie':
295            $name = $this->_getCookieName($key);
296            if ($this->exists($key)) {
297                $val = json_decode($_COOKIE[$name]);
298                $app->logMsg(sprintf('Found %s in cookie: %s', $key, getDump($val)), LOG_DEBUG, __FILE__, __LINE__);
299                return $val;
300            } else {
301                $app->logMsg(sprintf('Key not found in cookie: %s', $key), LOG_DEBUG, __FILE__, __LINE__);
302                return null;
303            }
304            break;
305        }
306    }
307
308    /**
309     * To see if a preference has been set.
310     *
311     * @param string $key       The name of the preference to check.
312     * @return boolean          True if the preference isset and not empty false otherwise.
313     */
314    public function exists($key)
315    {
316        switch ($this->getParam('storagetype')) {
317        case 'session':
318        case 'database':
319            return array_key_exists($key, $_SESSION['_prefs'][$this->_ns]['saved']);
320
321        case 'cookie':
322            $name = $this->_getCookieName($key);
323            return isset($_COOKIE[$name]);
324        }
325
326    }
327
328    /**
329     * Clear a set preference value. This will also remove the value from the database.
330     *
331     * @param string $key       The name of the preference to delete.
332     */
333    public function delete($key)
334    {
335        $app =& App::getInstance();
336
337        switch ($this->getParam('storagetype')) {
338        case 'session':
339        case 'database':
340            unset($_SESSION['_prefs'][$this->_ns]['saved'][$key]);
341            break;
342
343        case 'cookie':
344            if ($this->exists($key)) {
345                // Just set the existing value to an empty string, which expires in the past.
346                $name = $this->_getCookieName($key);
347                $app->setCookie($name, '', time() - 86400);
348                // Also unset the received cookie value, so it is unavailable.
349                unset($_COOKIE[$name]);
350            }
351            break;
352        }
353
354    }
355
356    /**
357     * Resets the $_SESSION cache. This should be executed with the same consideration
358     * as $auth->clear(), such as when logging out.
359     */
360    public function clear($focus='all')
361    {
362
363        switch ($focus) {
364        case 'all' :
365            switch ($this->getParam('storagetype')) {
366            case 'session':
367            case 'database':
368                $_SESSION['_prefs'][$this->_ns] = array(
369                    'loaded' => false,
370                    'load_datetime' => '1970-01-01',
371                    'defaults' => array(),
372                    'saved' => array(),
373                );
374                break;
375            case 'cookie':
376                foreach ($_COOKIE as $key => $value) {
377                    // All cookie keys with our internal prefix. Use only the last part as the key.
378                    if (preg_match('/^' . preg_quote(sprintf('strangecode-%s-', $this->_ns)) . '(.+)$/i', $key, $match)) {
379                        $this->delete($match[1]);
380                    }
381                }
382                break;
383            }
384            break;
385        case 'defaults' :
386            $_SESSION['_prefs'][$this->_ns]['defaults'] = array();
387            break;
388        case 'saved' :
389            $_SESSION['_prefs'][$this->_ns]['saved'] = array();
390            break;
391        }
392    }
393
394    /*
395    * Retrieves all prefs from the database and stores them in the $_SESSION.
396    *
397    * @access   public
398    * @param    bool    $force  Set to always load from database, regardless if _isLoaded() or not.
399    * @return   bool    True if loading succeeded.
400    * @author   Quinn Comendant <quinn@strangecode.com>
401    * @version  1.0
402    * @since    04 Jun 2006 16:56:53
403    */
404    public function load($force=false)
405    {
406        $app =& App::getInstance();
407        $db =& DB::getInstance();
408
409        // Skip this method if not using the db.
410        if ('database' != $this->getParam('storagetype')) {
411            return true;
412        }
413
414        $this->initDB();
415
416        // Prefs already loaded for this session.
417        if (!$force && $this->_isLoaded()) {
418            return true;
419        }
420
421        // User_id must not be empty.
422        if ('' == $this->getParam('user_id')) {
423            $app->logMsg(sprintf('Cannot save prefs because user_id not set.', null), LOG_WARNING, __FILE__, __LINE__);
424            return false;
425        }
426
427        // Clear existing cache.
428        $this->clear('saved');
429
430        // Retrieve all prefs for this user and namespace.
431        $qid = $db->query("
432            SELECT pref_key, pref_value
433            FROM " . $db->escapeString($this->getParam('db_table')) . "
434            WHERE user_id = '" . $db->escapeString($this->getParam('user_id')) . "'
435            AND pref_namespace = '" . $db->escapeString($this->_ns) . "'
436            LIMIT 10000
437        ");
438        while (list($key, $val) = mysql_fetch_row($qid)) {
439            $_SESSION['_prefs'][$this->_ns]['saved'][$key] = unserialize($val);
440        }
441
442        $app->logMsg(sprintf('Loaded %s prefs from database.', mysql_num_rows($qid)), LOG_DEBUG, __FILE__, __LINE__);
443
444        // Data loaded only once per session.
445        $_SESSION['_prefs'][$this->_ns]['loaded'] = true;
446        $_SESSION['_prefs'][$this->_ns]['load_datetime'] = date('Y-m-d H:i:s');
447
448        return true;
449    }
450
451    /*
452    * Returns true if the prefs had been loaded from the database into the $_SESSION recently.
453    * This function is simply a check so the database isn't access every page load.
454    *
455    * @access   private
456    * @return   bool    True if prefs are loaded.
457    * @author   Quinn Comendant <quinn@strangecode.com>
458    * @version  1.0
459    * @since    04 Jun 2006 17:12:44
460    */
461    private function _isLoaded()
462    {
463        if ('database' != $this->getParam('storagetype')) {
464            return true;
465        }
466
467        if (isset($_SESSION['_prefs'][$this->_ns]['load_datetime'])
468        && strtotime($_SESSION['_prefs'][$this->_ns]['load_datetime']) > time() - $this->getParam('load_timeout')
469        && isset($_SESSION['_prefs'][$this->_ns]['loaded'])
470        && true === $_SESSION['_prefs'][$this->_ns]['loaded']) {
471            return true;
472        } else {
473            return false;
474        }
475    }
476
477    /*
478    * Saves all prefs stored in the $_SESSION into the database.
479    *
480    * @access   public
481    * @return   bool    True if prefs exist and were saved.
482    * @author   Quinn Comendant <quinn@strangecode.com>
483    * @version  1.0
484    * @since    04 Jun 2006 17:19:56
485    */
486    public function save()
487    {
488        $app =& App::getInstance();
489        $db =& DB::getInstance();
490
491        // Skip this method if not using the db.
492        if ('database' != $this->getParam('storagetype')) {
493            return true;
494        }
495
496        // User_id must not be empty.
497        if ('' == $this->getParam('user_id')) {
498            $app->logMsg(sprintf('Cannot save prefs because user_id not set.', null), LOG_WARNING, __FILE__, __LINE__);
499            return false;
500        }
501
502        $this->initDB();
503
504        if (isset($_SESSION['_prefs'][$this->_ns]['saved']) && is_array($_SESSION['_prefs'][$this->_ns]['saved']) && !empty($_SESSION['_prefs'][$this->_ns]['saved'])) {
505            // Delete old prefs from database.
506            $db->query("
507                DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
508                WHERE user_id = '" . $db->escapeString($this->getParam('user_id')) . "'
509                AND pref_namespace = '" . $db->escapeString($this->_ns) . "'
510            ");
511
512            // Insert new prefs.
513            $insert_values = array();
514            foreach ($_SESSION['_prefs'][$this->_ns]['saved'] as $key => $val) {
515                $insert_values[] = sprintf("('%s', '%s', '%s', '%s')",
516                    $db->escapeString($this->getParam('user_id')),
517                    $db->escapeString($this->_ns),
518                    $db->escapeString($key),
519                    $db->escapeString(serialize($val))
520                );
521            }
522            // TODO: after MySQL 5.0.23 is released this query could benefit from INSERT DELAYED.
523            $db->query("
524                INSERT INTO " . $db->escapeString($this->getParam('db_table')) . "
525                (user_id, pref_namespace, pref_key, pref_value)
526                VALUES " . join(', ', $insert_values) . "
527            ");
528
529            $app->logMsg(sprintf('Saved %s prefs to database.', sizeof($insert_values)), LOG_DEBUG, __FILE__, __LINE__);
530            return true;
531        }
532
533        return false;
534    }
535
536    /*
537    *
538    *
539    * @access   public
540    * @param
541    * @return
542    * @author   Quinn Comendant <quinn@strangecode.com>
543    * @version  1.0
544    * @since    02 May 2014 18:17:04
545    */
546    private function _getCookieName($key)
547    {
548        $app =& App::getInstance();
549
550        if (mb_strpos($key, sprintf('strangecode-%s', $this->_ns)) === 0) {
551            $app->logMsg(sprintf('Invalid key name (%s). Leave off "strangecode-%s-" and it should work.', $key, $this->_ns), LOG_NOTICE, __FILE__, __LINE__);
552        }
553        return sprintf('strangecode-%s-%s', $this->_ns, $key);
554    }
555}
556
Note: See TracBrowser for help on using the repository browser.