<?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;
		}
	}
