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