Gregory Huxley 1 viikko sitten
vanhempi
sitoutus
4c5028103a

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+timekeeperClientServices/*debug.dat
+audio/
+test.html

+ 16 - 21
PreHeaderFunctions.php

@@ -14,9 +14,11 @@
 class ExcelFunctions {
 
     public function GeneratePdf() {
-        //require_once(e_PLUGIN.'pdf/e107pdf.php');	//require the e107pdf class
-        //$pdf = new e107PDF();// e_IMAGE.'admin_images/true_32.png'  e_IMAGE.'admin_images/false_32.png'
         $user = filter_input(INPUT_GET, 'user');
+        if (!is_string($user) || !preg_match('/^[A-Za-z0-9_-]{1,50}$/', $user)) {
+            return;
+        }  // before any query
+
         $whereDate = $this->whereDate();
         if ($user == "") {
             return;
@@ -29,35 +31,28 @@ class ExcelFunctions {
         $text .= $this->questionsTable($sql, $user, $whereDate);
 
         $text .= "\nMemory use: " . (memory_get_peak_usage(true) / 1024 / 1024) . " MiB.\nGeneration time " . (microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"]) . ' seconds.';
-        file_put_contents('test.html', $text);
+
         $sql->close();
 
-        $stdOut = '';
         $output = filter_input(INPUT_GET, 'output');
         if ($output == 'html') {
             echo $text;
             exit;
         } elseif ($output == 'pdf') {
-            if (!preg_match('/^[A-Za-z0-9_-]{1,50}$/', $user)) {
-                return;
-            }
-            $html = tempnam(sys_get_temp_dir(), 'tk') . '.html';
-            $pdf = tempnam(sys_get_temp_dir(), 'tk') . '.pdf';
+            // pdf branch:
+            $base = tempnam(sys_get_temp_dir(), 'tk');
+            $html = $base . '.html';
+            $pdf = $base . '.pdf';
             file_put_contents($html, $text);
             exec('wkhtmltopdf ' . escapeshellarg($html) . ' ' . escapeshellarg($pdf));
+            header('Content-Type: application/pdf');
+            header('Content-Length: ' . filesize($pdf));
+            readfile($pdf);
+            unlink($html);
+            unlink($pdf);
+            unlink($base);
+            exit;
         }
-
-        $filename = "$user.pdf";
-
-// Header content type 
-        header("Content-type: application/pdf");
-
-        header("Content-Length: " . filesize($filename));
-
-// Send the file to the browser. 
-        readfile($filename);
-
-        exit;
     }
 
     private function coverPage($sql, $user, $whereDate) {

+ 28 - 45
TkAnswerStore.php

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

+ 15 - 5
answerChecker2.php

@@ -99,7 +99,7 @@ class answerChecker2 {
             $this->updateScore(false);
         }
 
-        $this->addToReport($answer['answer1'], $usersAnswer, $subject, str_replace('{ANSWER}', $answer['answer1'], str_replace('{QUESTION}', $answer['question'], $this->messages[$subject][0])));
+        $this->addToReport($answer['answer1'], $usersAnswer, $subject, str_replace('{ANSWER}', $answer['answer1'], str_replace('{QUESTION}', $answer['question'], $this->messages[$subject][0])),$this->isCorrect($answer['answer1'], $usersAnswer));
         e107::getRender()->tablerender(ucfirst($subject) . ' answer check', $text);
     }
 
@@ -128,7 +128,7 @@ class answerChecker2 {
             $this->updateScore(false);
         }
 
-        $this->addToReport($answer['answer1'], $usersAnswer, 'anagrams', str_replace('{ANSWER}', $answer['answer1'], str_replace('{QUESTION}', $answer['question'], $this->messages['anagrams'][0])));
+        $this->addToReport($answer['answer1'], $usersAnswer, 'anagrams', str_replace('{ANSWER}', $answer['answer1'], str_replace('{QUESTION}', $answer['question'], $this->messages['anagrams'][0])),$this->isCorrect($answer['answer1'], $usersAnswer));
         e107::getRender()->tablerender(ucfirst('anagrams') . ' answer check', $text);
     }
 
@@ -162,7 +162,7 @@ class answerChecker2 {
             //file_put_contents('debug_Spelling_data.txt','<->'.$answer['word'].'<->'.$usersAnswer.'<->False'."\n",FILE_APPEND);//=================================
         }
 
-        $this->addToReport($answer['word'], $usersAnswer, 'spelling', "Spell the word " . $answer['word']);
+        $this->addToReport($answer['word'], $usersAnswer, 'spelling', "Spell the word " . $answer['word'],$this->isCorrect($answer['word'], $usersAnswer));
         e107::getRender()->tablerender('Spelling answer check', $text);
     }
 
@@ -230,9 +230,9 @@ class answerChecker2 {
     /**
      * Add the last answered question to the report DB table.
      */
-    private function addToReport($answer, $usersAnswer, $subject, $questionString) {
+    private function addToReport($answer, $usersAnswer, $subject, $questionString, $isCorrect) {
 
-        if ($answer == $usersAnswer) {
+        if ($isCorrect) {
             $correct = 1;
         } else {
             $correct = 0;
@@ -270,4 +270,14 @@ class answerChecker2 {
     }
     return $answer;
 }
+
+private function isCorrect($expected, $given): bool {
+    $expected = trim((string) $expected);
+    $given = trim((string) $given);
+    if (is_numeric($expected) && is_numeric($given)) {
+        return abs((float) $expected - (float) $given) < 1e-9;
+    }
+    return mb_strtolower($expected) === mb_strtolower($given);
+}
+
 }

+ 1 - 1
questionGenerators/math/numeracy2.php

@@ -191,4 +191,4 @@ class numeracy2 {
 
         return $fString;
     }
-}
+}

+ 1 - 1
timekeeperClientServices/silasdebug.dat

@@ -1 +1 @@
--22-
+-0-