| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- <?php
- /**
- * Timekeeper timer endpoint: timekeeperClientServices/timer.php?user=<name>&game=<process>
- *
- * 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 . "<br/>";
- }
- }
- /**
- * 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();
|