qcubed/framework

View on GitHub
includes/qform_state_handlers/QDbBackedFormStateHandler.class.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php
    /**
     * This will store the formstate in a pre-specified table in the DB.
     * This offers significant speed advantage over PHP SESSION because EACH form state
     * is saved in its own row in the DB, and only the form state that is needed for loading will
     * be accessed (as opposed to with session, ALL the form states are loaded into memory
     * every time).
     * 
     * The downside is that because it doesn't utilize PHP's session management subsystem,
     * this class must take care of its own garbage collection/deleting of old/outdated
     * formstate files.
     * 
     * Because the index is randomly generated and MD5-hashed, there is no benefit from
     * encrypting it -- therefore, the QForm encryption preferences are ignored when using
     * QFileFormStateHandler.
     * 
     * This handler can handle asynchronous calls.
     *
     * The table used should have the following fields:
     * 1. page_id: varchar(MAX_PAGE_SIZE) - Substitute the maximum size, which depends on your session_id algorithm (MAX_SESSION_SIZE + 33 is safe, see below).
     * 2. save_time: integer
     * 3. state_data: text
     * 4. session_id: varchar(MAX_SESSION_SIZE) - Substitute the maximum session id size, which depends on session id algorithm.
     *    PHP gives you some control over the how you create session ids, so be aware of the maximum size it might generate here.
     *       45 is probably safe for now, but if you add a prefix to your session_ids, then use a bigger number.

     */
    class QDbBackedFormStateHandler extends QBaseClass {

        /**
         * The database index in configuration.inc.php where the formstates have to be managed
         */
        public static $intDbIndex = __DB_BACKED_FORM_STATE_HANDLER_DB_INDEX__;

        /**
         * The table name which will handle the formstates. It must have the following columns:
         */
        public static $strTableName = __DB_BACKED_FORM_STATE_HANDLER_TABLE_NAME__;
        /**
         * The interval of hits before the garbage collection should kick in to delete
         * old FormState files, or 0 if it should never be run.  The higher the number,
         * the less often it runs (better aggregated-average performance, but requires more
         * hard drive space).  The lower the number, the more often it runs (slower aggregated-average
         * performance, but requires less hard drive space).
         * @var integer GarbageCollectInterval
         */
        public static $intGarbageCollectOnHitCount = 20000;

        /**
         * The minimum age (in days) a formstate file has to be in order to be considered old enough
         * to be garbage collected.  So if set to "1.5", then all formstate files older than 1.5 days
         * will be deleted when the GC interval is kicked off.
         * Obviously, if the GC Interval is set to 0, then this GC Days Old value will be never used.
         * @var integer GarbageCollectDaysOld
         */
        public static $intGarbageCollectDaysOld = 2;

        /** @var bool Whether to compress the formstate data. */
        public static $blnCompress = true;

        /** @var bool Whether to base64 encode the formstate data. Encoding is required if storing in a TEXT field. */
        public static $blnBase64 = false;


        private static function Initialize() {
            self::$intDbIndex = QType::Cast(self::$intDbIndex, QType::Integer);
            self::$strTableName = QType::Cast(self::$strTableName, QType::String);

            // If the database index exists
            if (!array_key_exists(self::$intDbIndex, QApplication::$Database)) {
                throw new QCallerException('No database defined at DB_CONNECTION index ' . self::$intDbIndex . '. Correct your settings in configuration.inc.php.');
            }
            $objDatabase = QApplication::$Database[self::$intDbIndex];
            // see if the database contains a table with desired name
            if (!in_array(self::$strTableName, $objDatabase->GetTables())) {
                throw new QCallerException('Table ' . self::$strTableName . ' not found in database at DB_CONNECTION index ' . self::$intDbIndex . '. Correct your settings in configuration.inc.php.');
            }
        }

        /**
         * @static
         * This function is responsible for removing the old values from
         */
        public static function GarbageCollect() {
            // Its not perfect and not sure but should be executed on expected intervals
            $objDatabase = QApplication::$Database[self::$intDbIndex];
            $query = '
                                    DELETE FROM
                                            ' . $objDatabase->EscapeIdentifier(self::$strTableName) . '
                                    WHERE
                                                ' . $objDatabase->EscapeIdentifier('save_time') . ' < ' . $objDatabase->SqlVariable(time() - 60 * 60 * 24 * self::$intGarbageCollectDaysOld);

            $objDatabase->NonQuery($query);
        }

        /**
         * @static
         * If PHP SESSION is enabled, then this method will delete all formstate files specifically
         * for this SESSION user (and no one else).  This can be used in lieu of or in addition to the
         * standard interval-based garbage collection mechanism.
         * Also, for standard web applications with logins, it might be a good idea to call
         * this method whenever the user logs out.
         */

        public static function DeleteFormStateForSession() {
            // Figure Out Session Id (if applicable)
            $strSessionId = session_id();

            //Get database
            $objDatabase = QApplication::$Database[self::$intDbIndex];
            // Create the query
            $query = '
                                DELETE FROM
                                        ' . $objDatabase->EscapeIdentifier(self::$strTableName) . '
                                WHERE
                                        ' . $objDatabase->EscapeIdentifier('session_id') . ' = ' . $objDatabase->SqlVariable($strSessionId);

            $result = $objDatabase->NonQuery($query);
        }

        /**
         * @static
         *
         * @param $strFormState
         * @param $blnBackButtonFlag
         *
         * @return string
         */
        public static function Save($strFormState, $blnBackButtonFlag) {
            $objDatabase = QApplication::$Database[self::$intDbIndex];
            $strOriginal = $strFormState;

            // compress (if available)
            if (function_exists('gzcompress') && self::$blnCompress) {
                $strFormState = gzcompress($strFormState, 9);
            }

            if (defined('__DB_BACKED_FORM_STATE_HANDLER_ENCRYPTION_KEY__')) {
                try {
                    $crypt = new QCryptography(__DB_BACKED_FORM_STATE_HANDLER_ENCRYPTION_KEY__, false, null, __DB_BACKED_FORM_STATE_HANDLER_HASH_KEY__);
                    $strFormState = $crypt->Encrypt($strFormState);
                }
                catch(Exception $e) {
                }
            }

            if (self::$blnBase64) {
                $encoded = base64_encode($strFormState);
                if ($strFormState && !$encoded) {
                    throw new Exception ("Base64 Encoding Failed on " . $strOriginal);
                }
                else {
                    $strFormState = $encoded;
                }
            }

            if (!empty($_POST['Qform__FormState']) && QApplication::$RequestMode == QRequestMode::Ajax) {
                // update the current form state if possible
                $strPageId = $_POST['Qform__FormState'];

                $strQuery = '
                                UPDATE
                                        ' . $objDatabase->EscapeIdentifier(self::$strTableName) . '
                                SET
                                        ' . $objDatabase->EscapeIdentifier('save_time') . ' = ' . $objDatabase->SqlVariable(time()) . ',
                                        ' . $objDatabase->EscapeIdentifier('state_data') . ' = ' . $objDatabase->SqlVariable($strFormState) . '
                                WHERE
                                        ' . $objDatabase->EscapeIdentifier('page_id') . ' = ' . $objDatabase->SqlVariable($strPageId);

                $objDatabase->NonQuery($strQuery);
                if ($objDatabase->AffectedRows > 0) {
                    return $strPageId;    // successfully updated the current record. No need to create a new one.
                }
            }
            // First see if we need to perform garbage collection
            // Decide for garbage collection
            if ((self::$intGarbageCollectOnHitCount > 0) && (rand(1, self::$intGarbageCollectOnHitCount) == 1)) {
                self::GarbageCollect();
            }

            //*/

            // Figure Out Session Id (if applicable)
            $strSessionId = session_id();

            // Calculate a new unique Page Id
            $strPageId = md5(microtime());

            // Figure Out Page ID to be saved onto the database
            $strPageId = sprintf('%s_%s',
                $strSessionId,
                $strPageId);

            // Save THIS formstate to the database
            //Get database
            // Create the query
            $strQuery = '
                                INSERT INTO
                                        ' . $objDatabase->EscapeIdentifier(self::$strTableName) . '
                                (
                                        ' . $objDatabase->EscapeIdentifier('page_id') . ',
                                        ' . $objDatabase->EscapeIdentifier('session_id') . ',
                                        ' . $objDatabase->EscapeIdentifier('save_time') . ',
                                        ' . $objDatabase->EscapeIdentifier('state_data') . '
                                )
                                VALUES
                                (
                                        ' . $objDatabase->SqlVariable($strPageId) . ',
                                        ' . $objDatabase->SqlVariable($strSessionId) . ',
                                        ' . $objDatabase->SqlVariable(time()) . ',
                                        ' . $objDatabase->SqlVariable($strFormState) . '
                                )';

            $result = $objDatabase->NonQuery($strQuery);

            // Return the Page Id
            // Because of the MD5-random nature of the Page ID, there is no need/reason to encrypt it
            return $strPageId;
        }

        public static function Load($strPostDataState) {
            // Pull Out strPageId
            $strPageId = $strPostDataState;

            //Get database
            $objDatabase = QApplication::$Database[self::$intDbIndex];
            // The query to run
            $strQuery = '
                                SELECT
                                        ' . $objDatabase->EscapeIdentifier('state_data') . '
                FROM
                                        ' . $objDatabase->EscapeIdentifier(self::$strTableName) . '
                                WHERE
                                        ' . $objDatabase->EscapeIdentifier('page_id') . ' = ' . $objDatabase->SqlVariable($strPageId);

            if ($strSessionId = session_id()) {
                $strQuery .= ' AND ' . $objDatabase->EscapeIdentifier('session_id') . ' = ' . $objDatabase->SqlVariable($strSessionId);
            }


            // Perform the Query
            $objDbResult = $objDatabase->Query($strQuery);

            $strFormStateRow = $objDbResult->FetchRow()[0];

            if (empty($strFormStateRow)) {
                // The formstate with that page ID was not found, or session expired.
                return null;
            }
            $strSerializedForm = $strFormStateRow;


            if (self::$blnBase64) {
                $strSerializedForm = base64_decode($strSerializedForm);

                if ($strSerializedForm === false) {
                    throw new Exception("Failed decoding formstate " . $strSerializedForm);
                }
            }

            if (defined('__DB_BACKED_FORM_STATE_HANDLER_ENCRYPTION_KEY__')) {
                try {
                    $crypt = new QCryptography(__DB_BACKED_FORM_STATE_HANDLER_ENCRYPTION_KEY__, false, null, __DB_BACKED_FORM_STATE_HANDLER_HASH_KEY__);
                    $strSerializedForm = $crypt->Decrypt($strSerializedForm);
                }
                catch(Exception $e) {
                }
            }

            if (function_exists('gzcompress') && self::$blnCompress) {
                try {
                    $strSerializedForm = gzuncompress($strSerializedForm);
                } catch (Exception $e) {
                    print ("Error on uncompress of page id " . $strPageId);
                    throw $e;
                }
            }

            return $strSerializedForm;
        }
    }