Index: /trunk/lib/App.inc.php
===================================================================
--- /trunk/lib/App.inc.php	(revision 692)
+++ /trunk/lib/App.inc.php	(revision 693)
@@ -54,6 +54,9 @@
     public $running = false;
 
-    // Instance of database object.
+    // Instance of database object (from mysql_connect() PHP version < 7).
     public $db;
+
+    // Instance of PDO object.
+    public $pdo;
 
     // Instance of database session handler object.
@@ -135,4 +138,5 @@
         // Use database?
         'enable_db' => false,
+        'enable_db_pdo' => false,
 
         // Use db-based sessions?
@@ -397,5 +401,5 @@
          */
 
-        if (true === $this->getParam('enable_db')) {
+        if (true === $this->getParam('enable_db') || true === $this->getParam('enable_db_pdo')) {
 
             // DB connection parameters taken from environment variables in the server httpd.conf file (readable only by root)âŠ
@@ -418,17 +422,22 @@
                 if (false !== $db_auth_file = stream_resolve_include_path($this->getParam('db_auth_file'))) {
                     if (is_readable($db_auth_file)) {
-                        $this->setParam(json_decode(file_get_contents($db_auth_file), true));
+                        $db_auth = json_decode(file_get_contents($db_auth_file), true);
+                        if (is_null($db_auth)) {
+                            $app->logMsg(sprintf('Unable to decode json in DB auth file: %s', $db_auth_file), LOG_ERR, __FILE__, __LINE__);
+                        } else {
+                            $this->setParam($db_auth);
+                        }
                     } else {
-                        $this->logMsg(sprintf('Unable to read DB auth file: %s', $db_auth_file), LOG_NOTICE, __FILE__, __LINE__);
+                        $this->logMsg(sprintf('Unable to read DB auth file: %s', $db_auth_file), LOG_ERR, __FILE__, __LINE__);
                     }
                 } else {
-                    $this->logMsg(sprintf('DB auth file not found: %s', $this->getParam('db_auth_file')), LOG_NOTICE, __FILE__, __LINE__);
+                    $this->logMsg(sprintf('DB auth file not found: %s', $this->getParam('db_auth_file')), LOG_ERR, __FILE__, __LINE__);
                 }
             }
 
-            // There will ever only be one instance of the DB object, and here is where it is instantiated.
-            require_once dirname(__FILE__) . '/DB.inc.php';
-            $this->db =& DB::getInstance();
-            $this->db->setParam(array(
+            // If the app wants a DB connection, always set up a PDO object.
+            require_once dirname(__FILE__) . '/PDO.inc.php';
+            $this->pdo =& \Strangecode\Codebase\PDO::getInstance();
+            $this->pdo->setParam(array(
                 'db_server' => $this->getParam('db_server'),
                 'db_name' => $this->getParam('db_name'),
@@ -442,7 +451,24 @@
                 'collation' => $this->getParam('db_collation'),
             ));
-
-            // Connect to database.
-            $this->db->connect();
+            $this->pdo->connect();
+
+            // Only create a legacy mysql_* DB object if it is explicitly requested.
+            if (true === $this->getParam('enable_db')) {
+                require_once dirname(__FILE__) . '/DB.inc.php';
+                $this->db =& DB::getInstance();
+                $this->db->setParam(array(
+                    'db_server' => $this->getParam('db_server'),
+                    'db_name' => $this->getParam('db_name'),
+                    'db_user' => $this->getParam('db_user'),
+                    'db_pass' => $this->getParam('db_pass'),
+                    'db_always_debug' => $this->getParam('db_always_debug'),
+                    'db_debug' => $this->getParam('db_debug'),
+                    'db_die_on_failure' => $this->getParam('db_die_on_failure'),
+                    'timezone' => $this->getParam('db_timezone'),
+                    'character_set' => $this->getParam('db_character_set'),
+                    'collation' => $this->getParam('db_collation'),
+                ));
+                $this->db->connect();
+            }
         }
 
@@ -506,7 +532,7 @@
 
         // To get a safe hostname, remove port and invalid hostname characters.
-        $safe_http_host = preg_replace('/[^a-z\d.:-]/', '', strtok(getenv('HTTP_HOST'), ':')); // FIXME: strtok shouldn't be used if there is a chance HTTP_HOST may be empty except for the port, e.g., `:80` will return `80`
+        $safe_http_host = preg_replace('/[^a-z\d.:-]/u', '', strtok(getenv('HTTP_HOST'), ':')); // FIXME: strtok shouldn't be used if there is a chance HTTP_HOST may be empty except for the port, e.g., `:80` will return `80`
         // If strtok() matched a ':' in the previous line, the rest of the string contains the port number (or FALSE)
-        $safe_http_port = preg_replace('/[^0-9]/', '', strtok(''));
+        $safe_http_port = preg_replace('/[^0-9]/u', '', strtok(''));
         if ('' != $safe_http_host && '' == $this->getParam('site_hostname')) {
             $this->setParam(array('site_hostname' => $safe_http_host));
@@ -1153,5 +1179,5 @@
                 // Avoid indexed-array query params because in a URL array param keys should all match.
                 // I.e, we want to use `array[]=A&array[]=B` instead of `array[0]=A&array[1]=B`.
-                $key = preg_replace('/\[\d+\]$/', '[]', $key);
+                $key = preg_replace('/\[\d+\]$/u', '[]', $key);
                 // Check value is set and value does not already exist in the url.
                 if (!preg_match('/[?&]' . preg_quote($key) . '=/', $url)) {
@@ -1222,5 +1248,5 @@
 
         // Replace any & not followed by an html or unicode entity with its &amp; equivalent.
-        $url = preg_replace('/&(?![\w\d#]{1,10};)/', '&amp;', $url);
+        $url = preg_replace('/&(?![\w\d#]{1,10};)/u', '&amp;', $url);
 
         return $url;
@@ -1538,5 +1564,5 @@
         if ('' != $url && is_string($url)) {
             // Delete any boomerang request keys in the query string (along with any trailing delimiters after the deletion).
-            $url = preg_replace(array('/([&?])boomerang=[^&?]+[&?]?/', '/[&?]$/'), array('$1', ''), $url);
+            $url = preg_replace(array('/([&?])boomerang=[^&?]+[&?]?/u', '/[&?]$/'), array('$1', ''), $url);
 
             if (isset($_SESSION['_app'][$this->_ns]['boomerang']) && is_array($_SESSION['_app'][$this->_ns]['boomerang']) && !empty($_SESSION['_app'][$this->_ns]['boomerang'])) {
Index: /trunk/lib/PDO.inc.php
===================================================================
--- /trunk/lib/PDO.inc.php	(revision 693)
+++ /trunk/lib/PDO.inc.php	(revision 693)
@@ -0,0 +1,601 @@
+<?php
+/*
+* The Strangecode Codebase - a general application development framework for PHP
+* For details visit the project site: <http://trac.strangecode.com/>
+* Copyright Â© 2019 Strangecode, LLC
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/*
+* PDO.inc.php
+*
+*
+*
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    09 Jul 2019 08:11:03
+*
+* Example of use:
+---------------------------------------------------------------------
+$x = new PDO();
+$x->setParam(array('foo' => 'bar'));
+$x->doIt();
+echo $x->getIt();
+---------------------------------------------------------------------
+*/
+namespace Strangecode\Codebase;
+use \App;
+
+class PDO
+{
+    // A place to keep an object instance for the singleton pattern.
+    protected static $instance = null;
+
+    // If $db->connect has successfully opened a db connection.
+    protected $_connected = false;
+
+    // Database handle.
+    public $dbh;
+
+    // Count how many queries run during the whole instance.
+    protected $_query_count = 0;
+
+    // Hash of DB parameters.
+    protected $_params = array(
+
+        // DB passwords should be set as apache environment variables in httpd.conf, readable only by root.
+        'db_server' => 'localhost',
+        'db_name' => null,
+        'db_user' => null,
+        'db_pass' => null,
+
+        // Display all SQL queries. FALSE recommended for production sites.
+        'db_always_debug' => false,
+
+        // Display db errors. FALSE recommended for production sites.
+        'db_debug' => false,
+
+        // Script stops on db error. TRUE recommended for production sites.
+        'db_die_on_failure' => true,
+
+        // Special date settings. These will dynamically changes depending on MySQL version or settings.
+        'zero_date' => '0000-00-00',
+        'infinity_date' => '9999-12-31',
+
+        // Timezone for MySQL.
+        'timezone' => 'UTC',
+
+        // MySQL character set and collation.
+        'character_set' => '',
+        'collation' => '',
+    );
+
+    // Translate between HTML and MySQL character set names.
+    public $mysql_character_sets = array(
+        'utf-8' => 'utf8',
+        'iso-8859-1' => 'latin1',
+    );
+
+    // Caches.
+    protected static $existing_tables = null;
+    protected static $table_columns = [];
+
+    /**
+     * PDO constructor.
+     *
+     * @access public
+     * @param string $namespace Namespace for this object, used to avoid collisions in global contexts.
+     * @param string $params    Configuration parameters for this object.
+     */
+    public function __construct($params=null)
+    {
+        // Set custom parameters.
+        $this->setParam($params);
+    }
+
+    /**
+     * This method enforces the singleton pattern for this class.
+     *
+     * @return  object  Reference to the global DB object.
+     * @access  public
+     * @static
+     */
+    public static function &getInstance()
+    {
+        if (self::$instance === null) {
+            self::$instance = new self();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * Set the params of this object.
+     *
+     * @access public
+     * @param  array $params   Array of param keys and values to set.
+     */
+    public function setParam($params=null)
+    {
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (isset($this->_params[$param])) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /*
+    * Connect to database with credentials in params.
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    09 Jul 2019 08:16:42
+    */
+    public function connect()
+    {
+        $app =& App::getInstance();
+
+        if (!$this->getParam('db_name') || !$this->getParam('db_user') || !$this->getParam('db_pass')) {
+            $app->logMsg('Database credentials missing.', LOG_EMERG, __FILE__, __LINE__);
+            return false;
+        }
+
+        // If db_server not specified, assume localhost.
+        if (!$this->getParam('db_server')) {
+            $this->setParam(array('db_server' => 'localhost'));
+        }
+
+        // If the mysql charset is not defined, try to determine a mysql charset from the app charset.
+        if ('' == $this->getParam('character_set') && '' != $app->getParam('character_set') && isset($this->mysql_character_sets[mb_strtolower($app->getParam('character_set'))])) {
+            $this->setParam(array('character_set' => $this->mysql_character_sets[mb_strtolower($app->getParam('character_set'))]));
+        }
+
+        try {
+            $dsn = sprintf('mysql:host=%s;dbname=%s;charset=%s', $this->getParam('db_server'), $this->getParam('db_name'), $this->getParam('character_set'));
+            $options = [
+                \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,
+                \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
+                \PDO::ATTR_EMULATE_PREPARES   => false,
+            ];
+            $this->dbh = new \PDO($dsn, $this->getParam('db_user'), $this->getParam('db_pass'), $options);
+        } catch (\PDOException $e) {
+            $mysql_error_msg = sprintf('PDOException: (%s) %s', $e->getCode(), $e->getMessage());
+            // sprintf('PDO error: Could not connect to server (db_server=%s, db_name=%s, db_user=%s, db_pass=%s)', $this->getParam('db_server'), $this->getParam('db_name'), $this->getParam('db_user'), ('' == $this->getParam('db_pass') ? 'NO' : 'YES'));
+            $app->logMsg($mysql_error_msg, LOG_EMERG, __FILE__, __LINE__);
+
+            // Print helpful or pretty error?
+            if ($this->getParam('db_debug')) {
+                echo $mysql_error_msg . "\n";
+            }
+
+            // Die if db_die_on_failure = true, or just continue without connection.
+            return $this->_fail();
+
+            // throw new \PDOException($e->getMessage(), (int)$e->getCode());
+        }
+
+        // DB connection success!
+        $this->_connected = true;
+
+        // Update config for this version of MySQL.
+        if (version_compare($this->dbh->getAttribute(\PDO::ATTR_SERVER_VERSION), '5.7.4', '>=')) {
+            $this->setParam(array('zero_date' => '1000-01-01'));
+        }
+
+        // Set MySQL session timezone.
+        if ($this->getParam('timezone')) {
+            // https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html
+            $this->dbh->query(sprintf("SET time_zone = '%s'", $this->getParam('timezone')));
+        }
+
+        return true;
+    }
+
+    /**
+     * Close db connection.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Aug 2005 14:32:01
+     */
+    public function close()
+    {
+        if (!$this->_connected) {
+            return false;
+        }
+        $this->_connected = false;
+        $this->dbh = null;
+        return true;
+    }
+
+    /*
+    *
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    03 Jul 2013 14:50:23
+    */
+    public function reconnect()
+    {
+        $this->close();
+        $this->connect();
+    }
+
+    /*
+    *
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    09 Jul 2019 10:05:34
+    */
+    public function ping()
+    {
+        if (!$this->_connected) {
+           return false;
+        }
+
+        try {
+            $this->dbh->query('SELECT 1');
+        } catch (\PDOException $e) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /*
+    * Die only if db_die_on_failure is true. This will be set to false for some cases
+    * when a database is not required for web app functionality.
+    *
+    * @access   public
+    * @param    string  $msg Print $msg when dying.
+    * @return   bool    false If we don't die.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Jan 2007 15:59:00
+    */
+    protected function _fail()
+    {
+        $app =& App::getInstance();
+
+        if ($this->getParam('db_die_on_failure')) {
+            if (!$app->isCLI()) {
+                // For http requests, send a Service Unavailable header.
+                header(' ', true, 503);
+                echo _("This page is temporarily unavailable. Please try again in a few minutes.");
+            }
+            die;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns connection status
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Aug 2005 14:58:09
+     */
+    public function isConnected()
+    {
+        return (true === $this->_connected);
+    }
+
+    /*
+    * Return the total number of queries executed thus far.
+    *
+    * @access   public
+    * @return   int Number of queries
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Jun 2006 11:46:05
+    */
+    public function numQueries()
+    {
+        return $this->_query_count;
+    }
+
+    /**
+     * Reset cached items.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Aug 2005 22:10:50
+     */
+    public function resetCache()
+    {
+        self::$existing_tables = null;
+        self::$table_columns = [];
+    }
+
+    /*
+    *
+    *
+    * @access   public
+    * @param    string  $query   The SQL query to execute
+    * @param    bool    $debug   If true, prints debugging info
+    * @return   resource         PDOStatement
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    09 Jul 2019 10:00:00
+    */
+    public function query($query, $debug=false)
+    {
+        $app =& App::getInstance();
+
+        if (!$this->_connected) {
+           return false;
+        }
+
+        $this->_query_count++;
+
+        $debugqry = preg_replace("/\n[\t ]+/u", "\n", $query);
+        if ($this->getParam('db_always_debug') || $debug) {
+            if ($debug > 1) {
+                dump($debugqry, true, SC_DUMP_PRINT_R, __FILE__, __LINE__);
+            } else {
+                echo "<!-- ----------------- Query $this->_query_count ---------------------\n$debugqry\n-->\n";
+            }
+        }
+
+        // Ensure we have an active connection.
+        // If we continue on a dead connection we might experience a "MySQL server has gone away" error.
+        // http://dev.mysql.com/doc/refman/5.0/en/gone-away.html
+        if (!$this->ping()) {
+            $app->logMsg(sprintf('MySQL ping failed; reconnectingâŠ ("%s")', truncate(trim($debugqry), 150)), LOG_DEBUG, __FILE__, __LINE__);
+            $this->reconnect();
+        }
+
+        // Execute!
+        try {
+            $stmt = $this->dbh->query($query);
+        } catch (\PDOException $e) {
+            $mysql_error_msg = sprintf('PDOException: (%s) %s', $e->getCode(), $e->getMessage());
+            $app->logMsg(sprintf('PDOException (%s): %s in query: %s', $e->getCode(), $e->getMessage(), $debugqry), LOG_EMERG, __FILE__, __LINE__);
+            if ($this->getParam('db_debug')) {
+                if (!$app->isCLI()) {
+                    printf('<pre style="padding:2em; background:#ddd; font:9px monospace;">%s<hr>%s</pre>', wordwrap($e->getMessage()), htmlspecialchars($debugqry));
+                }
+            }
+            // Die if db_die_on_failure = true, or just continue without connection
+            return $this->_fail();
+        }
+
+        return $stmt;
+    }
+
+    /*
+    *
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    09 Jul 2019 19:26:37
+    */
+    public function prepare($query, ...$params)
+    {
+        $app =& App::getInstance();
+
+        if (!$this->_connected) {
+           return false;
+        }
+
+        $this->_query_count++;
+
+        $debugqry = preg_replace("/\n[\t ]+/u", "\n", $query);
+        if ($this->getParam('db_always_debug')) {
+            echo "<!-- ----------------- Prepare $this->_query_count ---------------------\n$debugqry\n-->\n";
+        }
+
+        // Ensure we have an active connection.
+        // If we continue on a dead connection we might experience a "MySQL server has gone away" error.
+        // http://dev.mysql.com/doc/refman/5.0/en/gone-away.html
+        if (!$this->ping()) {
+            $app->logMsg(sprintf('MySQL ping failed; reconnectingâŠ ("%s")', truncate(trim($debugqry), 150)), LOG_DEBUG, __FILE__, __LINE__);
+            $this->reconnect();
+        }
+
+        // Execute!
+        try {
+            $stmt = $this->dbh->prepare($query, ...$params);
+        } catch (\PDOException $e) {
+            $mysql_error_msg = sprintf('PDOException: (%s) %s', $e->getCode(), $e->getMessage());
+            $app->logMsg(sprintf('PDOException (%s): %s in prepare: %s', $e->getCode(), $e->getMessage(), $debugqry), LOG_EMERG, __FILE__, __LINE__);
+            if ($this->getParam('db_debug')) {
+                if (!$app->isCLI()) {
+                    printf('<pre style="padding:2em; background:#ddd; font:9px monospace;">%s<hr>%s</pre>', wordwrap($e->getMessage()), htmlspecialchars($debugqry));
+                }
+            }
+            // Die if db_die_on_failure = true, or just continue without connection
+            return $this->_fail();
+        }
+
+        return $stmt;
+    }
+
+    /*
+    *
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    09 Jul 2019 19:42:48
+    */
+    public function lastInsertId($name=null)
+    {
+        return $this->dbh->lastInsertId($name);
+    }
+
+    /*
+    *
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    09 Jul 2019 18:32:55
+    */
+    public function quote(...$params)
+    {
+        return $this->dbh->quote(...$params);
+    }
+
+    /*
+    *
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    09 Jul 2019 18:32:55
+    */
+    static function sanitizeIdentifier($str)
+    {
+        return preg_replace('/\W/u', '', $str);
+    }
+
+    /**
+     * Loads a list of tables in the current database into an array, and returns
+     * true if the requested table is found. Use this function to enable/disable
+     * functionality based upon the current available db tables or to dynamically
+     * create tables if missing.
+     *
+     * @param  string $table                The name of the table to search.
+     * @param  bool   $use_cached_results   Get fresh table info (in case DB changed).
+     * @return bool                         true if given $table exists.
+     */
+    public function tableExists($table, $use_cached_results=true)
+    {
+        $app =& App::getInstance();
+
+        if (!$this->_connected) {
+            return false;
+        }
+
+        if (null === self::$existing_tables || !$use_cached_results) {
+            $stmt = $this->query('SHOW TABLES');
+            self::$existing_tables = $stmt->fetchAll(\PDO::FETCH_COLUMN);
+        }
+
+        if (in_array($table, self::$existing_tables)) {
+            return true;
+        } else {
+            $app->logMsg(sprintf('Nonexistent DB table: %s.%s', $this->getParam('db_name'), $table), LOG_INFO, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Tests if the given array of columns exists in the specified table.
+     *
+     * @param  string $table                The name of the table to search.
+     * @param  array  $columns              An array of column names.
+     * @param  bool   $strict               Exact schema match. If TRUE, test if *only* the given columns exist. If FALSE, test if given columns exist (possibly amongst others).
+     * @param  bool   $use_cached_results   Get fresh table info (in case DB changed).
+     * @return bool                         true if column(s) exist.
+     */
+    public function columnExists($table, $columns, $strict=true, $use_cached_results=true)
+    {
+        $app =& App::getInstance();
+
+        if (!$this->_connected) {
+            $app->logMsg(sprintf('No DB connection to run %s', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Ensure the table exists.
+        if (!$this->tableExists($table, $use_cached_results)) {
+            $app->logMsg(sprintf('Table does not exist: %s', $table), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        // For single-value columns.
+        if (!is_array($columns)) {
+            $columns = array($columns);
+        }
+
+        if (!isset(self::$table_columns[$table]) || !$use_cached_results) {
+            // Populate and cache array of current columns for this table.
+            $stmt = $this->query(sprintf('DESCRIBE `%s`', $this->sanitizeIdentifier($table)));
+            self::$table_columns[$table] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
+        }
+
+        if ($strict) {
+            // Do an exact comparison of table schemas.
+            sort($columns);
+            sort(self::$table_columns[$table]);
+            return self::$table_columns[$table] == $columns;
+        } else {
+            // Only check that the specified columns are available in the table.
+            $match_columns = array_intersect(self::$table_columns[$table], $columns);
+            sort($columns);
+            sort($match_columns);
+            return $match_columns == $columns;
+        }
+    }
+
+    /**
+     * Returns the values of an ENUM or SET column, returning them as an array.
+     *
+     * @param  string $db_table   database table to lookup
+     * @param  string $db_col     database column to lookup
+     * @param  bool   $sort          Sort the output.
+     * @return array    Array of the set/enum values on success, false on failure.
+     */
+    public function getEnumValues($db_table, $db_col, $sort=false)
+    {
+        $app =& App::getInstance();
+
+        $stmt = $this->query(sprintf("SHOW COLUMNS FROM `%s` LIKE %s", $this->sanitizeIdentifier($db_table), $this->dbh->quote($db_col)), false);
+        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
+        if (isset($row['Type']) && preg_match('/^(?:enum|set)\((.*)\)$/i', $row['Type'], $matches) && isset($matches[1]) && '' != $matches[1]) {
+            $enum = str_getcsv($matches[1], ",", "'");
+            if ($sort) {
+                natsort($enum);
+            }
+            return $enum;
+        } else {
+            $app->logMsg(sprintf('No set or enum fields found in %s.%s', $db_table, $db_col), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+    }
+}
