1: <?php
2: /**
3: * This will store the formstate in a pre-specified table in the DB.
4: * This offers significant speed advantage over PHP SESSION because EACH form state
5: * is saved in its own row in the DB, and only the form state that is needed for loading will
6: * be accessed (as opposed to with session, ALL the form states are loaded into memory
7: * every time).
8: *
9: * The downside is that because it doesn't utilize PHP's session management subsystem,
10: * this class must take care of its own garbage collection/deleting of old/outdated
11: * formstate files.
12: *
13: * Because the index is randomly generated and MD5-hashed, there is no benefit from
14: * encrypting it -- therefore, the QForm encryption preferences are ignored when using
15: * QFileFormStateHandler.
16: *
17: * This handler can handle asynchronous calls.
18: */
19: class QDbBackedFormStateHandler extends QBaseClass {
20:
21: /**
22: * The database index in configuration.inc.php where the formstates have to be managed
23: */
24: public static $intDbIndex = __DB_BACKED_FORM_STATE_HANDLER_DB_INDEX__;
25:
26: /**
27: * The table name which will handle the formstates. It must have the following columns:
28: * 1. page_id: varchar(80)
29: * 2. save_time: integer
30: * 3. state_data: text
31: * 4. session_id: varchar(32)
32: */
33: public static $strTableName = __DB_BACKED_FORM_STATE_HANDLER_TABLE_NAME__;
34: /**
35: * The interval of hits before the garbage collection should kick in to delete
36: * old FormState files, or 0 if it should never be run. The higher the number,
37: * the less often it runs (better aggregated-average performance, but requires more
38: * hard drive space). The lower the number, the more often it runs (slower aggregated-average
39: * performance, but requires less hard drive space).
40: * @var integer GarbageCollectInterval
41: */
42: public static $intGarbageCollectOnHitCount = 20000;
43:
44: /**
45: * The minimum age (in days) a formstate file has to be in order to be considered old enough
46: * to be garbage collected. So if set to "1.5", then all formstate files older than 1.5 days
47: * will be deleted when the GC interval is kicked off.
48: * Obviously, if the GC Interval is set to 0, then this GC Days Old value will be never used.
49: * @var integer GarbageCollectDaysOld
50: */
51: public static $intGarbageCollectDaysOld = 2;
52:
53: private static function Initialize() {
54: self::$intDbIndex = QType::Cast(self::$intDbIndex, QType::Integer);
55: self::$strTableName = QType::Cast(self::$strTableName, QType::String);
56:
57: // If the database index exists
58: if (!array_key_exists(self::$intDbIndex, QApplication::$Database)) {
59: throw new QCallerException('No database defined at DB_CONNECTION index ' . self::$intDbIndex . '. Correct your settings in configuration.inc.php.');
60: }
61: $objDatabase = QApplication::$Database[self::$intDbIndex];
62: // see if the database contains a table with desired name
63: if (!in_array(self::$strTableName, $objDatabase->GetTables())) {
64: throw new QCallerException('Table ' . self::$strTableName . ' not found in database at DB_CONNECTION index ' . self::$intDbIndex . '. Correct your settings in configuration.inc.php.');
65: }
66: }
67:
68: /**
69: * @static
70: * This function is responsible for removing the old values from
71: */
72: public static function GarbageCollect() {
73: // Its not perfect and not sure but should be executed on expected intervals
74: $objDatabase = QApplication::$Database[self::$intDbIndex];
75: $query = '
76: DELETE FROM
77: ' . $objDatabase->EscapeIdentifier(self::$strTableName) . '
78: WHERE
79: ' . $objDatabase->EscapeIdentifier('save_time') . ' < ' . $objDatabase->SqlVariable(time() - 60 * 60 * 24 * self::$intGarbageCollectDaysOld);
80:
81: $objDatabase->NonQuery($query);
82: }
83:
84: /**
85: * @static
86: * If PHP SESSION is enabled, then this method will delete all formstate files specifically
87: * for this SESSION user (and no one else). This can be used in lieu of or in addition to the
88: * standard interval-based garbage collection mechanism.
89: * Also, for standard web applications with logins, it might be a good idea to call
90: * this method whenever the user logs out.
91: */
92:
93: public static function DeleteFormStateForSession() {
94: // Figure Out Session Id (if applicable)
95: $strSessionId = session_id();
96:
97: //Get database
98: $objDatabase = QApplication::$Database[self::$intDbIndex];
99: // Create the query
100: $query = '
101: DELETE FROM
102: ' . $objDatabase->EscapeIdentifier(self::$strTableName) . '
103: WHERE
104: ' . $objDatabase->EscapeIdentifier('session_id') . ' = ' . $objDatabase->SqlVariable($strSessionId);
105:
106: $result = $objDatabase->NonQuery($query);
107: }
108:
109: /**
110: * @static
111: *
112: * @param $strFormState
113: * @param $blnBackButtonFlag
114: *
115: * @return string
116: */
117: public static function Save($strFormState, $blnBackButtonFlag) {
118: $objDatabase = QApplication::$Database[self::$intDbIndex];
119:
120: // compress (if available)
121: if (function_exists('gzcompress')) {
122: $strFormState = gzcompress($strFormState, 9);
123: }
124:
125: if (!empty($_POST['Qform__FormState']) && QApplication::$RequestMode == QRequestMode::Ajax) {
126: // update the current form state if possible
127: $strPageId = $_POST['Qform__FormState'];
128:
129: $strQuery = '
130: UPDATE
131: ' . $objDatabase->EscapeIdentifier(self::$strTableName) . '
132: SET
133: ' . $objDatabase->EscapeIdentifier('save_time') . ' = ' . $objDatabase->SqlVariable(time()) . ',
134: ' . $objDatabase->EscapeIdentifier('state_data') . ' = ' . $objDatabase->SqlVariable(base64_encode($strFormState)) . '
135: WHERE
136: ' . $objDatabase->EscapeIdentifier('page_id') . ' = ' . $objDatabase->SqlVariable($strPageId);
137:
138: $objDatabase->NonQuery($strQuery);
139: if ($objDatabase->AffectedRows > 0) {
140: return $strPageId; // successfully updated the current record. No need to create a new one.
141: }
142: }
143: // First see if we need to perform garbage collection
144: // Decide for garbage collection
145: if ((self::$intGarbageCollectOnHitCount > 0) && (rand(1, self::$intGarbageCollectOnHitCount) == 1)) {
146: self::GarbageCollect();
147: }
148:
149: //*/
150:
151: // Figure Out Session Id (if applicable)
152: $strSessionId = session_id();
153:
154: // Calculate a new unique Page Id
155: $strPageId = md5(microtime());
156:
157: // Figure Out Page ID to be saved onto the database
158: $strPageId = sprintf('%s_%s',
159: $strSessionId,
160: $strPageId);
161:
162: // Save THIS formstate to the database
163: //Get database
164: // Create the query
165: $strQuery = '
166: INSERT INTO
167: ' . $objDatabase->EscapeIdentifier(self::$strTableName) . '
168: (
169: ' . $objDatabase->EscapeIdentifier('page_id') . ',
170: ' . $objDatabase->EscapeIdentifier('session_id') . ',
171: ' . $objDatabase->EscapeIdentifier('save_time') . ',
172: ' . $objDatabase->EscapeIdentifier('state_data') . '
173: )
174: VALUES
175: (
176: ' . $objDatabase->SqlVariable($strPageId) . ',
177: ' . $objDatabase->SqlVariable($strSessionId) . ',
178: ' . $objDatabase->SqlVariable(time()) . ',
179: ' . $objDatabase->SqlVariable(base64_encode($strFormState)) . '
180: )';
181:
182: $result = $objDatabase->NonQuery($strQuery);
183:
184: // Return the Page Id
185: // Because of the MD5-random nature of the Page ID, there is no need/reason to encrypt it
186: return $strPageId;
187: }
188:
189: public static function Load($strPostDataState) {
190: // Pull Out strPageId
191: $strPageId = $strPostDataState;
192:
193: //Get database
194: $objDatabase = QApplication::$Database[self::$intDbIndex];
195: // The query to run
196: $strQuery = '
197: SELECT
198: ' . $objDatabase->EscapeIdentifier('state_data') . '
199: FROM
200: ' . $objDatabase->EscapeIdentifier(self::$strTableName) . '
201: WHERE
202: ' . $objDatabase->EscapeIdentifier('page_id') . ' = ' . $objDatabase->SqlVariable($strPageId);
203:
204: if ($strSessionId = session_id()) {
205: $strQuery .= ' AND ' . $objDatabase->EscapeIdentifier('session_id') . ' = ' . $objDatabase->SqlVariable($strSessionId);
206: }
207:
208:
209: // Perform the Query
210: $objDbResult = $objDatabase->Query($strQuery);
211:
212: $strFormStateRow = $objDbResult->FetchArray();
213:
214: if (empty($strFormStateRow)) {
215: // The formstate with that page ID was not found, or session expired.
216: return null;
217: }
218: $strSerializedForm = $strFormStateRow['state_data'];
219: $strSerializedForm = base64_decode($strSerializedForm);
220:
221: if (function_exists('gzcompress')) {
222:
223: $strSerializedForm = gzuncompress($strSerializedForm);
224: }
225:
226: return $strSerializedForm;
227: }
228: }
229: