From dc9c7e5ded703bdcff739faf2c3ef1e4ca28e22a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiesner=20Andr=C3=A1s?= Date: Tue, 5 Mar 2024 16:07:47 +0100 Subject: [PATCH] initial --- autologin.php | 22 ++ common_func.php | 14 ++ composer.json | 7 + default_frame.php | 33 +++ examples/minta_quiz_táblázat.xlsx | Bin 0 -> 6727 bytes game_manager_frame.php | 66 ++++++ gamemgr.php | 186 +++++++++++++++++ globals.php | 34 ++++ group_manager_frame.php | 62 ++++++ groupmgr.php | 192 ++++++++++++++++++ index.php | 10 + install.php | 24 +++ interface.php | 324 ++++++++++++++++++++++++++++++ js/default_frame.js | 29 +++ js/gamemgr.js | 175 ++++++++++++++++ js/groupmgr.js | 123 ++++++++++++ js/hintbox.js | 70 +++++++ js/o.js | 21 ++ js/req.js | 31 +++ js/spreadquiz.js | 52 +++++ js/testground.js | 30 +++ js/usermgr.js | 164 +++++++++++++++ login.php | 24 +++ main.php | 50 +++++ style/spreadquiz.css | 197 ++++++++++++++++++ testground.php | 31 +++ testmgr.php | 93 +++++++++ user_manager_frame.php | 104 ++++++++++ usermgr.php | 113 +++++++++++ 29 files changed, 2281 insertions(+) create mode 100644 autologin.php create mode 100644 common_func.php create mode 100644 composer.json create mode 100644 default_frame.php create mode 100644 examples/minta_quiz_táblázat.xlsx create mode 100644 game_manager_frame.php create mode 100644 gamemgr.php create mode 100644 globals.php create mode 100644 group_manager_frame.php create mode 100644 groupmgr.php create mode 100644 index.php create mode 100644 install.php create mode 100644 interface.php create mode 100644 js/default_frame.js create mode 100644 js/gamemgr.js create mode 100644 js/groupmgr.js create mode 100644 js/hintbox.js create mode 100644 js/o.js create mode 100644 js/req.js create mode 100644 js/spreadquiz.js create mode 100644 js/testground.js create mode 100644 js/usermgr.js create mode 100644 login.php create mode 100644 main.php create mode 100644 style/spreadquiz.css create mode 100644 testground.php create mode 100644 testmgr.php create mode 100644 user_manager_frame.php create mode 100644 usermgr.php diff --git a/autologin.php b/autologin.php new file mode 100644 index 0000000..ec79d43 --- /dev/null +++ b/autologin.php @@ -0,0 +1,22 @@ + + + + + + + SpreadQuiz + + + + + + + +
+ +
+ + + + diff --git a/examples/minta_quiz_táblázat.xlsx b/examples/minta_quiz_táblázat.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..db628f1ea4eec981581615b219078a724db8c174 GIT binary patch literal 6727 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 SfPq1Me2G26<|*;h=zjnsBDMJd literal 0 HcmV?d00001 diff --git a/game_manager_frame.php b/game_manager_frame.php new file mode 100644 index 0000000..c56b381 --- /dev/null +++ b/game_manager_frame.php @@ -0,0 +1,66 @@ + + + + + + SpreadQuiz + + + + + + + + + +
+ + + + + + + + + + + +
NévLeírásTulajdonos
+
+
+ + +
+
+
+
+ Név:
+ Leírás:
+ Tulajdonos:
+ Szerkesztők:
+ Kérdés-fájl: + + +
+ Csoportok:
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/gamemgr.php b/gamemgr.php new file mode 100644 index 0000000..ad03759 --- /dev/null +++ b/gamemgr.php @@ -0,0 +1,186 @@ + 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; +} \ No newline at end of file diff --git a/globals.php b/globals.php new file mode 100644 index 0000000..6981379 --- /dev/null +++ b/globals.php @@ -0,0 +1,34 @@ + + + + + + SpreadQuiz + + + + + + + +
+ + + + + + + + + + + +
NévLeírásTulajdonos
+
+
+ + +
+
+
+
+ Név:
+ Leírás:
+ Tulajdonos:
+ Szerkesztők:
+ Tagok: +
+ +
+
+ + + + + + + diff --git a/groupmgr.php b/groupmgr.php new file mode 100644 index 0000000..729f239 --- /dev/null +++ b/groupmgr.php @@ -0,0 +1,192 @@ + 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]); + } +} \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..25f6806 --- /dev/null +++ b/index.php @@ -0,0 +1,10 @@ + + + + + SpreadQuiz + + + + + diff --git a/install.php b/install.php new file mode 100644 index 0000000..97191e7 --- /dev/null +++ b/install.php @@ -0,0 +1,24 @@ + $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; +} \ No newline at end of file diff --git a/js/default_frame.js b/js/default_frame.js new file mode 100644 index 0000000..533204e --- /dev/null +++ b/js/default_frame.js @@ -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); + }); + }); +} \ No newline at end of file diff --git a/js/gamemgr.js b/js/gamemgr.js new file mode 100644 index 0000000..8ac1e18 --- /dev/null +++ b/js/gamemgr.js @@ -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); +// } \ No newline at end of file diff --git a/js/groupmgr.js b/js/groupmgr.js new file mode 100644 index 0000000..892993e --- /dev/null +++ b/js/groupmgr.js @@ -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; +} \ No newline at end of file diff --git a/js/hintbox.js b/js/hintbox.js new file mode 100644 index 0000000..7070c74 --- /dev/null +++ b/js/hintbox.js @@ -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); + }); + } + } +} \ No newline at end of file diff --git a/js/o.js b/js/o.js new file mode 100644 index 0000000..66d8ebb --- /dev/null +++ b/js/o.js @@ -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"); +} diff --git a/js/req.js b/js/req.js new file mode 100644 index 0000000..a4bad86 --- /dev/null +++ b/js/req.js @@ -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); + }); + }); +} diff --git a/js/spreadquiz.js b/js/spreadquiz.js new file mode 100644 index 0000000..7589a58 --- /dev/null +++ b/js/spreadquiz.js @@ -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 = "(üres)"; + } + 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"); +} \ No newline at end of file diff --git a/js/testground.js b/js/testground.js new file mode 100644 index 0000000..a55de24 --- /dev/null +++ b/js/testground.js @@ -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); + }); + }); +} \ No newline at end of file diff --git a/js/usermgr.js b/js/usermgr.js new file mode 100644 index 0000000..bab68da --- /dev/null +++ b/js/usermgr.js @@ -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); +} \ No newline at end of file diff --git a/login.php b/login.php new file mode 100644 index 0000000..c6a6f1d --- /dev/null +++ b/login.php @@ -0,0 +1,24 @@ + + + + + + SpreadQuiz :: Bejelentkezés + + + + + + + + + diff --git a/main.php b/main.php new file mode 100644 index 0000000..339d4a4 --- /dev/null +++ b/main.php @@ -0,0 +1,50 @@ + + + + + + + + SpreadQuiz + + + + + +
+
+ +
+
+
+ +
+ + + + + + + +
+ +
+
+ + + diff --git a/style/spreadquiz.css b/style/spreadquiz.css new file mode 100644 index 0000000..b707720 --- /dev/null +++ b/style/spreadquiz.css @@ -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 { + +} \ No newline at end of file diff --git a/testground.php b/testground.php new file mode 100644 index 0000000..f0beeed --- /dev/null +++ b/testground.php @@ -0,0 +1,31 @@ + + + + + + + SpreadQuiz + + + + + + + +
+ +
+ + \ No newline at end of file diff --git a/testmgr.php b/testmgr.php new file mode 100644 index 0000000..5215d80 --- /dev/null +++ b/testmgr.php @@ -0,0 +1,93 @@ + 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); +} \ No newline at end of file diff --git a/user_manager_frame.php b/user_manager_frame.php new file mode 100644 index 0000000..1f78457 --- /dev/null +++ b/user_manager_frame.php @@ -0,0 +1,104 @@ + + + + + + SpreadQuiz + + + + + + + + + +
+ + + + + + + + + + + + (üres)"; + // } + // return ""; + // } + // + // function create_table_row(array $keys, array $record) + // { + // $tr = ""; + // foreach ($keys as $k) { + // $tr .= ""; + // } + // $tr .= ""; + // return $tr; + // } + // + // $users = get_all_users(); + // foreach ($users as $u) { + // $nickname = $u["nickname"]; + // $tr = ""; + // $tr .= create_cell("", "checkbox"); + // $tr .= create_cell($u["nickname"]); + // $tr .= create_cell($u["realname"]); + // $tr .= create_cell(implode(",", $u["groups"])); + // $tr .= create_cell($u["privilege"]); + // $tr .= ""; + // echo $tr; + // } + // ?> + +
FelhasználónévNévCsoportokJogosultság
$content
${record[$k]}
+
+
+ + + +
+
+
+
+ Felhasználónév:
+ Teljes név:
+ Jelszó:
+ Csoportok:
+ Jogosultság: + + +
+ +
+
+ + + + + + + diff --git a/usermgr.php b/usermgr.php new file mode 100644 index 0000000..1062229 --- /dev/null +++ b/usermgr.php @@ -0,0 +1,113 @@ + 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; +} \ No newline at end of file