42, 'question' => '6 x 7'] 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 === '') { error_log("TK consume: no token posted (user=$user)"); return null; } $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; } $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; } if (time() - (int) $row['created'] > self::TTL) { error_log("TK consume: expired"); return null; } $payload = json_decode($row['payload'], true); return is_array($payload) ? $payload : null; } }