&game= * * Called once a minute by timekeeperLocal.php on the student's machine. The * reply is either the number of play minutes remaining (a number) or a * message explaining why play isn't allowed (any non-number: the client * shows it and closes the game). * * Changes from the current repo version: * - ?user= and ?game= were concatenated straight into SQL with no login * required (SQL injection), and ?user= was also used to build a file name * for debug output (arbitrary file write). Both are now validated against * a strict pattern and escaped, and the debug file is gone (that's what * the *debug.dat files in the repo were) * - unknown users are only auto-created if they have an e107 account (any * visitor could create rows by requesting ?user=anything) * - removed the leftover "reply 31 until 2026-05-13" override * - counters are updated with atomic UPDATE ... SET x = x + 1 instead of * writing back a value read earlier in the request * - when the free minutes run out the game was killed even if the student * had earned minutes: remaining free time of exactly 0 replied "0". * Earned minutes now take over, and the reply is 0 (never negative) * once there are none, so the client keeps closing the game * - new ?peek=1: reply with the minutes remaining WITHOUT using one up. * The client calls it when it first sees a game so the first minute is no * longer charged twice * - plain-text response header * - a student with Timekeeper switched off gets 1440 (was 30, which the * Java client would read as a real 30 minutes and warn about every minute) */ if (!defined('e107_INIT')) { require_once('../../../class2.php'); } if (!e107::isInstalled('timekeeper')) { e107::redirect(); exit; } /** * The Timekeeper's timer connection class. * * This class checks if a user has the right to play games and if not states why. * This is an adapter to the local Timekeeper application monitor. * * @author gregory */ class timer { private $user = null; private $game = ''; private $invalid = false; private $peek = false; private $debug = false; private $sql; private $settings; function __construct() { $user = filter_input(INPUT_GET, 'user'); if (is_string($user) && preg_match('/^[A-Za-z0-9_.-]{1,50}$/', $user)) { $this->user = $user; } else { $this->invalid = true; } $game = filter_input(INPUT_GET, 'game'); if (is_string($game) && $game !== '') { if (preg_match('/^[\w.+: ()\-]{1,100}$/', $game)) { $this->game = $game; } else { $this->invalid = true; } } $this->peek = filter_input(INPUT_GET, 'peek') ? true : false; $this->debug = filter_input(INPUT_GET, 'dbg') ? true : false; $this->sql = e107::getDb(); date_default_timezone_set('Australia/Perth'); } private function q($value) { return $this->sql->escape($value); } private function debug($text) { if ($this->debug) { echo "Entered " . $text . "
"; } } /** * Main entry point. */ public function main() { if (!headers_sent()) { header('Content-Type: text/plain; charset=utf-8'); } if ($this->invalid || $this->user === null) { echo 'Invalid request.'; return; } $this->debug(__FUNCTION__); // load the settings and init values from the database $this->getSettings(); // Check if user is allowed to play games, if not reply and exit now if (!$this->isTimekeeperOperable()) { return; } if (!$this->checkPlayTime()) { return; } // If a game is being played then update the statistics database if ($this->charging()) { $this->playTimeStatistics(); } } /** * Is this request supposed to use up a minute of play time? */ private function charging() { return $this->game !== '' && !$this->peek; } private function getSettings() { $this->debug(__FUNCTION__); $this->settings = $this->sql->retrieve('timekeeper', '*', 'username="' . $this->q($this->user) . '"'); } /** * Should the timekeeper be operable for this user right now? * * @return boolean */ private function isTimekeeperOperable() { $this->debug(__FUNCTION__); if (is_array($this->settings) && isset($this->settings['enabled'])) { // timekeeper switched off for this user: no limit. The reply is a large number // (the Java client treats the number literally and warns at exactly 30 minutes). if (!$this->settings['enabled']) { echo 1440; return false; } } else { // Only people with an e107 account get a settings row. if ($this->sql->count('user', '(*)', 'user_loginname="' . $this->q($this->user) . '"') < 1) { echo 'Unknown user.'; return false; } $query = ['username' => $this->user, 'enabled' => 1, 'freeMins' => 10, 'enableTime' => 10.00, 'disableTime' => 22.00, 'minQuestionCount' => 100, 'usedMinutes' => 0, 'lastDate' => 0, 'maxDailyPlayTime' => 1440]; $this->sql->insert('timekeeper', $query); echo $this->user . ' added to timekeepers database.'; // exit here as the class variable would not be setup for the user. exit; } // Check if user is within allowed play times if (!$this->playTimeWindow()) { echo 'You are not within allowed play times.'; return false; } // Check has the user answered enough questions today. if (!$this->minimumQuestionsMet()) { if ($this->settings['freeMins'] >= 1) { return true; } echo 'You have not answered enough questions to play.'; return false; } return true; } /** * Is the time of day within the play window? * * @return boolean */ private function playTimeWindow() { $this->debug(__FUNCTION__); if (isset($this->settings['enableTime']) && isset($this->settings['disableTime'])) { // Disable time is less than enable time consider it the next day if ((double) date('H.i') <= (double) $this->settings['disableTime'] && (double) $this->settings['disableTime'] <= (double) $this->settings['enableTime']) { return true; } // Check if the enable time has been met, return false if not if ((double) date('H.i') <= (double) $this->settings['enableTime']) { return false; } // Check if the disable time has been exceeded (not when disable < enable) if ((double) date('H.i') >= (double) $this->settings['disableTime'] && !((double) $this->settings['disableTime'] <= (double) $this->settings['enableTime'])) { return false; } } return true; } /** * Has the user answered at least minQuestionCount questions today? * * @return boolean */ private function minimumQuestionsMet() { $this->debug(__FUNCTION__); if (isset($this->settings['minQuestionCount'])) { $answered = (int) $this->sql->retrieve('tkReport', 'COUNT(id)', 'username="' . $this->q($this->user) . '" ' . 'AND answereddatestamp >= ' . (int) mktime(0, 0, 0, date('n'), date('j')) . ' ' . 'AND answereddatestamp <= ' . (int) mktime(23, 59, 59, date('n'), date('j'))); if ($answered < (int) $this->settings['minQuestionCount']) { return false; } } return true; } /** * Work out the minutes remaining, and (unless peeking) use one up. * * Free minutes (freeMins minus today's usedMinutes) are used first; once * they are gone the minutes earned from answering questions are used. * * @return boolean False when the user may not play now (reply already sent). */ private function checkPlayTime() { $this->debug(__FUNCTION__); $s = $this->settings; if (!isset($s['freeMins']) || !isset($s['usedMinutes']) || !isset($s['lastDate']) || !isset($s['maxDailyPlayTime'])) { return true; } // New day: today's counter starts again from zero. $isToday = ((int) $s['lastDate'] > 0) && date('Ymd', (int) $s['lastDate']) === date('Ymd'); $used = $isToday ? (int) $s['usedMinutes'] : 0; if ($used >= (int) $s['maxDailyPlayTime']) { echo 'Max play time exceeded.'; return false; } $free = (int) $s['freeMins'] - $used; if ($free > 0) { // Free play time left: use it before earned minutes. $this->useMinute($isToday, false); echo $free; return true; } // Free time is gone, so the questions-answered rule applies. if (!$this->minimumQuestionsMet()) { echo 'You have not answered enough questions to play.'; return false; } $earned = (int) $this->sql->retrieve('timekeeperScore', 'timekeeper_minutes', 'timekeeper_username="' . $this->q($this->user) . '"'); if ($earned <= 0) { echo 0; return false; } $this->useMinute($isToday, true); echo $earned; return true; } /** * Use up one minute of play time (no-op when peeking or no game given). * * @param boolean $isToday Whether the stored counter belongs to today. * @param boolean $fromScore Also take the minute off the earned minutes. */ private function useMinute($isToday, $fromScore) { if (!$this->charging()) { return; } $u = $this->q($this->user); if ($isToday) { $this->sql->gen("UPDATE `" . MPREFIX . "timekeeper` SET `usedMinutes` = `usedMinutes` + 1 WHERE `username` = '{$u}'"); } else { $this->sql->gen("UPDATE `" . MPREFIX . "timekeeper` SET `lastDate` = " . (int) time() . ", `usedMinutes` = 1 WHERE `username` = '{$u}'"); } if ($fromScore) { $this->sql->gen("UPDATE `" . MPREFIX . "timekeeperScore` SET `timekeeper_minutes` = `timekeeper_minutes` - 1 WHERE `timekeeper_username` = '{$u}' AND `timekeeper_minutes` > 0"); } } /** * Update the statistics for games played and for how long. */ private function playTimeStatistics() { $this->debug(__FUNCTION__); $where = 'userName="' . $this->q($this->user) . '" AND gameName="' . $this->q($this->game) . '" AND playDate="' . (int) mktime(12, 0, 0, date('n'), date('j')) . '"'; $playMinutes = (int) $this->sql->retrieve('timekeeperStatistics', 'minutesPlayed', $where); if ($playMinutes === 0) { $this->sql->insert('timekeeperStatistics', ['uniqueId' => 0, 'minutesPlayed' => 1, 'userName' => $this->user, 'gameName' => $this->game, 'playDate' => mktime(12, 0, 0, date('n'), date('j'))]); } else { $this->sql->update('timekeeperStatistics', ['minutesPlayed' => ($playMinutes + 1), 'WHERE' => $where]); } } } //run the timer class as standalone $timerClass = new timer(); $timerClass->main();