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

Last change on this file since 640 was 615, checked in by anonymous, 7 years ago

Add 'href' key to printSubmitButtons()

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