| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081 |
- <?php
- /**
- * Server-side storage for the expected answer of the question a user is
- * currently looking at.
- *
- * 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.
- *
- * Table: tkPendingAnswer (username PK, token, payload, created) - see
- * timekeeper_sql.php. One pending row per user.
- */
- class TkAnswerStore
- {
- const TABLE = 'tkPendingAnswer';
- const TTL = 7200; // seconds a question stays answerable
- /**
- * Store the expected-answer payload for $user and return the token to
- * embed in the form's hidden `checkerData` field.
- *
- * @param string $user
- * @param array $payload e.g. ['answer1' => 42, 'question' => [...]] or ['word' => 'because']
- * @return string
- */
- public static function issue($user, array $payload)
- {
- $db = e107::getDb();
- $u = $db->escape($user);
- // Replace this user's pending row and sweep expired ones.
- $db->delete(self::TABLE, "username = '{$u}' OR created < " . (time() - self::TTL));
- $token = bin2hex(random_bytes(16));
- $db->insert(self::TABLE, [
- 'username' => $user,
- 'token' => $token,
- 'payload' => json_encode($payload),
- 'created' => time(),
- ]);
- return $token;
- }
- /**
- * Validate the posted token and return the payload, consuming it so the
- * same question cannot be scored twice. Returns null if the token is
- * missing, wrong (e.g. stale browser tab), expired, or already used.
- *
- * @param string $user
- * @param string|null $token value of POST `checkerData`
- * @return array|null
- */
- public static function consume($user, $token)
- {
- if (!is_string($token) || $token === '') {
- return null;
- }
- $db = e107::getDb();
- $u = $db->escape($user);
- $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
- }
- // 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) {
- return null;
- }
- $payload = json_decode($row['payload'], true);
- return is_array($payload) ? $payload : null;
- }
- }
|