|
|
@@ -3,23 +3,12 @@
|
|
|
* Server-side storage for the expected answer of the question a user is
|
|
|
* currently looking at.
|
|
|
*
|
|
|
- * Before: the generators put the correct answer in a hidden `checkerData`
|
|
|
- * field and the checker trusted whatever came back, so a student could edit
|
|
|
- * it. Now the payload stays on the server and the form only carries an opaque
|
|
|
- * single-use token (still posted as `checkerData`, so form markup is unchanged).
|
|
|
+ * The generators store the expected-answer payload here and put only an
|
|
|
+ * opaque single-use token in the form's hidden `checkerData` field, so a
|
|
|
+ * student cannot edit the answer client-side.
|
|
|
*
|
|
|
- * Schema (add to timekeeper_sql.php in e107's format, or run once by hand with
|
|
|
- * the e107_ prefix on the table name):
|
|
|
- *
|
|
|
- * CREATE TABLE tkPendingAnswer (
|
|
|
- * username varchar(100) NOT NULL,
|
|
|
- * token char(32) NOT NULL,
|
|
|
- * payload text NOT NULL,
|
|
|
- * created int(10) unsigned NOT NULL,
|
|
|
- * PRIMARY KEY (username)
|
|
|
- * ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
|
|
- *
|
|
|
- * One pending row per user, matching the existing one-open-question model.
|
|
|
+ * Table: tkPendingAnswer (username PK, token, payload, created) - see
|
|
|
+ * timekeeper_sql.php. One pending row per user.
|
|
|
*/
|
|
|
class TkAnswerStore
|
|
|
{
|
|
|
@@ -31,7 +20,7 @@ class TkAnswerStore
|
|
|
* embed in the form's hidden `checkerData` field.
|
|
|
*
|
|
|
* @param string $user
|
|
|
- * @param array $payload e.g. ['answer1' => 42, 'question' => '6 x 7'] or ['word' => 'because']
|
|
|
+ * @param array $payload e.g. ['answer1' => 42, 'question' => [...]] or ['word' => 'because']
|
|
|
* @return string
|
|
|
*/
|
|
|
public static function issue($user, array $payload)
|
|
|
@@ -62,37 +51,31 @@ class TkAnswerStore
|
|
|
* @param string|null $token value of POST `checkerData`
|
|
|
* @return array|null
|
|
|
*/
|
|
|
- public static function consume($user, $token)
|
|
|
-{
|
|
|
- if (!is_string($token) || $token === '') {
|
|
|
- error_log("TK consume: no token posted (user=$user)");
|
|
|
- return null;
|
|
|
- }
|
|
|
+ public static function consume($user, $token)
|
|
|
+ {
|
|
|
+ if (!is_string($token) || $token === '') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
|
|
|
- $db = e107::getDb();
|
|
|
- $u = $db->escape($user);
|
|
|
+ $db = e107::getDb();
|
|
|
+ $u = $db->escape($user);
|
|
|
|
|
|
- $row = $db->retrieve(self::TABLE, 'token, payload, created', "username = '{$u}'");
|
|
|
- if (!is_array($row)) {
|
|
|
- error_log("TK consume: no pending row for user=$user");
|
|
|
- return null;
|
|
|
- }
|
|
|
- if (!hash_equals((string) $row['token'], $token)) {
|
|
|
- error_log("TK consume: token mismatch user=$user posted=" . substr($token, 0, 40));
|
|
|
- return null;
|
|
|
- }
|
|
|
+ $row = $db->retrieve(self::TABLE, 'token, payload, created', "username = '{$u}'");
|
|
|
+ if (!is_array($row) || !hash_equals((string) $row['token'], $token)) {
|
|
|
+ return null; // wrong token: leave the real pending question intact
|
|
|
+ }
|
|
|
|
|
|
- $deleted = $db->delete(self::TABLE, "username = '{$u}' AND token = '" . $db->escape($row['token']) . "'");
|
|
|
- error_log("TK consume: delete returned " . var_export($deleted, true));
|
|
|
- // Temporarily ignore the delete result while diagnosing:
|
|
|
- // if (!$deleted) { return null; }
|
|
|
+ // Single use: delete() returns the affected-row count, so of two
|
|
|
+ // simultaneous submits only the first gets 1 here.
|
|
|
+ if (!$db->delete(self::TABLE, "username = '{$u}' AND token = '" . $db->escape($row['token']) . "'")) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
|
|
|
- if (time() - (int) $row['created'] > self::TTL) {
|
|
|
- error_log("TK consume: expired");
|
|
|
- return null;
|
|
|
- }
|
|
|
+ if (time() - (int) $row['created'] > self::TTL) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
|
|
|
- $payload = json_decode($row['payload'], true);
|
|
|
- return is_array($payload) ? $payload : null;
|
|
|
-}
|
|
|
+ $payload = json_decode($row['payload'], true);
|
|
|
+ return is_array($payload) ? $payload : null;
|
|
|
+ }
|
|
|
}
|