~skye/poker/src/db.php

view raw


<?php

/* represent a user from the database */
class User {
	public $id;
	public $name;
	public $hash;
	public $info;

	public function __construct($i, $n, $h, $in) {
		$this->id = $i;
		$this->name = $n;
		$this->hash = $h;
		$this->info = $in;
	}

	/* decode an sql result into a user */
	public static function from_sql($x): User {
		return new User(
			$x["id"],
			$x["name"],
			$x["hash"],
			$x["info"]
		);
	}

}

class Info {
	public $id;
	public $uid;
	public $about;
	public $games;
	public $wins;
	public $losses;
	public $highest;

	public function __construct(
		$i, $u, $a, $g, $w, $l, $h
	) {
		$this->id = (integer)$i;
		$this->uid = (integer)$u;
		$this->about = $a;
		$this->games = $g;
		$this->wins = $w;
		$this->losses = $l;
		$this->highest = $h;
	}

	public static function from_sql($x): Info {
		$g = fn($x, $k) =>
			isset($x[$k])
				? $x[$k]
				: null;
		eputs("games: " . $g($x, "games"));
		return new Info(
			$g($x, "id"),
			$g($x, "uid"),
			$g($x, "about"),
			$g($x, "games"),
			$g($x, "wins"),
			$g($x, "losses"),
			$g($x, "highest")
		);
	}

	public static function empty(): Info {
		return new Info(
			null, null, null, null,
			null, null, null
		);
	}

	public function into_array(): array {
		return [
			"id" => $this->id,
			"uid" => $this->uid,
			"about" => $this->about,
			"games" => $this->games,
			"wins" => $this->wins,
			"losses" => $this->losses,
			"highest" => $this->highest
		];
	}
}

class Database {
	public $db;

	/* constructor with the main db as the default file */
	public function __construct($f = "run/poker.db") {
		$this->db = new SQLite3($f);
		$q = <<<SQL
		create table if not exists users (
			id integer primary key autoincrement,
			name text not null,     
			hash text not null,
			info integer
		);
		create table if not exists info (
			id integer primary key autoincrement,
			uid integer not null,
			about text,
			games integer,
			wins integer,
			losses integer,
			highest integer
		);
		SQL;

		eputs($q);
		$this->db->exec($q);
	}

	/* run a query on the database.
	 * we wrap this function so that we can do things like sanitize.
	 *
	 * accepts a query $q */
	public function exec($q) {
		eputs($q);
		$this->db->exec($q);
	}

	/* query the db and collect the results into an array.
	 *
	 * accepts a query $q */
	public function query($q) {
		eputs($q);
		return collect_rows($this->db->query($q));
	}

	/* make a new user
	 * 
	 * accepts a name $n and a password $p */
	public function new_user($n, $p) {
		$u = null;
		try { $u = $this->get_user($n); } catch (Exception $e) { }
		if ($u != null) {
			throw new Exception(
				"user " . $u->name . " already exists"
			);
		}

		/* sanitize our variables */
		$n = SQLite3::escapeString($n);
		$p = SQLite3::escapeString($p);

		/* hash the password */
		$h = password_hash($p, PASSWORD_DEFAULT);
		if (!$h) {
			throw new Exception("failed to make password hash");
		}

		$this->exec(<<<SQL
			insert into users (name, hash)
			values ('$n', '$h');
		SQL);
	}

	/* add info for a user
	 *
	 * accepts a dict of info $d */
	public function new_info($d) {
		/* names */
		$n = array();
		/* values */
		$v = array();

		foreach ($d as $q => $p) {
			$t = gettype($p);
			switch ($t) {
			case "string": {
				$_p = SQLite3::escapeString(
					$p
				);
				$p = "'$_p',";
				break;
			}
			case "integer": {
				$p = "$p,";
				break;
			}
			default: {
				throw new Exception(
					<<<STR
					value with type $t
					invalid
					STR
				);
			}
			}

			array_push($n, "$q,");
			array_push($v, $p);
		}

		/* raze to str */
		$j = fn($x) => join("\n", $x);
		$n = rtrim($j($n), ",");
		$v = rtrim($j($v), ",");

		$this->exec(<<<SQL
		insert into info (
			$n
		) values (
			$v
		);
		SQL);
	}

	/* find a user by name
	 *
	 * accepts a name $n */
	public function get_user($n) {
		/* sanitize */
		$n = SQLite3::escapeString($n);

		/* query and convert to user */
		$a = $this->query(<<<SQL
			select * from users
			where name = '$n';
		SQL);
		$a = array_map(fn($x) => User::from_sql($x), $a);

		/* TODO: how should we handle users with the same name?
		 * it should never happen in theory because we should make
		 * the garuantee in new_user(), so would this work? */
		return count($a) > 0
			? $a[0]
			: throw new Exception("user not found");
	}

	public function get_user_by_id($i) {
		$a = $this->query(<<<SQL
			select * from users
			where id = $i;
		SQL);
		return count($a) > 0
			? User::from_sql($a[0])
			: throw new Exception("user not found");
	}

	/* accepts uid $i */
	public function get_info($i) {
		$a = $this->query(<<<SQL
			select * from info
			where uid = $i;
		SQL);
		return count($a) > 0
			? Info::from_sql($a[0])
			: Info::empty();
	}

	/* verify that a user matches the given login info
	 *
	 * accepts a name $n and a password $p */
	public function login($n, $p) {
		$e = null;

		try {
			$u = $this->get_user($n);

			if (password_verify($p, $u->hash)) {
				$_SESSION["id"] = $u->id;
				return true;
			} else {
				throw new Exception("incorrect password");
			}
		} catch (Exception $_e) {
			$e = $_e;
		}

		throw new Exception($e->getMessage());
	}

	/* inc a user with id $i to have one additional game */
	public function inc_games($i) {
		$u = null;
		try {
			$u = $this->get_user_by_id($i);
		} catch (Exception $e) {
			throw new Exception(
				"failed getting user "
				. $e->getMessage()
			);
		}

		$f = null;
		try {
			$f = $this->get_info($i);
		} catch (Exception $e) {
			throw new Exception(
				"failed getting user info "
				. $e->getMessage()
			);
		}
		eputs('$f->games: ' . $f->games);

		$a = ($f->games == null ? 0 : $f->games) + 1;

		$this->exec(<<<SQL
		update info
		set
			games = $a
		where uid = $i;
		SQL);
	}
}

/* collect rows from an sql query into an array
 *
 * accepts a "result" from a query $r */
function collect_rows($r) {
	$a = array();
	while ($x = $r->fetchArray()) {
		array_push($a, $x);
	}
	return $a;
}

?>