This commit is contained in:
Wiesner András 2024-03-05 16:07:47 +01:00
commit dc9c7e5ded
29 changed files with 2281 additions and 0 deletions

22
autologin.php Normal file
View File

@ -0,0 +1,22 @@
<?php
require_once "globals.php";
require_once "usermgr.php";
// attempt to auto-login
$autologin_user_data = [];
$auto_logged_in = false;
if ((session_status() === PHP_SESSION_ACTIVE) && isset($_SESSION["nickname"])) {
$autologin_user_data = get_user($_SESSION["nickname"]);
$auto_logged_in = count($autologin_user_data) != 0;
}
function get_autologin_state() : bool {
global $auto_logged_in;
return $auto_logged_in;
}
function get_autologin_user_data() : array {
global $autologin_user_data;
return $autologin_user_data;
}

14
common_func.php Normal file
View File

@ -0,0 +1,14 @@
<?php
function explode_list(string $str) : array {
return explode(",", str_replace(" ", "", $str));
}
function alter_array_contents(array &$a, $add, $remove) {
if (($add !== null) && !array_search($add, $a)) { // if user was not assigned to the corresponding group
$a[] = $add;
}
if (($remove !== null) && (($i = array_search($remove, $a)) !== false)) { // only perform deleting if user is assigned to the passed group
array_splice($a, $i, 1);
}
}

7
composer.json Normal file
View File

@ -0,0 +1,7 @@
{
"require": {
"rakibtg/sleekdb": "2.15",
"ext-http": "*",
"ext-json": "*"
}
}

33
default_frame.php Normal file
View File

@ -0,0 +1,33 @@
<?php
require_once "globals.php";
require_once "autologin.php";
$user_data = get_autologin_user_data();
if (!get_autologin_state()) {
exit();
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SpreadQuiz</title>
<script src="js/spreadquiz.js"></script>
<script src="js/req.js"></script>
<script src="js/o.js"></script>
<script src="js/default_frame.js"></script>
<link rel="stylesheet" href="style/spreadquiz.css">
</head>
<body>
<section id="game_list_panel">
</section>
<script>
list_available_games();
</script>
</body>
</html>

Binary file not shown.

66
game_manager_frame.php Normal file
View File

@ -0,0 +1,66 @@
<?php
require_once "globals.php";
require_once "autologin.php";
$user_data = get_autologin_user_data();
if (!get_autologin_state() || (($user_data["privilege"] !== PRIVILEGE_CREATOR) && ($user_data["privilege"] !== PRIVILEGE_QUIZMASTER))) {
exit(); // this page is only available for quizmasters
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SpreadQuiz</title>
<script src="js/req.js"></script>
<script src="js/spreadquiz.js"></script>
<script src="js/o.js"></script>
<script src="js/hintbox.js"></script>
<script src="js/usermgr.js"></script>
<script src="js/gamemgr.js"></script>
<link rel="stylesheet" href="style/spreadquiz.css"/>
</head>
<body>
<section id="table_section">
<table class="management">
<thead>
<tr>
<th></th>
<th>Név</th>
<th>Leírás</th>
<th>Tulajdonos</th>
</tr>
</thead>
<tbody id="game_manager_table">
</tbody>
</table>
</section>
<section>
<input type="button" value="Új játék" onclick="create_edit_game();">
<input type="button" value="Játék(ok) törlése" onclick="delete_games()">
</section>
<section class="window" shown="false" id="game_editor_window">
<section class="window-inner">
<section style="text-align: right">
<span>Név: <input type="text" id="game_name"></span><br>
<span>Leírás: <input type="text" id="game_description"></span><br>
<span>Tulajdonos: <input type="text" id="game_owner" readonly></span><br>
<span>Szerkesztők: <input type="text" id="game_contributors"></span><br>
<span>Kérdés-fájl: <input type="button" id="download_challenges_btn" value="Letöltés CSV-ként" shown="false" onclick="download_challenges()">
<input type="button" value="Új feltöltése" id="show_game_file_upload" onclick="show_hide_gamefile_upload(true)">
<input type="file" id="game_file" shown="false">
<input type="button" value="Mégse" id="cancel_game_file_upload" shown="false" onclick="show_hide_gamefile_upload(false);"></span><br>
<span>Csoportok: <input type="text" id="game_groups"></span><br>
</section>
<span><input type="button" value="" id="game_editor_submit_btn"><input type="button" value="Mégse" onclick="hide('game_editor_window')"></span>
</section>
</section>
<script>
list_all_games();
</script>
</body>
</html>

186
gamemgr.php Normal file
View File

@ -0,0 +1,186 @@
<?php
require_once "globals.php";
require_once "common_func.php";
$gamedb = new \SleekDB\Store(GAMEDB, DATADIR, ["timeout" => false]);
const DEFAULT_GAME_PROPERTIES = [
"forward_only" => false, // player may traverse back and forth between challenges
"time_limit" => -1, // no time limit; otherwise, this field indicates time limit in seconds
"repeatable" => false // this test can be taken multiple times
];
function create_game(string $name, string $owner, string $description = "", array $properties = DEFAULT_GAME_PROPERTIES, array $contributors = [], array $challenges = []): bool
{
global $testdb;
$game_data = [
"name" => $name,
"owner" => $owner,
"contributors" => $contributors,
"description" => $description,
"game_file_present" => false,
"properties" => $properties,
"groups" => [],
];
$game_data = $testdb->insert($game_data);
// prepare game context
$id = $game_data["_id"];
$current_game_media_dir = GAMEMEDIA_DIR . DIRECTORY_SEPARATOR . $id;
mkdir($current_game_media_dir);
save_challenges($id, []);
return true;
}
function get_game(string $gameid): array
{
global $testdb;
return $testdb->findById($gameid);
}
function update_game(array $game_data)
{
global $testdb;
$testdb->update($game_data);
}
function delete_game(string $gameid)
{
global $testdb;
$game_data = get_game($gameid);
if (count($game_data) != 0) {
foreach ($game_data["groups"] as $groupid) {
change_group_game_assignments($groupid, null, $gameid);
}
$testdb->deleteById($gameid);
}
}
function change_game_group_assignments(string $gid, $groupname_add, $groupname_remove)
{
$game_data = get_game($gid);
if (count($game_data) != 0) {
alter_array_contents($game_data["groups"], $groupname_add, $groupname_remove);
update_game($game_data); // update user
}
}
function get_all_games()
{
global $testdb;
return $testdb->findAll();
}
function get_all_game_data_by_contributor_nickname(string $nickname): array
{
global $testdb;
$game_headers = [];
if ($nickname !== "*") {
$game_data_array = $testdb->findBy([["owner", "=", $nickname], "OR", ["contributors", "CONTAINS", $nickname]]);
} else {
$game_data_array = $testdb->findAll();
}
foreach ($game_data_array as $game_data) {
$game_headers[] = $game_data;
}
return $game_headers;
}
function import_challenges_from_csv(string $csv_path, string $gameid)
{
$game_data = get_game($gameid);
if (count($game_data) === []) {
return;
}
$challenges = [];
// load filled CSV file
$f = fopen($csv_path, "r");
if (!$f) { // failed to open file
return;
}
while ($csvline = fgetcsv($f)) {
if (count($csvline) >= 3) {
$ch = [
"question" => $csvline[0],
"image_url" => $csvline[1],
"correct_answer" => $csvline[2],
"answers" => array_filter(array_slice($csvline, 2), function ($v) {
return trim($v) !== "";
})
];
$challenges[] = $ch;
}
}
fclose($f);
// save challenges
save_challenges($gameid, $challenges);
// update game with game file present
$game_data["game_file_present"] = true;
update_game($game_data);
}
function is_user_contributor_to_game(string $gameid, string $nickname): bool
{
$game_data = get_game($gameid);
if (count($game_data) === 0) {
return false;
}
return in_array($nickname, $game_data["contributors"]) || ($game_data["owner"] === $nickname);
}
function is_user_owner_of_the_game(string $gameid, string $nickname): bool
{
$game_data = get_game($gameid);
if (count($game_data) === 0) {
return false;
}
return $game_data["owner"] === $nickname;
}
function save_challenges(string $gameid, array $challenges)
{
file_put_contents(get_game_file_by_gameid($gameid), json_encode($challenges)); // store challenges in JSON-format
}
function load_challenges(string $gameid)
{
return json_decode(file_get_contents(get_game_file_by_gameid($gameid)), true);
}
function export_challenges_to_csv($f, string $gameid)
{
$game_data = get_game($gameid);
if ((count($game_data) === []) || (!$game_data["game_file_present"])) {
return;
}
// load challenges
$challenges = load_challenges($gameid);
// populate CSV file
foreach ($challenges as $ch) {
$csvline = [
$ch["question"],
$ch["image_url"],
];
$csvline = array_merge($csvline, $ch["answers"]);
fputcsv($f, $csvline);
}
}
function get_contributors(string $gameid): array
{
$game_data = get_game($gameid);
return $game_data["contributors"];
}
function get_game_file_by_gameid(string $gameid)
{
return GAMEMEDIA_DIR . DIRECTORY_SEPARATOR . $gameid . DIRECTORY_SEPARATOR . GAME_FILE;
}

34
globals.php Normal file
View File

@ -0,0 +1,34 @@
<?php
require_once "vendor/autoload.php";
const DATADIR = "appdata";
const GAMEMEDIA_DIR = DATADIR . DIRECTORY_SEPARATOR . "game_media";
const GAME_FILE = "challenges.json";
const USERDB = "users";
const GROUPDB = "groups";
const GAMEDB = "games";
const TESTDB = "tests";
const INSTALL_INDICATOR = "INSTALLED";
const QUIZMASTER_NICKNAME = "quizmaster";
const LOGIN_URL = "login.php";
const MAIN_URL = "main.php";
const SESSION_NAME = "spreadquiz_sid";
session_name(SESSION_NAME);
// autoload session
if ((session_status() === PHP_SESSION_NONE) && isset($_COOKIE[SESSION_NAME])) {
session_start();
}
// ----------
// initialize data directory
function init_datadir() {
if (!file_exists(DATADIR)) {
mkdir(DATADIR);
mkdir(GAMEMEDIA_DIR);
}
}

62
group_manager_frame.php Normal file
View File

@ -0,0 +1,62 @@
<?php
require_once "globals.php";
require_once "autologin.php";
$user_data = get_autologin_user_data();
if (!get_autologin_state() || ($user_data["privilege"] !== PRIVILEGE_QUIZMASTER)) {
exit(); // this page is only available for quizmasters
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SpreadQuiz</title>
<script src="js/req.js"></script>
<script src="js/spreadquiz.js"></script>
<script src="js/o.js"></script>
<script src="js/groupmgr.js"></script>
<link rel="stylesheet" href="style/spreadquiz.css"/>
</head>
<body>
<section id="table_section">
<table class="management">
<thead>
<tr>
<th></th>
<th>Név</th>
<th>Leírás</th>
<th>Tulajdonos</th>
</tr>
</thead>
<tbody id="group_manager_table">
</tbody>
</table>
</section>
<section>
<input type="button" value="Új csoport" onclick="create_edit_group();">
<input type="button" value="Csoport(ok) törlése" onclick="delete_groups();">
</section>
<section class="window" shown="false" id="group_editor_window">
<section class="window-inner">
<section style="text-align: right">
<span>Név: <input type="text" id="groupname"></span><br>
<span>Leírás: <input type="text" id="group_description"></span><br>
<span>Tulajdonos: <input type="text" id="group_owner" readonly></span><br>
<span>Szerkesztők: <input type="text" id="group_editors"></span><br>
<span>Tagok: <textarea id="group_members" readonly></textarea></span>
</section>
<span><input type="button" value="" id="group_editor_submit_btn"><input type="button" value="Mégse" onclick="hide('group_editor_window')"></span>
</section>
</section>
<script>
list_all_groups();
</script>
</body>
</html>

192
groupmgr.php Normal file
View File

@ -0,0 +1,192 @@
<?php
require_once "globals.php";
require_once "common_func.php";
require_once "usermgr.php";
$groupdb = new \SleekDB\Store(GROUPDB, DATADIR, ["timeout" => false]);
function create_group(string $groupname, string $owner, string $description = "", array $editors = []): bool
{
global $groupdb;
// test name uniqueness
$unique = clear_unique_in_siblings($groupname);
// initialize group data
$group_data = [
"groupname" => $groupname,
"unique" => $unique,
"owner" => $owner,
"description" => $description,
"editors" => $editors,
"users" => [],
"games" => []
];
$groupdb->insert($group_data); // insert group
return true;
}
function get_group(string $groupid): array
{
global $groupdb;
return $groupdb->findById($groupid);
}
function update_group(array $group_data)
{
global $groupdb;
$groupdb->update($group_data);
}
function delete_group(string $groupid)
{
global $groupdb;
$group_data = get_group($groupid);
if (count($group_data) !== 0) {
foreach ($group_data["users"] as $nickname) { // remove all references to this group
change_user_group_assignments($nickname, null, $groupid);
}
}
$groupdb->deleteById($groupid);
}
function get_all_groups()
{
global $groupdb;
return $groupdb->findAll();
}
function change_group_user_assignments(string $groupid, $nickname_add, $nickname_remove)
{
$group_data = get_group($groupid);
if (count($group_data) != 0) {
// --------- UPDATE group assignments (DEV ONLY!)------
$group_data["editors"] = $group_data["editors"] ?: [];
// ---------
alter_array_contents($group_data["users"], $nickname_add, $nickname_remove); // add to local storage
alter_array_contents($group_data["editors"], null, $nickname_remove); // removed users must be excluded from the editors' list as well
update_group($group_data); // update user
if ($nickname_add !== null) {
change_user_group_assignments($nickname_add, $groupid, null); // add to user's storage remote
}
if ($nickname_remove !== null) {
change_user_group_assignments($nickname_remove, null, $groupid); // remove from user's storage
}
}
}
function change_group_game_assignments(string $groupid, $gameid_add, $gameid_remove)
{
$group_data = get_group($groupid);
if (count($group_data) != 0) {
alter_array_contents($group_data["games"], $gameid_add, $gameid_remove); // add to local storage
update_group($group_data); // update user
if ($gameid_add !== null) {
change_game_group_assignments($gameid_add, $groupid, null); // add to user's storage remote
}
if ($gameid_remove !== null) {
change_game_group_assignments($gameid_remove, null, $groupid); // remove from user's storage
}
}
}
function search_groups(string $needle): array
{
if (strlen($needle) < 3) {
return [];
}
global $groupdb;
$parts = explode("#", $needle, 2);
$groupname = $parts[0];
$group_data_array = [];
if (count($parts) === 1) {
$group_data_array = $groupdb->findBy(["groupname", "LIKE", "%$groupname%"]);
} else if (count($parts) > 1) {
$groupid = $parts[1];
$group_data_array = $groupdb->findBy([["groupname", "LIKE", "%$groupname%"], "AND", ["_id", "LIKE", "%$groupid%"]]);
}
$results = [];
foreach ($group_data_array as $group_data) {
$results[] = [
"groupname" => $group_data["groupname"],
"_id" => $group_data["_id"],
"unique" => $group_data["unique"]
];
}
return $results;
}
function clear_unique_in_siblings($groupname): bool
{
// make test on name uniqueness
global $groupdb;
$twins = $groupdb->findBy(["groupname", "=", "$groupname"]);
$unique = count($twins) == 0;
if (count($twins) === 1) { // if fails, then also indicate in the original group that its name is no longer unique
$twins[0]["unique"] = false;
update_group($twins[0]);
}
return $unique;
}
function get_groupids_by_compounds(array $compounds): array
{
global $groupdb;
$groupids = [];
foreach ($compounds as $compound) {
if (trim($compound) === "") { // skip empty entries
continue;
}
// fetch the group
$parts = explode("#", $compound);
$group_data = [];
if (count($parts) === 1) {
$fetch_cmd = ["groupname", "=", $parts[0]];
$group_data = $groupdb->findBy($fetch_cmd);
if (count($group_data) == 1) { // too many hits
$group_data = $group_data[0];
} else {
$group_data = [];
}
} else {
$group_data = $groupdb->findById($parts[1]);
}
if ($group_data !== []) {
$groupids[] = $group_data["_id"];
}
}
return $groupids;
}
function is_user_editor_to_group(string $groupid, string $nickname): bool
{
$group_data = get_group($groupid);
if (count($group_data) === 0) {
return false;
}
return in_array($nickname, $group_data["editors"]) || ($group_data["owner"] === $nickname);
}
function get_group_unique_name(string $groupid): string
{
$group_data = get_group($groupid);
if (count($group_data) !== 0) {
return $group_data["groupname"] . (!$group_data["unique"] ? "#" . $groupid : "");
}
return "";
}
function resolve_groupids(array &$groups): void
{
for ($i = 0; $i < count($groups); $i++) {
$groups[$i] = get_group_unique_name($groups[$i]);
}
}

10
index.php Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="hu">
<head>
<meta charset="UTF-8">
<title>SpreadQuiz</title>
</head>
<body>
<?="asd" ?>
</body>
</html>

24
install.php Normal file
View File

@ -0,0 +1,24 @@
<?php
require_once "globals.php";
if (file_exists(INSTALL_INDICATOR)) {
echo "SpreadQuiz already installed!";
exit();
}
init_datadir(); // create data directory
// auto-create databases
require_once "usermgr.php";
require_once "groupmgr.php";
require_once "gamemgr.php";
// create "quizmaster" (admin) user
$pw = uniqid();
add_user(QUIZMASTER_NICKNAME, $pw, "");
change_privilege_level(QUIZMASTER_NICKNAME, PRIVILEGE_QUIZMASTER);
echo "Quizmaster account: quizmaster, $pw\n";
// deploy install indicator
touch(INSTALL_INDICATOR);

324
interface.php Normal file
View File

@ -0,0 +1,324 @@
<?php
require_once "globals.php";
if (!file_exists(INSTALL_INDICATOR)) {
exit();
}
if (!isset($_REQUEST["action"])) {
exit();
}
require_once "common_func.php";
// load databases only if something meaningful have arrived
require_once "usermgr.php";
require_once "groupmgr.php";
require_once "gamemgr.php";
$action = $_REQUEST["action"];
$result = "";
// no-login accessible actions
switch ($action) {
case "login":
{
$nickname = $_REQUEST["nickname"];
$password = $_REQUEST["password"];
if (check_user_credentials($nickname, $password)) {
session_start();
$_SESSION["nickname"] = $nickname;
$result = "OK";
} else {
$result = "FAIL";
}
}
break;
}
// exit script if there's no live session or nickname is missing or the referenced user is non-existent
if ((session_status() != PHP_SESSION_ACTIVE) || (!isset($_SESSION["nickname"])) || (count(get_user($_SESSION["nickname"])) == 0)) {
goto print_result;
}
$user_data = get_user($_SESSION["nickname"]);
$nickname = $user_data["nickname"];
$privilege = $user_data["privilege"];
// login-requiring actions
switch ($action) {
case "logout":
{
$_SESSION = []; // clean up session data
setcookie(SESSION_NAME, "", -1); // invalidate cookie
}
break;
case "get_user_info":
{
$user_data_filtered = $user_data;
unset($user_data_filtered["password"]);
$result = json_encode($user_data_filtered);
}
break;
case "get_available_games":
{
$games_by_groups = [];
$groupids = $user_data["groups"];
foreach ($groupids as $groupid) {
$group_data = get_group($groupid);
$game_collection = [
"groupname" => $group_data["groupname"],
"description" => $group_data["description"],
"games" => []
];
$gameids = $group_data["games"];
foreach ($gameids as $gameid) {
$game = get_game($gameid);
$game_collection["games"][] = $game;
}
$games_by_groups[] = $game_collection;
}
$result = json_encode($games_by_groups);
}
break;
}
// creator or quizmaster actions
if (($privilege !== PRIVILEGE_CREATOR) && ($privilege !== PRIVILEGE_QUIZMASTER)) {
goto print_result;
}
switch ($action) {
case "create_game":
case "update_game":
{
$update = $action === "update_game";
$data = json_decode($_REQUEST["data"], true) ?: [];
if (($data === []) || (trim($data["name"] ?: "") === "")) { // no further processing
goto print_result; // ~exit...
}
$gameid = $data["_id"];
$name = $data["name"];
$description = $data["description"];
$contributors = explode_list($data["contributors"] ?: "");
$owner = $update ? trim($data["owner"] ?: $nickname) : $nickname;
$groups = explode_list($data["groups"] ?: "");
$groupids = get_groupids_by_compounds($groups); // convert group compounds to _ids
// remove group ID's this user cannot edit
$groupids_with_editor_access = [];
foreach ($groupids as $groupid) {
if (is_user_editor_to_group($groupid, $nickname)) {
$groupids_with_editor_access[] = $groupid;
}
}
$groupids_with_editor_access = $groupid;
if (!$update) {
create_game($name, $owner, $description);
} else if (is_user_contributor_to_game($gameid, $nickname)) {
$game_data = get_game($gameid);
if (count($game_data) !== 0) {
// group management
$old_groupids = $game_data["groups"]; // retain old groupids
$new_groupids = $groupids; // get new groupids
$groupids_add = array_diff($new_groupids, $old_groupids); // groups this user needs to be added to
$groupids_remove = array_diff($old_groupids, $new_groupids); // groups this user need to be removed from
foreach ($groupids_add as $groupid) { // execute insertion and removal
change_group_game_assignments($groupid, $gameid, null);
}
foreach ($groupids_remove as $groupid) {
change_group_game_assignments($groupid, null, $gameid);
}
// re-fetch game data
$game_data = get_game($gameid);
// update game header data
$game_data["name"] = $name;
$game_data["description"] = $description;
if (($game_data["owner"] === $nickname) || ($privilege === PRIVILEGE_QUIZMASTER)) {
$game_data["owner"] = $owner;
}
$game_data["contributors"] = array_intersect($contributors, get_all_nicknames());
update_game($game_data);
// update game file if supplied
if (isset($_FILES["game_file"])) {
import_challenges_from_csv($_FILES["game_file"]["tmp_name"], $data["_id"]);
}
}
}
}
break;
case "get_all_game_headers":
{
$requester_nickname = ($privilege === PRIVILEGE_QUIZMASTER) ? "*" : $nickname; // "*" means every game
$game_headers = get_all_game_data_by_contributor_nickname($requester_nickname);
foreach ($game_headers as &$game_header) {
resolve_groupids($game_header["groups"]);
}
$result = json_encode($game_headers);
}
break;
case "delete_games":
{
$gameids = explode_list(trim($_REQUEST["ids"] ?: ""));
foreach ($gameids as $gameid) {
if (($gameid !== "") && (is_user_owner_of_the_game($gameid, $nickname))) { // only the owner may delete a game
delete_game($gameid);
}
}
}
break;
case "export_game_file_csv":
{
$gameid = trim($_REQUEST["gameid"] ?: "");
if (($gameid !== "") && is_user_contributor_to_game($gameid, $nickname)) {
$f = tmpfile();
header("Content-Type: text/csv");
header("Content-Disposition: attachment; filename=\"challenges_$gameid.csv\"\r\n");
export_challenges_to_csv($f, $gameid);
fseek($f, 0);
fpassthru($f);
}
}
break;
}
// quizmaster actions
if ($privilege !== PRIVILEGE_QUIZMASTER) {
goto print_result;
}
switch ($action) {
case "create_group":
case "update_group":
{
$update = $action === "update_group";
$groupname = trim($_REQUEST["groupname"] ?: "");
$description = trim($_REQUEST["description"] ?: "");
$editors = explode_list(trim($_REQUEST["editors"] ?: ""));
$owner = (!$update) ? $user_data["nickname"] : trim($_REQUEST["owner"]);
if ($owner === "") {
$owner = $user_data["nickname"];
}
if ($groupname != "") {
switch ($action) {
case "create_group":
create_group($groupname, $owner, $description, $editors);
break;
case "update_group":
{
$gid = $_REQUEST["id"];
$group = get_group($gid);
if (count($group) !== 0) {
$group["unique"] = clear_unique_in_siblings($groupname); // manage unique flag in case of renaming
$group["groupname"] = $groupname;
$group["description"] = $description;
$group["editors"] = array_intersect($editors, $group["users"]); // a user cannot be an editor if not part of the group
$group["owner"] = $owner;
update_group($group);
}
}
break;
}
}
}
break;
case "delete_groups":
{
$groups = explode_list($_REQUEST["ids"] ?: "");
foreach ($groups as $g) {
delete_group($g);
}
}
break;
case "get_all_groups":
$result = json_encode(get_all_groups());
break;
case "search_groups":
{
$needle = $_REQUEST["needle"] ?: "";
$result = json_encode(search_groups($needle));
}
break;
case "create_user":
case "update_user":
{
$update = $action === "update_user";
$target_nickname = trim($_REQUEST["nickname"] ?: "");
$password = trim($_REQUEST["password"] ?: "");
$groups = explode_list($_REQUEST["groups"] ?: "");
$realname = trim($_REQUEST["realname"] ?: "");
$privilege = trim($_REQUEST["privilege"] ?: PRIVILEGE_PLAYER);
$groupids = get_groupids_by_compounds($groups); // convert group compounds to _ids
if (($target_nickname !== "")) {
if ((!$update) && ($password !== "")) { // CREATE
add_user($target_nickname, $password, $realname, $groupids, $privilege);
} else if ($update) { // UPDATE
$user_data = get_user($target_nickname); // load user data
// group management
$old_groupids = $user_data["groups"]; // retain old groupids
$new_groupids = $groupids; // get new groupids
$groupids_add = array_diff($new_groupids, $old_groupids); // groups this user needs to be added to
$groupids_remove = array_diff($old_groupids, $new_groupids); // groups this user need to be removed from
foreach ($groupids_add as $groupid) { // execute insertion and removal
change_group_user_assignments($groupid, $target_nickname, null);
}
foreach ($groupids_remove as $groupid) {
change_group_user_assignments($groupid, null, $target_nickname);
}
// re-fetch user
$user_data = get_user($target_nickname); // load user data
// further field update
$user_data["realname"] = $realname;
$user_data["privilege"] = $privilege;
// password replacement, if requested
if ($password !== "") {
$user_data["password"] = password_hash($password, PASSWORD_DEFAULT);
}
update_user($user_data);
}
}
}
break;
case "delete_users":
{
$users = explode_list($_REQUEST["users"] ?: "");
foreach ($users as $g) {
delete_user($g);
}
}
break;
case "get_all_users":
{
$user_data_filtered = get_all_users();
for ($i = 0; $i < count($user_data_filtered); $i++) {
unset($user_data_filtered[$i]["password"]); // remove password from records
resolve_groupids($user_data_filtered[$i]["groups"]); // resolve group IDs
}
$result = json_encode($user_data_filtered);
}
break;
}
// ----------
print_result:
if ($result !== "") {
echo $result;
}

29
js/default_frame.js Normal file
View File

@ -0,0 +1,29 @@
function list_available_games() {
let game_list_panel = document.getElementById("game_list_panel");
let req = {
action: "get_available_games"
}
request(req).then(resp => {
let games_by_groups = JSON.parse(resp);
games_by_groups.forEach((game_collection) => {
let group_box = document.createElement("section");
group_box.classList.add("group-box");
let group_box_caption = document.createElement("span");
group_box_caption.classList.add("group-box-caption");
group_box_caption.innerHTML = game_collection["groupname"];
let group_box_inner = document.createElement("section");
group_box_inner.classList.add("group-box-inner");
group_box.append(group_box_caption, group_box_inner);
game_collection["games"].forEach((game) => {
let game_box = document.createElement("section");
game_box.classList.add("game-box");
game_box.innerHTML = game["name"];
group_box_inner.appendChild(game_box);
});
game_list_panel.appendChild(group_box);
});
});
}

175
js/gamemgr.js Normal file
View File

@ -0,0 +1,175 @@
function list_all_games() {
let req = {action: "get_all_game_headers"};
let tbody = document.getElementById("game_manager_table");
tbody.innerHTML = "";
request(req).then(resp => {
let games = JSON.parse(resp);
for (let i = 0; i < games.length; i++) {
let g = games[i];
let row = document.createElement("tr");
let chkbox = document.createElement("input");
chkbox.type = "checkbox";
chkbox.name = "game_chkbox";
chkbox.game = g;
let tdChkBox = document.createElement("td");
tdChkBox.appendChild(chkbox);
tdChkBox.classList.add("checkbox");
let tdGameName = create_table_cell(g["name"]);
let tdGameDescription = create_table_cell(g["description"]);
let tdOwner = create_table_cell(g["owner"]);
row.append(tdChkBox, tdGameName, tdGameDescription, tdOwner);
tbody.appendChild(row);
let edit_group_action = () => {
create_edit_game(g);
};
tdGameName.addEventListener("click", edit_group_action);
tdGameDescription.addEventListener("click", edit_group_action);
tdOwner.addEventListener("click", edit_group_action);
}
});
}
var EDITED_GAME = null;
function create_edit_game(game = null) {
EDITED_GAME = game;
let updating = game !== null;
let nameF = document.getElementById("game_name");
let descriptionF = document.getElementById("game_description");
let submit_btn = document.getElementById("game_editor_submit_btn");
let ownerF = document.getElementById("game_owner");
let contributorsF = document.getElementById("game_contributors");
let gameFileF = document.getElementById("game_file");
let download_challenges_btn = document.getElementById("download_challenges_btn");
let show_game_file_upload_btn = document.getElementById("show_game_file_upload");
let cancel_game_file_upload_btn = document.getElementById("cancel_game_file_upload");
let groupF = document.getElementById("game_groups");
if (!updating) { // creating a new game
nameF.value = "";
descriptionF.value = "";
submit_btn.value = "Létrehozás"
ownerF.value = USERDATA["nickname"];
ownerF.readOnly = true;
contributorsF.value = "";
groupF.value = "";
} else { // editing an existing one
nameF.value = game["name"];
descriptionF.value = game["description"];
submit_btn.value = "Mentés"
ownerF.value = game["owner"];
ownerF.readOnly = false;
contributorsF.value = game["contributors"].join(", ");
groupF.value = game["groups"].join(", ");
}
gameFileF.value = "";
let game_file_present = updating && game["game_file_present"];
if (game_file_present) {
show(download_challenges_btn);
}
show_hide_gamefile_upload(false);
submit_btn.onclick = () => {
let game_name = document.getElementById("game_name").value.trim();
if (game_name !== "") {
let reqData = {
name: game_name,
description: descriptionF.value.trim(),
owner: updating ? ownerF.value.trim() : USERDATA["nickname"],
contributors: contributorsF.value.trim(),
groups: groupF.value.trim()
};
if (updating) {
reqData["_id"] = game["_id"];
}
let req = {
action: updating ? "update_game" : "create_game",
data: JSON.stringify(reqData)
};
if (gameFileF.files.length > 0) { // append game file if selected
req["game_file"] = gameFileF.files[0];
}
request(req).then(resp => {
list_all_games();
});
hide("game_editor_window");
}
};
show("game_editor_window");
}
function show_hide_gamefile_upload(en) {
if (en) {
// hide("download_challenges_btn");
hide("show_game_file_upload");
show("game_file");
show("cancel_game_file_upload");
} else {
// show("download_challenges_btn");
show("show_game_file_upload");
hide("game_file");
hide("cancel_game_file_upload");
document.getElementById("game_file").value = "";
}
}
function download_challenges() {
let action = "export_game_file_csv";
let gameid = EDITED_GAME["_id"];
window.open(`interface.php?action=${action}&gameid=${gameid}`, "_blank");
}
function get_selected_games() {
let selected_chkboxes = document.getElementsByName("game_chkbox");
let selected_games = [];
selected_chkboxes.forEach((chkbox) => {
if (chkbox.checked) {
selected_games.push(chkbox.game);
}
});
return selected_games;
}
function delete_games() {
let games = get_selected_games();
if (games.length === 0) {
return;
}
let game_names = [];
let game_ids = [];
games.forEach((g) => {
game_names.push(g["name"]);
game_ids.push(g["_id"]);
});
let msg = "Biztosan törölni kívánja a következő játéko(ka)t?\n\n" + game_names.join(", ") + "\n\n"
+ "A törlés nem vonható vissza!";
if (confirm(msg)) {
let req = {action: "delete_games", ids: game_ids.join(",")};
request(req).then(resp => {
list_all_games();
});
}
}
// function hint_all_groups(target_element_id) {
// const hintbox_insert_fn = (record) => {
// let targetF = document.getElementById(target_element_id);
// let groups = explode_sanitize_string_list(targetF.value);
// groups.pop();
// groups.push(record);
// targetF.value = groups.join(", ");
// close_hintbox(true);
// }
//
// let req = {
// action: "search_groups",
// }
//
// open_hintbox_at(target_element_id, req, print_group_name, hintbox_insert_fn);
// }

123
js/groupmgr.js Normal file
View File

@ -0,0 +1,123 @@
function list_all_groups() {
let req = {action: "get_all_groups"};
let tbody = document.getElementById("group_manager_table");
tbody.innerHTML = "";
request(req).then(resp => {
let groups = JSON.parse(resp);
for (let i = 0; i < groups.length; i++) {
let g = groups[i];
let row = document.createElement("tr");
let chkbox = document.createElement("input");
chkbox.type = "checkbox";
chkbox.name = "group_chkbox";
chkbox.group = g;
let tdChkBox = document.createElement("td");
tdChkBox.appendChild(chkbox);
tdChkBox.classList.add("checkbox");
let tdGroupName = create_table_cell(print_group_name(g));
let tdGroupDescription = create_table_cell(g["description"]);
let tdOwner = create_table_cell(g["owner"]);
row.append(tdChkBox, tdGroupName, tdGroupDescription, tdOwner);
tbody.appendChild(row);
let edit_group_action = () => {
create_edit_group(g);
};
tdGroupName.addEventListener("click", edit_group_action);
tdGroupDescription.addEventListener("click", edit_group_action);
tdOwner.addEventListener("click", edit_group_action);
}
});
}
function create_edit_group(group = null) {
let update = group !== null;
let groupnameF = document.getElementById("groupname");
let group_descriptionF = document.getElementById("group_description");
let submit_btn = document.getElementById("group_editor_submit_btn");
let group_ownerF = document.getElementById("group_owner");
let group_editorsF = document.getElementById("group_editors");
let group_membersF = document.getElementById("group_members");
if (!update) { // create a new group
groupnameF.value = "";
group_descriptionF.value = "";
submit_btn.value = "Létrehozás"
group_ownerF.value = USERDATA["nickname"];
group_ownerF.readOnly = true;
group_editorsF.value = "";
group_membersF.value = "";
} else { // update and existing one
groupnameF.value = group["groupname"];
group_descriptionF.value = group["description"];
submit_btn.value = "Mentés"
group_ownerF.value = group["owner"];
group_ownerF.readOnly = false;
group_editorsF.value = group["editors"].join(", ");
group_membersF.value = group["users"].join(", ");
}
submit_btn.onclick = () => {
let groupname = document.getElementById("groupname").value.trim();
if (groupname !== "") {
let req = {
action: update ? "update_group" : "create_group",
groupname: groupname,
description: group_descriptionF.value.trim(),
editors: group_editorsF.value.trim()
};
if (update) {
req["id"] = group["_id"];
}
request(req).then(resp => {
list_all_groups();
});
hide("group_editor_window");
}
};
show("group_editor_window");
}
function get_selected_groups() {
let selected_chkboxes = document.getElementsByName("group_chkbox");
let selected_groups = [];
selected_chkboxes.forEach((chkbox) => {
if (chkbox.checked) {
selected_groups.push(chkbox.group);
}
});
return selected_groups;
}
function delete_groups() {
let groups = get_selected_groups();
if (groups.length === 0) {
return;
}
let group_names = [];
let group_ids = [];
groups.forEach((g) => {
group_names.push(g["groupname"]);
group_ids.push(g["_id"]);
});
let msg = "Biztosan törölni kívánja a következő csoporto(ka)t?\n\n" + group_names.join(", ") + "\n\n"
+ "A törlés nem vonható vissza!";
if (confirm(msg)) {
let req = {action: "delete_groups", ids: group_ids.join(",")};
request(req).then(resp => {
list_all_groups();
});
}
}
function print_group_name(group_data) {
let record = group_data["groupname"];
if (!group_data["unique"]) {
record += "#" + group_data["_id"];
}
return record;
}

70
js/hintbox.js Normal file
View File

@ -0,0 +1,70 @@
function explode_sanitize_string_list(list, explodeAt = ",") {
let elements = list.split(explodeAt);
let sanitized = [];
elements.forEach((e) => {
if (e.trim() !== "") {
sanitized.push(e);
}
});
return sanitized;
}
const HINTBOX_ID = "HINTBOX";
function close_hintbox(close_if_focused = false) {
setTimeout(() => {
if (close_if_focused || (document.getElementById(HINTBOX_ID).firstChild !== document.activeElement)) {
hide(HINTBOX_ID);
}
}, 20);
}
function open_hintbox(x, y, contents, print_fun, insert_fun) {
let hbw = document.getElementById(HINTBOX_ID);
if (hbw === null) {
hbw = document.createElement("section");
hbw.id = HINTBOX_ID;
hbw.classList.add("hintbox-window");
let lb = document.createElement("select");
lb.size = 5;
lb.style.width = "100%";
hbw.appendChild(lb);
document.body.appendChild(hbw);
}
let lb = hbw.firstChild;
hbw.style.left = `${x}px`;
hbw.style.top = `${y}px`;
lb.innerHTML = "";
contents.forEach((record) => {
let line = print_fun(record);
let opt = document.createElement("option");
opt.value = line;
opt.innerText = line;
lb.appendChild(opt);
});
lb.ondblclick = () => {
insert_fun(lb.value);
};
show(HINTBOX_ID);
}
function open_hintbox_at(target_element_id, req_data, print_fn, insert_fn) {
let targetF = document.getElementById(target_element_id);
let bbox = targetF.getBoundingClientRect();
let list_str = targetF.value.trim().split(",");
if (list_str.length > 0) {
let last_le_str = list_str[list_str.length - 1].trim();
if (last_le_str.length > 2) {
req_data["needle"] = last_le_str; // auto-insert needle
request(req_data).then(resp => {
let groups = JSON.parse(resp);
let x = bbox.x + bbox.width;
let y = bbox.y;
open_hintbox(x, y, groups, print_fn, insert_fn);
});
}
}
}

21
js/o.js Normal file
View File

@ -0,0 +1,21 @@
function o(input) {
var o;
if (typeof input === "string")
o = document.getElementById(input);
else if (typeof input === "object")
o = input;
return o;
}
// megjelenítés / eltüntetés
function show(obj) {
o(obj).setAttribute("shown", "true");
}
function hide(obj) {
o(obj).setAttribute("shown", "false");
}
function toggle_show(obj) {
o(obj).setAttribute("shown", o(obj).getAttribute("shown") === "false");
}

31
js/req.js Normal file
View File

@ -0,0 +1,31 @@
// kérés indítása a szerver felé (eredeti: KL.)
function request(data, url = "interface.php", method = "POST") {
return new Promise((resolve, reject) => {
var fd;
// ha van adat megadva...
if (data != null) {
fd = new FormData();
// mezők hozzáfűzése a kéréshez
for (let prop in data) {
if (Object.prototype.hasOwnProperty.call(data, prop)) {
fd.append(prop, data[prop]);
}
}
}
// kérés feladása
fetch(url, {
method: method,
body: fd,
})
.then(response => response.text())
.then(data => resolve(data))
.catch((error) => {
console.error('Error: ', error);
reject(error);
});
});
}

52
js/spreadquiz.js Normal file
View File

@ -0,0 +1,52 @@
function login() {
let nicknameF = document.getElementById("nickname"); // fetch fields
let pwF = document.getElementById("password");
let nickname = nicknameF.value; // extract values
let pw = pwF.value;
let loginReq = {
action: "login",
nickname: nickname,
password: pw
};
request(loginReq).then(resp => {
if (resp === "OK") {
location.href = "main.php"
}
});
}
function open_in_content_frame(url) {
document.getElementById("content_frame").src = url;
}
var USERDATA = {};
function load_userdata() {
let req = {action: "get_user_info"};
request(req).then(resp => {
USERDATA = JSON.parse(resp);
});
}
load_userdata();
function create_table_cell(content, styleClass = "") {
if (content.trim() === "") {
content = "<i>(üres)</i>";
}
let td = document.createElement("td");
td.innerHTML = content;
if (styleClass !== "") {
td.classList.add(styleClass);
}
return td;
}
// ---------------
function highlight_row(nickname) {
let hl_on = document.getElementById("user_chk_" + nickname).checked;
let row = document.getElementById("row_" + nickname);
row.setAttribute("highlight", hl_on ? "true" : "false");
}

30
js/testground.js Normal file
View File

@ -0,0 +1,30 @@
function populate_test(test_id) {
let test_display = document.getElementById("test_display");
let req = {
action: "get_test",
id: test_id
}
request(req).then(resp => {
let test_data = JSON.parse(resp);
test_data["challenges"].forEach((challenge) => {
let challenge_box = document.createElement("section");
challenge_box.classList.add("challenge");
let question = document.createElement("span");
question.classList.add("question");
question.innerHTML = challenge["question"];
let answer_container = document.createElement("section");
answer_container.classList.add("answer-container");
challenge_box.append(question, answer_container);
challenge["answers"].forEach((answer) => {
let answer_section = document.createElement("section");
answer_section.classList.add("answer");
answer_section.innerHTML = answer;
answer_container.appendChild(answer_section);
});
test_display.appendChild(challenge_box);
});
});
}

164
js/usermgr.js Normal file
View File

@ -0,0 +1,164 @@
function list_all_users() {
let tbody = document.getElementById("user_manager_table_body");
tbody.innerHTML = "";
let req = {action: "get_all_users"};
request(req).then(resp => {
let users = JSON.parse(resp);
for (let i = 0; i < users.length; i++) {
let u = users[i];
let row = document.createElement("tr");
let chkbox = document.createElement("input");
chkbox.type = "checkbox";
chkbox.name = "user_chkbox";
chkbox.user = u;
let tdChkBox = document.createElement("td");
tdChkBox.appendChild(chkbox);
tdChkBox.classList.add("checkbox");
let tdNickName = create_table_cell(u["nickname"]);
let tdRealName = create_table_cell(u["realname"]);
let tdGroups = create_table_cell(u["groups"].join(", "));
let tdPrivilege = create_table_cell(u["privilege"]);
row.append(tdChkBox, tdNickName, tdRealName, tdGroups, tdPrivilege);
tbody.appendChild(row);
let edit_user_action = () => {
if (get_selected_users().length === 0) {
edit_user(u);
}
};
tdNickName.addEventListener("click", edit_user_action);
tdRealName.addEventListener("click", edit_user_action);
tdGroups.addEventListener("click", edit_user_action);
tdPrivilege.addEventListener("click", edit_user_action)
}
});
}
function create_new_user() {
const generateRandomString = () => {
return Math.floor(Math.random() * Date.now()).toString(36);
};
let nicknameF = document.getElementById("nickname");
let realnameF = document.getElementById("realname");
let passwordF = document.getElementById("password");
let groupsF = document.getElementById("groups");
let privilegeF = document.getElementById("privilege");
let submit_btn = document.getElementById("user_editor_submit_btn");
nicknameF.value = "";
nicknameF.readOnly = false;
realnameF.value = "";
passwordF.type = "text";
passwordF.value = generateRandomString();
passwordF.readOnly = true;
groupsF.value = "";
submit_btn.value = "Létrehozás"
submit_btn.onclick = () => {
let nickname = nicknameF.value.trim();
if (nickname !== "") {
let req = {
action: "create_user",
nickname: nickname,
realname: realnameF.value.trim(),
password: passwordF.value,
groups: groupsF.value.trim(),
privilege: privilegeF.value
};
request(req).then(resp => {
list_all_users();
});
hide("user_editor_window");
}
};
show("user_editor_window");
}
function edit_user(user) {
let nicknameF = document.getElementById("nickname");
let realnameF = document.getElementById("realname");
let passwordF = document.getElementById("password");
let groupsF = document.getElementById("groups");
let privilegeF = document.getElementById("privilege");
let submit_btn = document.getElementById("user_editor_submit_btn");
nicknameF.value = user["nickname"];
nicknameF.readOnly = true;
realnameF.value = user["realname"];
passwordF.type = "password";
passwordF.value = "";
passwordF.readOnly = false;
groupsF.value = user["groups"].join(", ");
privilegeF.value = user["privilege"];
submit_btn.value = "Mentés"
submit_btn.onclick = () => {
let nickname = nicknameF.value.trim();
if (nickname !== "") {
let req = {
action: "update_user",
nickname: nickname,
realname: realnameF.value.trim(),
password: passwordF.value,
groups: groupsF.value.trim(),
privilege: privilegeF.value
};
request(req).then(resp => {
list_all_users();
});
hide("user_editor_window");
}
};
show("user_editor_window");
}
function get_selected_users() {
let selected_chkboxes = document.getElementsByName("user_chkbox");
let selected_users = [];
selected_chkboxes.forEach((chkbox) => {
if (chkbox.checked) {
selected_users.push(chkbox.user);
}
});
return selected_users;
}
function delete_users() {
let users = get_selected_users();
if (users.length === 0) {
return;
}
let user_nicknames = [];
users.forEach((u) => {
user_nicknames.push(u["nickname"]);
});
let msg = "Biztosan törölni kívánja a következő felhasználó(ka)t?\n\n" + user_nicknames.join(", ") + "\n\n"
+ "A törlés nem vonható vissza!";
if (confirm(msg)) {
let req = {action: "delete_users", users: user_nicknames.join(",")};
request(req).then(resp => {
list_all_users();
});
}
}
function hint_all_groups(target_element_id) {
const hintbox_insert_fn = (record) => {
let targetF = document.getElementById(target_element_id);
let groups = explode_sanitize_string_list(targetF.value);
groups.pop();
groups.push(record);
targetF.value = groups.join(", ");
close_hintbox(true);
}
let req = {
action: "search_groups",
}
open_hintbox_at(target_element_id, req, print_group_name, hintbox_insert_fn);
}

24
login.php Normal file
View File

@ -0,0 +1,24 @@
<?php
require_once "globals.php";
require_once "autologin.php";
if (get_autologin_state()) {
header("Location: " . MAIN_URL);
exit();
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SpreadQuiz :: Bejelentkezés</title>
<script src="js/req.js"></script>
<script src="js/spreadquiz.js"></script>
</head>
<body>
<input type="text" placeholder="Felhasználónév" id="nickname">
<input type="password" placeholder="Jelszó" id="password">
<input type="button" value="Belépés" onclick="login()">
</body>
</html>

50
main.php Normal file
View File

@ -0,0 +1,50 @@
<?php
require_once "globals.php";
require_once "usermgr.php";
require_once "autologin.php";
// if not logged in, then redirect to login page
$logged_in = get_autologin_state();
if (!$logged_in) {
header("Location: " . LOGIN_URL);
}
$user_data = get_autologin_user_data();
$privilege = $user_data["privilege"];
?>
<?php if ($logged_in) { ?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SpreadQuiz</title>
<script src="js/req.js"></script>
<script src="js/spreadquiz.js"></script>
<link rel="stylesheet" href="style/spreadquiz.css"/>
</head>
<body>
<section id="screen_panel">
<section id="content_pane">
<iframe id="content_frame" src="default_frame.php"></iframe>
</section>
<section id="info_pane">
<section id="user_info" class="info-pane-element"><?= $user_data["nickname"]; ?></section>
<?php if ($privilege != PRIVILEGE_PLAYER) { ?>
<section id="action_panel" class="info-pane-element">
<?php if (($privilege === PRIVILEGE_CREATOR) || ($privilege === PRIVILEGE_QUIZMASTER)) { ?>
<input type="button" value="Tartalmak kezelése" onclick="open_in_content_frame('game_manager_frame.php')">
<?php } ?>
<?php if ($privilege === PRIVILEGE_QUIZMASTER) { ?>
<input type="button" value="Felhasználók kezelése" onclick="open_in_content_frame('user_manager_frame.php')">
<input type="button" value="Csoportok kezelése" onclick="open_in_content_frame('group_manager_frame.php')">
<?php } ?>
</section>
<?php } ?>
</section>
</section>
</body>
</html>
<?php } ?>

197
style/spreadquiz.css Normal file
View File

@ -0,0 +1,197 @@
*[shown="false"] {
visibility: hidden;
display: none;
opacity: 0;
width: 0;
height: 0;
}
*[shown="true"] {
opacity: 1;
}
/* ----------------- */
section#screen_panel {
display: block;
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
section#content_pane, section#info_pane {
position: absolute;
display: block;
top: 0;
bottom: 0;
}
section#content_pane {
left: 0;
width: 80vw;
background-color: lightcyan;
}
section#info_pane {
right: 0;
width: 20vw;
background-color: beige;
}
.info-pane-element {
display: block;
left: 0;
right: 0;
padding: 1em;
}
section#user_info {
position: relative;
top: 0;
background-color: aquamarine;
font-size: 18pt;
text-align: center;
}
section#action_panel {
background-color: antiquewhite;
}
iframe#content_frame {
display: block;
width: 100%;
height: 100%;
border: 0 transparent;
}
section#table_section {
position: sticky;
height: calc(100vh - 4em);
overflow-y: auto;
}
table.management {
width: 100%;
border-collapse: collapse;
}
table.management thead th {
padding: 0.5em 0;
position: sticky;
top: 0;
background-color: darkgray;
}
table.management tbody td {
border: 1pt lightgrey solid;
padding: 0.3em 0.5em;
}
table.management tbody tr[highlight="true"] {
background-color: antiquewhite;
}
table.management tbody tr[highlight="false"] td input {
pointer-events: none;
}
section#user_manager_action_bar {
margin-top: 0.5em;
}
section.window {
position: fixed;
display: block;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(128, 128, 128, 0.7);
padding: 4em;
}
section.window-inner {
margin: 0 auto;
display: block;
border: 2pt solid darkslategray;
background-color: whitesmoke;
padding: 1em;
width: fit-content;
}
td.checkbox {
width: 0;
}
.hintbox-window {
position: fixed;
display: block;
width: 15em;
height: 8em;
}
/* ----- */
section#game_list_panel {
display: block;
position: absolute;
left: 0;
right: 0;
}
section.group-box {
margin: 1em;
left: 0;
right: 0;
background-color: beige;
}
span.group-box-caption {
display: block;
left: 0;
right: 0;
padding: 1em;
background-color: chartreuse;
}
section.group-box-inner {
display: inline-table;
padding: 1em;
collapse: 1em;
border-spacing: 1em;
}
section.game-box {
display: table-cell;
width: 8em;
height: 8em;
border: 2pt dashed gray;
text-align: center;
vertical-align: middle;
cursor: pointer;
}
section#test_area {
display: block;
position: absolute;
left: 0;
right: 0;
}
section.challenge {
}
span.question {
}
section.answer-container {
}
section.answer {
}

31
testground.php Normal file
View File

@ -0,0 +1,31 @@
<?php
require_once "globals.php";
require_once "autologin.php";
$user_data = get_autologin_user_data();
if (!get_autologin_state() || !isset($_REQUEST["testid"])) {
exit();
}
$testid = $_REQUEST["testid"];
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SpreadQuiz</title>
<script src="js/spreadquiz.js"></script>
<script src="js/req.js"></script>
<script src="js/o.js"></script>
<script src="js/testground.js"></script>
<link rel="stylesheet" href="style/spreadquiz.css">
</head>
<body>
<section id="test_display">
</section>
</body>
</html>

93
testmgr.php Normal file
View File

@ -0,0 +1,93 @@
<?php
require_once "globals.php";
require_once "common_func.php";
require_once "gamemgr.php";
require_once "usermgr.php";
$testdb = new \SleekDB\Store(TESTDB, DATADIR, ["timeout" => false]);
const TEST_ONGOING = "ongoing";
const TEST_CONCLUDED = "concluded";
function create_or_continue_test(string $gameid, string $nickname): string
{
global $testdb;
// get game and user data
$game_data = get_game($gameid);
$user_data = get_user($nickname);
if ((count($game_data) === 0) || (count($user_data) === 0)) {
return "";
}
// check if this user has permission to take this test
// if the intersection of user's groups and game's assigned groups is zero, then the user has no access to this game
if (count(array_intersect($game_data["groups"], $user_data["groups"])) === 0) {
return "";
}
// check if the user had taken this test before
$fetch_criteria = [["gameid", "=", $gameid], "AND", ["nickname", "=", $nickname]];
$previous_tests = $testdb->findBy($fetch_criteria);
if (count($previous_tests) > 0) { // if there are previous attempts, then...
// update timed tests to see if they had expired
update_timed_tests($previous_tests);
// re-fetch tests, look only for ongoing
$ongoing_tests = $testdb->findBy([$fetch_criteria, "AND", ["state", "=", TEST_ONGOING]]);
if (count($ongoing_tests) !== 0) { // if there's an ongoing test
return $ongoing_tests[0]["_id"];
} else { // there's no ongoing test
if ($game_data["properties"]["repeatable"]) { // test is repeatable...
return create_test($game_data, $user_data);
} else { // test is non-repeatable, cannot be attempted more times
return "";
}
}
} else { // there were no previous attempts
return create_test($game_data, $user_data);
}
}
function create_test(array $game_data, array $user_data) : string
{
$gameid = $game_data["_id"];
// fill basic data
$game_data = [
"gameid" => $gameid,
"nickname" => $user_data["nickname"],
];
// fill challenges
$challenges = load_challenges($gameid);
// shuffle answers
foreach ($challenges as &$ch) {
shuffle($ch["answers"]);
}
$game_data["challenges"] = $challenges;
// involve properties
}
function update_timed_tests(array $test_data_array)
{
$now = time();
foreach ($test_data_array as $test_data) {
// look for unprocessed expired tests
if (($test_data["state"] === TEST_ONGOING) && ($test_data["end_limit_time"] < $now)) {
$test_data["state"] = TEST_CONCLUDED;
update_test($test_data);
}
}
}
function update_test(array $test_data)
{
global $testdb;
$testdb->update($test_data);
}

104
user_manager_frame.php Normal file
View File

@ -0,0 +1,104 @@
<?php
require_once "globals.php";
require_once "autologin.php";
$user_data = get_autologin_user_data();
if (!get_autologin_state() || ($user_data["privilege"] !== PRIVILEGE_QUIZMASTER)) {
exit(); // this page is only available for quizmasters
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SpreadQuiz</title>
<script src="js/req.js"></script>
<script src="js/spreadquiz.js"></script>
<script src="js/o.js"></script>
<script src="js/groupmgr.js"></script>
<script src="js/hintbox.js"></script>
<script src="js/usermgr.js"></script>
<link rel="stylesheet" href="style/spreadquiz.css"/>
</head>
<body>
<section id="table_section">
<table id="user_manager_table" class="management">
<thead>
<tr>
<th></th>
<th>Felhasználónév</th>
<th>Név</th>
<th>Csoportok</th>
<th>Jogosultság</th>
</tr>
</thead>
<tbody id="user_manager_table_body">
<!-- --><?php
// function create_cell(string $content, string $class = "")
// {
// if (trim($content) === "") {
// $content = "<i>(üres)</i>";
// }
// return "<td class='$class'>$content</td>";
// }
//
// function create_table_row(array $keys, array $record)
// {
// $tr = "<tr>";
// foreach ($keys as $k) {
// $tr .= "<td>${record[$k]}</td>";
// }
// $tr .= "</tr>";
// return $tr;
// }
//
// $users = get_all_users();
// foreach ($users as $u) {
// $nickname = $u["nickname"];
// $tr = "<tr id='row_$nickname'>";
// $tr .= create_cell("<input type='checkbox' id='user_chk_$nickname' onchange='highlight_row(\"$nickname\")'>", "checkbox");
// $tr .= create_cell($u["nickname"]);
// $tr .= create_cell($u["realname"]);
// $tr .= create_cell(implode(",", $u["groups"]));
// $tr .= create_cell($u["privilege"]);
// $tr .= "</tr>";
// echo $tr;
// }
// ?>
</tbody>
</table>
</section>
<section>
<input type="button" value="Új felhasználó" onclick="create_new_user()">
<input type="button" value="Felhasználó(k) törlése" onclick="delete_users()">
<input type="button" value="Felhasználók importálása CSV-ből">
</section>
<section class="window" shown="false" id="user_editor_window">
<section class="window-inner">
<section style="text-align: right">
<span>Felhasználónév: <input type="text" id="nickname"></span><br>
<span>Teljes név: <input type="text" id="realname"></span><br>
<span>Jelszó: <input type="text" id="password" readonly></span><br>
<span>Csoportok: <input type="text" id="groups" oninput="hint_all_groups('groups')" onblur="close_hintbox()"></span><br>
<span>Jogosultság:
<select id="privilege">
<option value="player">játékos</option>
<option value="creator">szerkesztő</option>
<option value="admin">kvízmester</option>
</select>
</span>
</section>
<span><input type="button" value="" id="user_editor_submit_btn"><input type="button" value="Mégse" onclick="hide('user_editor_window')"></span>
</section>
</section>
<script>
list_all_users();
</script>
</body>
</html>

113
usermgr.php Normal file
View File

@ -0,0 +1,113 @@
<?php
require_once "globals.php";
require_once "common_func.php";
$userdb = new \SleekDB\Store(USERDB, DATADIR, ["timeout" => false]);
const PRIVILEGE_PLAYER = "player";
const PRIVILEGE_CREATOR = "creator";
const PRIVILEGE_QUIZMASTER = "admin"; // TODO: refactor!
function add_user(string $nickname, string $password, string $realname, array $groups = [], string $privilege = PRIVILEGE_PLAYER): bool
{
global $userdb;
if (count(get_user($nickname)) != 0) { // user exists
return false;
}
$user_data = [
"nickname" => $nickname,
"password" => password_hash($password, PASSWORD_DEFAULT),
"realname" => $realname,
"groups" => $groups,
"privilege" => $privilege
];
$userdb->insert($user_data);
return true; // user registration successful
}
function delete_user(string $nickname)
{
global $userdb;
if ($nickname == QUIZMASTER_NICKNAME) {
return;
}
$user_data = get_user($nickname);
if (count($user_data) !== 0) {
foreach ($user_data["groups"] as $groupid) {
change_group_user_assignments($groupid, null, $nickname);
}
$userdb->deleteBy(["nickname", "=", $nickname]);
}
}
function get_user(string $nickname): array
{
global $userdb;
$user_data_array = $userdb->findBy(["nickname", "=", $nickname]);
return count($user_data_array) != 0 ? $user_data_array[0] : [];
}
function update_user(array $user_data)
{
global $userdb;
return $userdb->update($user_data);
}
function change_password(string $nickname, string $old, string $new): bool
{
$user_data = get_user($nickname);
if (count($user_data) != 0) {
if (password_verify($old, $user_data["password"])) {
$user_data["password"] = password_hash($new, PASSWORD_DEFAULT);
update_user($user_data);
return true;
}
}
return false;
}
function change_user_group_assignments(string $nickname, $groupname_add, $groupname_remove)
{
$user_data = get_user($nickname);
if (count($user_data) != 0) {
alter_array_contents($user_data["groups"], $groupname_add, $groupname_remove);
update_user($user_data); // update user
}
}
function change_privilege_level(string $nickname, string $privilege)
{
$user_data = get_user($nickname);
if (count($user_data) != 0) {
$user_data["privilege"] = $privilege;
update_user($user_data);
}
}
function check_user_credentials(string $nickname, string $password): bool
{
$user_data = get_user($nickname);
if (count($user_data) != 0) {
return password_verify($password, $user_data["password"]);
} else {
return false;
}
}
function get_all_users(): array
{
global $userdb;
return $userdb->findAll();
}
function get_all_nicknames() : array {
$nicknames = [];
$user_data_array = get_all_users();
foreach ($user_data_array as $user_data) {
$nicknames[] = $user_data["nickname"];
}
return $nicknames;
}