Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

66 changed files with 9116 additions and 3433 deletions

3
.gitignore vendored

@ -3,6 +3,3 @@ secrets
secrets/
secrets/config.php
qodana.yaml
*.br
.br
google*.html

@ -1,3 +0,0 @@
{
"esversion": 10
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

@ -1,113 +1,33 @@
let UserInfo = {};
let PageIntervals = [];
let config =
{
articleRefresh: 300000,
messageFadeIn: 300,
messageDisappear: 200
};
function isLoggedIn() {
"use strict";
return UserInfo.Email && UserInfo.Email.length > 0;
}
async function setElementClasses(element, newClasses) {
// Ensure the element exists
if (!element) return;
// Clear all existing classes
element.className = '';
// Add the new classes to the element
element.classList.add(...newClasses);
}
async function handleResponse(data, successMessage, failureMessage) {
"use strict";
const statusMessageContainer = document.getElementById("statusMessageContainer");
const statusMessage = document.createElement("div");
statusMessage.classList.add("status-message");
if (data.Status === 'Success') {
statusMessage.innerText = successMessage;
statusMessage.classList.add("success");
} else {
statusMessage.innerText = failureMessage;
statusMessage.classList.add("failure");
}
statusMessageContainer.appendChild(statusMessage);
// Automatically remove the message after 3 seconds
setTimeout(() => {
statusMessage.style.opacity = "0";
setTimeout(() => {
statusMessage.remove();
}, config.messageFadeIn);
}, config.messageDisappear);
}
async function showDashboardGreeting() {
"use strict";
document.getElementById("welcomeMsg").innerText = `Ahoj, ${UserInfo.FirstName}.`;
}
async function doAction(url, requestData, successMessage, failureMessage, silent) {
"use strict";
const params = new FormData();
for (const key in requestData) {
params.append(key, requestData[key]);
}
const response = await fetch(url, {
function doAccountAction(requestData, successMessage, failureMessage, silent=false) {
return fetch('/account', {
method: 'POST',
body: params,
});
if (!response.ok) {
console.error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
if (!silent) {
await handleResponse(data, successMessage, failureMessage);
}
return data;
body: requestData,
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
if(!silent) {
handleResponse(data, successMessage, failureMessage);
}
return data; // Returning the response data for further processing
})
.catch((error) => {
console.error('Error:', error);
});
}
async function handlePageResponse(data) {
"use strict";
const navbar = document.getElementById("navbar_container");
const pageArea = document.getElementById("page_container");
if (data.Navigation) {
navbar.innerHTML = data.Navigation;
}
if (data.PageTitle) {
document.title = data.PageTitle;
}
if (data.Page) {
pageArea.innerHTML = data.Page;
if (data.PageLocation) {
history.pushState({}, "", data.PageLocation);
}
}
}
async function displayList(data, elementId, deleteFunction) {
"use strict";
const tableContainer = document.getElementById(elementId);
function displayList(data, element_id, delete_function=null) {
const tableContainer = document.getElementById(element_id);
tableContainer.innerHTML = ""; // Clear previous content
const table = document.createElement("table");
table.classList.add("list-table");
// Create header row
const headerRow = table.insertRow(0);
for (const key in data[0]) {
const th = document.createElement("th");
@ -115,14 +35,13 @@ async function displayList(data, elementId, deleteFunction) {
headerRow.appendChild(th);
}
if (typeof deleteFunction === "function") {
if(typeof delete_function === "function") {
const th = document.createElement("th");
let deleteBtn = document.createElement('i');
deleteBtn.classList.add("ri-delete-bin-line");
th.appendChild(deleteBtn);
th.appendChild(document.createTextNode("Delete"));
headerRow.appendChild(th);
}
// Create data rows
for (const line of data) {
const dataRow = table.insertRow();
for (const key in line) {
@ -130,12 +49,14 @@ async function displayList(data, elementId, deleteFunction) {
td.appendChild(document.createTextNode(line[key]));
dataRow.appendChild(td);
}
if (typeof deleteFunction === "function") {
if(typeof delete_function === "function") {
const td = document.createElement("td");
const deleteButton = document.createElement('button');
deleteButton.innerHTML = "<i class='ri-delete-bin-line'></i>";
deleteButton.onclick = () => deleteFunction(line.ID);
td.appendChild(deleteButton);
let delete_button = document.createElement('button');
delete_button.textContent = "Delete"
delete_button.onclick = function (){
delete_function(line.ID);
}
td.appendChild(delete_button);
dataRow.appendChild(td);
}
}
@ -143,609 +64,35 @@ async function displayList(data, elementId, deleteFunction) {
tableContainer.appendChild(table);
}
async function doPageAction(requestData) {
"use strict";
const response = await fetch('/page', {
method: 'POST',
body: new URLSearchParams(requestData),
});
function handleResponse(data, SuccessMessage, failureMessage) {
const StatusMessageElement = document.getElementById("StatusMessage");
if (!response.ok) {
console.error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
await handlePageResponse(data);
return data;
}
async function initAjaxNavigationEvents() {
"use strict";
const allLinks = document.querySelectorAll('.navsite_link, .navpage_link');
const pageLinks = document.querySelectorAll('.navpage_link');
pageLinks.forEach(function (link) {
link.addEventListener('click', function () {
navLinks.classList.remove("active");
});
});
allLinks.forEach(function (link) {
link.addEventListener('click', function (e) {
e.preventDefault();
let site = this.dataset.site;
let page = this.dataset.page;
if (site && page) {
navigateTo(site, page);
}
});
});
const toggleButton = document.getElementById("toggle_button");
const navLinks = document.getElementById("navsite_list");
toggleButton.addEventListener('click', () => {
navLinks.classList.toggle("active");
});
}
async function initAjax() {
"use strict";
await initAjaxNavigationEvents();
await onPageLoad();
}
async function togglearticlecreate() {
"use strict";
let articleContainerElement = document.getElementById("articlecreatecontainer");
articleContainerElement.classList.toggle("hidden");
}
async function togglememecreate() {
"use strict";
let memeContainerElement = document.getElementById("memecreatecontainer");
await getMemeImages();
memeContainerElement.classList.toggle("hidden");
}
async function renderarticles() {
"use strict";
let template = document.querySelector('template[data-template-name="article"]').innerHTML;
let articles = await doAction(
"/newsarticle",
{
action: "getNewsArticles"
},
"Články načítané",
"Nastala chyba pri načítavaní článkov",
true
);
let articleout = "";
for (const article of articles.Articles) {
articleout += template.replace("__TEMPLATE_ARTICLE_TITLE__", article.Title).replace("__TEMPLATE_ARTICLE_AUTHOR__", article.WrittenByName).replace("__TEMPLATE_ARTICLE_DATE__", article.WrittenAt).replace("__TEMPLATE_ARTICLE_BODY__", article.Body);
}
document.getElementById("articleslist").innerHTML = articleout;
}
async function submitarticle() {
"use strict";
let articleTitleElement = document.getElementById("articletitleinput");
let articleBodyElement = document.getElementById("articlebodyinput");
await doAction(
"/newsarticle",
{
action: "addNewsArticle",
title: articleTitleElement.value,
body: articleBodyElement.value
},
"Článok úspešne pridaný",
"Nastala chyba pri pridávaní článku",
false
);
await togglearticlecreate();
}
async function articleInit() {
"use strict";
let articleContainerElement = document.getElementById("articlecreatecontainer");
let articleCreateOpenElement = document.getElementById("articlecreateopen");
articleContainerElement.addEventListener("keyup", function (ev) {
if (ev.key === "Escape") {
togglearticlecreate();
}
});
PageIntervals.push(setInterval(renderarticles, config.articleRefresh));
document.getElementById("articleprivilegeinput").setAttribute("max", UserInfo.Privileges);
if (UserInfo.Privileges < 2) {
articleContainerElement.style.display = "none";
articleCreateOpenElement.style.display = "none";
if (data.Status === 'Success') {
StatusMessageElement.innerText = SuccessMessage;
} else {
articleCreateOpenElement.style.display = "inline-block";
StatusMessageElement.innerText = failureMessage;
}
// Show the status message
StatusMessageElement.style.display = "block";
setTimeout(() => {
// Hide the status message after 3 seconds
StatusMessageElement.style.opacity = "0";
setTimeout(() => {
StatusMessageElement.style.display = "none";
// Reset opacity for future messages
StatusMessageElement.style.opacity = "1";
}, 500);
}, 3000);
}
async function onPageLoad() {
"use strict";
await restoreUserInfo();
let currentSite = localStorage.getItem("currentSite");
let currentPage = localStorage.getItem("currentPage");
function logout() {
const data = new URLSearchParams();
data.append("action", "logout");
for (let interval of PageIntervals) {
clearInterval(interval);
}
if (currentSite === "account" && currentPage === "settings") {
if (document.getElementById("user-settings")) {
await populateUserInfoFields(UserInfo);
}
if (document.getElementById("admin-settings")) {
await listActivationCodes(true);
await listUsers(true);
}
}
if (currentSite === "account" && currentPage === "index" && isLoggedIn()) {
await showDashboardGreeting();
}
if (currentSite === "news" && currentPage === "index") {
await articleInit();
}
if (currentSite === "account" && currentPage === "files") {
await listFiles();
}
if (currentSite === "memes" && currentPage === "index") {
await getMemeImages();
}
}
async function navigateTo(site, page) {
"use strict";
const data = {
action: "getPage",
site: site,
page: page,
};
doPageAction(data).then(() => {
localStorage.setItem("currentSite", site);
localStorage.setItem("currentPage", page);
onPageLoad();
doAccountAction(data, "Logout Successful!", "Logout failed.").then(() => {
location.reload();
// Expected output: "Success!"
});
}
async function softReload() {
"use strict";
let currentSite = localStorage.getItem("currentSite");
let currentPage = localStorage.getItem("currentPage");
await navigateTo(currentSite, currentPage);
umami.track("softReload");
}
async function refreshNavbar() {
"use strict";
const data = {
action: "getNavigation",
};
await doPageAction(data);
umami.track("refreshNavbar");
await initAjaxNavigationEvents();
}
async function logout() {
"use strict";
const data = {
action: "logout",
};
doAction('/account', data, "Logout Successful!", "Logout failed.", false)
.then(async () => {
await refreshNavbar();
await navigateTo(localStorage.getItem("defaultSite"), localStorage.getItem("defaultPage"));
localStorage.clear();
UserInfo = {};
umami.track("logout");
})
.catch((error) => {
// Handle errors if needed
console.error("An error occurred during logout:", error);
});
}
async function login() {
"use strict";
const email = document.getElementById("login_email").value;
const password = document.getElementById("login_password").value;
await doLogin(email, password);
await getUserInfo();
await refreshNavbar();
await softReload();
}
async function doLogin(email, password) {
"use strict";
const data = {
action: "login",
email: email,
password: password,
};
await doAction('/account', data, "Login Successful!", "Login failed. Please check your credentials.", false);
umami.track("login");
}
async function register() {
"use strict";
const firstName = document.getElementById("register_firstName").value;
const lastName = document.getElementById("register_lastName").value;
const email = document.getElementById("register_email").value;
const password = document.getElementById("register_password").value;
const activationToken = document.getElementById("register_activationToken").value;
const data = {
action: "register",
firstname: firstName,
lastname: lastName,
email: email,
password: password,
activation_token: activationToken,
};
await doRegister(data);
}
async function doRegister(requestData) {
"use strict";
await doAction('/account', requestData, "Registration Successful!", "Registration failed.", false);
umami.track("register");
}
//User settings start
async function changePassword() {
"use strict";
const oldPassword = document.getElementById("changeOldPassword").value;
const newPassword = document.getElementById("changeNewPassword").value;
const data = {
action: "change_password",
old_password: oldPassword,
new_password: newPassword,
};
await doChangePassword(data, "Password change Successful!", "Password change failed.");
}
async function doChangePassword(requestData, successMessage, failureMessage) {
"use strict";
await doAction('/account', requestData, successMessage, failureMessage, false);
umami.track("passwordChange");
}
async function updateUserProfile() {
"use strict";
const firstName = document.getElementById("updateFirstName").value;
const lastName = document.getElementById("updateLastName").value;
const nickname = document.getElementById("updateNickname").value;
const minecraftNick = document.getElementById("updateMinecraftNick").value;
const data = {
action: "update_user_profile",
first_name: firstName,
last_name: lastName,
nickname: nickname,
minecraft_nick: minecraftNick,
};
await doAction('/account', data, "Profile update Successful!", "Profile update failed.", false);
umami.track("updateUserProfile");
}
async function updateEmail() {
"use strict";
const newEmail = document.getElementById("updateNewEmail").value;
const data = {
action: "update_user_email",
email: newEmail,
};
await doAction('/account', data, "Email update Successful!", "Email update failed.", false);
umami.track("updateEmail");
}
async function populateUserInfoFields(userData) {
"use strict";
document.getElementById("updateFirstName").value = userData.FirstName || "";
document.getElementById("updateLastName").value = userData.LastName || "";
document.getElementById("updateNickname").value = userData.Nickname || "";
document.getElementById("updateMinecraftNick").value = userData.MinecraftNick || "";
document.getElementById("updateNewEmail").value = userData.Email || "";
}
async function restoreUserInfo() {
"use strict";
for (let i = 0; i < localStorage.length; i++) {
let key = localStorage.key(i);
if (key.startsWith("UserInfo_")) {
let keyClean = key.replace("UserInfo_", "");
UserInfo[keyClean] = localStorage.getItem(key);
}
}
}
async function getUserInfo() {
"use strict";
const data = {
action: "get_user_info",
};
const result = await doAction('/account', data, "User info retrieved Successfully!", "User info retrieval failed.", true);
if (result && result.Status === "Success") {
Object.keys(result.UserInfo).forEach(index => {
let value = result.UserInfo[index];
localStorage.setItem("UserInfo_" + index, value);
UserInfo[index] = value;
});
}
}
//User settings end
//Admin settings start
async function addActivationCodes() {
"use strict";
const count = document.getElementById("activationCodeCount").value;
const data = {
action: "add_activation_codes",
count: count,
};
doAction('/account', data, "Activation codes added Successfully!", "Activation codes addition failed.", false).then((result) => {
displayList(result.ActivationCodes, "codeListTable", deleteActivationCode);
umami.track("addActivationCodes");
});
}
async function listUsers(silent) {
"use strict";
const data = {
action: "list_users",
};
doAction('/account', data, "User list retrieved Successfully!", "User list retrieval failed.", silent).then((result) => {
if (result && result.Status === "Success") {
displayList(result.Users, "userListTable", deleteUser);
}
});
}
async function listActivationCodes(silent) {
"use strict";
const data = {
action: "list_activation_codes",
};
doAction('/account', data, "Activation code list retrieved Successfully!", "Activation code list retrieval failed.", silent).then((result) => {
displayList(result.ActivationCodes, "codeListTable", deleteActivationCode);
});
}
async function deleteUser(userId) {
"use strict";
const data = {
action: "delete_user",
user_id: userId,
};
await doAction('/account', data, "User deleted Successfully!", "User deletion failed.", false);
await listUsers(false);
umami.track("deleteUser");
}
async function deleteActivationCode(activationCode) {
"use strict";
const data = {
action: "delete_activation_code",
activation_code: activationCode,
};
await doAction('/account', data, "Activation code deleted Successfully!", "Activation code deletion failed.", false);
await listActivationCodes(false);
umami.track("deleteActivationCode");
}
//Admin settings end
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initAjax);
} else {
setTimeout(initAjax, 0);
}
async function uploadFile() {
const fileInput = document.getElementById('fileInput');
const fileForm = document.getElementById('uploadForm');
let data = {
action: "uploadFiles"
};
for (let i = 0; i < fileInput.files.length; i++) {
data[`userFile${i}`] = fileInput.files[i];
}
await doAction("/upload", data, "Súbor bol úspešne nahraný", "Nastala chyba pri nahrávaní súboru", false);
fileForm.reset();
await listFiles();
}
async function deleteFile(fileID) {
await doAction("/upload", {
action: "deleteFile",
file_id: fileID
}, "Súbor bol úspešne zmazaný", "Nastala chyba pri mazaní súboru", true);
await listFiles();
}
async function getFileList() {
const resp = await doAction("/upload", {
action: "getAllFiles"
}, "Zoznam súborov bol úspešne stiahnutý", "Nastala chyba pri sťahovaní zoznamu súborov", true);
if (resp.Status === "Success") {
return resp.Files;
} else {
return false;
}
}
async function listFiles() {
const fileList = await getFileList();
if (fileList) {
await displayList(fileList, "filelist", deleteFile);
}
}
async function addMeme() {
let memeTitleElement = document.getElementById("meme_title_input");
let memeTextElement = document.getElementById("meme_text_input");
let memeImageElement = document.getElementById("meme_image_input");
await doAction("/meme", {
action: "addMeme",
meme_title: memeTitleElement.value,
meme_text: memeTextElement.value,
meme_image_id: memeImageElement.value
}, "Meme bol zmazaný", "Nastala chyba pri mazaní meme-u", false);
memeTitleElement.value = "";
memeTextElement.value = "";
memeImageElement.selectedIndex = 0
await togglememecreate();
}
async function deleteMeme(memeId) {
await doAction("/meme", {
action: "deleteMeme",
meme_id: memeId
}, "Meme bol zmazaný", "Nastala chyba pri mazaní meme-u", false);
await softReload();
}
async function getMemeImages() {
let memeImageSelector = document.getElementById("meme_image_input");
let fileList = await getFileList();
fileList.forEach((item) => {
let option = document.createElement("option");
option.value = item.ID;
let splitPath = item.Path.split("/");
option.text = `${splitPath[splitPath.length - 1]} - ID: (${item.ID}) Autor: [${item.UploadedBy} (${item.UploadedByID})]`;
memeImageSelector.appendChild(option);
});
}
async function reloadMemeVotes(memeID) {
let memeVoteCounterElement = document.getElementById(`meme_votes_counter_${memeID}`);
let memeVoteUpvoteElement = document.getElementById(`meme_votes_upvote_${memeID}`);
let memeVoteDownvoteElement = document.getElementById(`meme_votes_downvote_${memeID}`);
let memeVoteUpvoteButtonElement = document.getElementById(`meme_votes_upvote_button_${memeID}`);
let memeVoteDownvoteButtonElement = document.getElementById(`meme_votes_downvote_button_${memeID}`);
let memeVoteResponse = await doAction('/meme', {
action: "getMemeVotes",
meme_id: memeID
}, "Počet hlasov k meme-u bol stiahnutý", "Nastala chyba pri sťahovaní počtu hlasov k meme-u", true);
let memeVotes = memeVoteResponse.NetVotes;
let userVote = memeVoteResponse.UserVote;
memeVoteCounterElement.innerText = memeVotes;
memeVoteCounterElement.classList.remove("positive", "negative", "neutral");
if (0 < memeVotes) {
memeVoteCounterElement.classList.add("positive");
} else if (0 > memeVotes) {
memeVoteCounterElement.classList.add("negative");
} else {
memeVoteCounterElement.classList.add("neutral");
}
memeVoteUpvoteButtonElement.classList.remove('visual_hover');
memeVoteDownvoteButtonElement.classList.remove('visual_hover');
let memeUpvoteVariant = "line";
let memeDownvoteVariant = "line";
if (0 < userVote) {
memeUpvoteVariant = "fill";
memeVoteUpvoteButtonElement.classList.add('visual_hover');
} else if (0 > userVote) {
memeDownvoteVariant = "fill";
memeVoteDownvoteButtonElement.classList.add('visual_hover');
}
await setElementClasses(memeVoteUpvoteElement, [`ri-arrow-up-circle-${memeUpvoteVariant}`]);
await setElementClasses(memeVoteDownvoteElement, [`ri-arrow-down-circle-${memeDownvoteVariant}`])
}
async function voteMeme(memeID, isUpvote) {
let memeVoteUpvoteElement = document.getElementById(`meme_votes_upvote_${memeID}`);
let memeVoteDownvoteElement = document.getElementById(`meme_votes_downvote_${memeID}`);
let memeVoteDelete = false;
if (isUpvote) {
if (memeVoteUpvoteElement.classList.contains("ri-arrow-up-circle-fill")) {
await deleteVoteMeme(memeID);
memeVoteDelete = true;
}
} else {
if (memeVoteDownvoteElement.classList.contains("ri-arrow-down-circle-fill")) {
await deleteVoteMeme(memeID);
memeVoteDelete = true;
}
}
if (!memeVoteDelete) {
await doAction("/meme", {
action: "voteMeme",
meme_id: memeID,
is_upvote: isUpvote
}, "Meme bol votovaný", "Nastala chyba pri votovaný", true);
}
await reloadMemeVotes(memeID);
}
async function deleteVoteMeme(memeId) {
await doAction("/meme", {
action: "deleteVoteMeme",
meme_id: memeId
}, "Hlas na meme bol zmazaný", "Nastala chyba pri mazaní hlasu na meme", true);
await reloadMemeVotes(memeId);
}
async function surveySubmit() {
const satisfaction = document.querySelector('input[name="satisfaction"]:checked');
const functionality = document.querySelector('input[name="functionality"]:checked');
const content = document.querySelector('input[name="content"]:checked');
const comment = document.querySelector('textarea[name="comment"]');
if (satisfaction && functionality && content && comment.value) {
await doAction("/survey", {
action: "surveySubmit",
satisfaction: satisfaction.value,
functionality: functionality.value,
content: content.value,
comment: comment.value
}, "Zaznamenané",
"Nastala chyba");
satisfaction.checked = false;
functionality.checked = false;
content.checked = false;
comment.value = "";
}
}
async function toggleRegister() {
let loginForm = document.getElementById("sign_in_form");
let registerForm = document.getElementById("sign_up_form");
loginForm.classList.toggle('hidden');
registerForm.classList.toggle('hidden');
}

@ -1,504 +1,256 @@
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap');
:root {
--error: #ff3700;
--pico-primary: #2a9dd6;
--pico-primary-background: #1b1529;
--pico-primary-hover: #2489bb;
--pico-secondary: #d2d6e5;
--pico-secondary-background: #1a1a1a;
--dimmer: rgba(0, 0, 0, 0.6);
--primary-bg: rgb(27, 21, 41);
--secondary-bg: #1a1a1a;
--third-bg: #383838;
--primary-text: #d2d6e5;
--error: rgb(255, 55, 0);
--primary: #2a9dd6;
--primary-hover: #2489bb;
}
body {
display: grid;
width: 100%;
grid-template-areas: "nav nav nav nav"
"main main main main"
"foot foot foot foot";
grid-template-rows: min-content 1fr min-content;
align-items: center;
background: linear-gradient(127deg, var(--pico-secondary-background), var(--pico-primary-background)) no-repeat fixed;
background: linear-gradient(127deg, var(--secondary-bg), var(--primary-bg)) no-repeat fixed;
background-size: cover;
flex-direction: column;
font-family: \'Poppins\', sans-serif;
height: 100%;
width: 100%;
font-family: 'Poppins', sans-serif;
color: var(--primary-text);
margin: 0;
padding: 0;
min-height: 100vh;
min-width: 100vw;
}
.dashboard {
height: 100%;
text-align: center;
width: 100%;
}
body > nav,
body > footer {
background-color: var(--dimmer);
padding: 1.2rem;
text-align: center;
}
body > nav {
nav {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 1.2rem 1rem;
background-color: rgba(0, 0, 0, 0.2);
-webkit-box-shadow: 0 20px 28px 0 rgba(0,0,0,0.2);
-moz-box-shadow: 0 20px 28px 0 rgba(0,0,0,0.2);
box-shadow: 0 20px 28px 0 rgba(0,0,0,0.2);
height: 75px !important;
position: relative;
z-index: 500;
grid-area: nav;
box-shadow: 0 20px 28px 0 var(--dimmer);
margin-bottom: 20px;
}
body > footer {
grid-area: foot;
box-shadow: 0 -20px 28px 0 var(--dimmer);
margin-top: 20px;
}
body > main#page_container {
grid-area: main;
height: 100%;
}
body > main#page_container > main {
height: 100%;
}
hr {
border-color: var(--pico-primary);
opacity: 0.5;
width: 30%;
margin: var(--pico-typography-spacing-vertical) auto;
ul {
display: flex;
flex-direction: row;
gap: 2.5rem;
list-style: none;
padding-left: 0;
}
li {
list-style: none;
}
nav li {
margin: unset;
}
header ul li {
list-style: circle;
width: fit-content;
}
ul {
display: flex;
flex-direction: column;
list-style: none;
padding-left: 0;
}
header {
align-items: center;
text-align: center;
width: 100%;
margin-top: 35px;
margin-bottom: 35px;
}
li a {
padding-bottom: .45rem;
position: relative;
padding-bottom: 0.45rem;
color: var(--primary-text);
text-decoration: none;
transition: all .3s ease;
color: var(--pico-secondary);
transition: all 0.3s ease;
}
/*li a::after {
content: "";
position: absolute;
height: 4px;
width: 0;
bottom: 0;
left: 0;
background-color: var(--primary);
transition: all 0.3s ease;
border-radius: 15px;
}*/
li a:hover::after {
margin: 0 auto;
width: 85%;
margin: 0 auto;
}
li.navpage_item {
padding: 0 20px;
}
table>tbody,
table>tbody>tr,
table>tbody>tr>th,
table>tbody>tr>td {
border: 2px solid var(--pico-primary);
border-collapse: collapse !important;
.wrapper-40x {
text-align: center;
}
table>tbody>tr>td>button {
border: unset;
border-radius: unset;
border-collapse: unset;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
}
table {
border-collapse: collapse;
}
ul.navpage_list {
gap: 10px;
border: 4px solid var(--pico-primary-hover) !important;
display: none;
flex-direction: column;
overflow: hidden;
transition: max-height .3s ease, border .325s ease !important;
z-index: 2;
position: fixed;
background: #00000066;
}
.back {
border-radius: 15px;
padding: .35rem .65rem;
text-decoration: none;
transition: all .3s ease;
}
.feature-list {
display: block;
margin: auto;
width: fit-content;
}
.feature-list-ul ul {
margin: 5px auto auto 20px;
}
.navsite_item {
align-items: center;
justify-content: center;
text-align: center;
}
.navsite_item:hover .navpage_list {
border: 4px solid var(--pico-primary-hover) !important;
display: flex !important;
max-height: unset;
width: inherit;
transition: max-height .3s ease, border .325s ease !important;
box-sizing: border-box;
}
.navsite_item:not(:hover) .navpage_list {
border: 0 solid transparent;
max-height: 0;
transition: max-height .3s ease, border .325s ease !important;
width: inherit;
}
#navsite_list {
display: flex;
flex-direction: row;
gap: 3.25rem;
text-align: center;
padding-right: 60px;
}
#toggle_button {
position: absolute;
right: 1.5rem;
display: none;
flex-direction: column;
justify-content: space-between;
width: 2rem;
height: 1.5rem;
}
#statusMessageContainer {
position: fixed;
top: 100px;
right: 20px;
z-index: 510;
display: flex;
flex-direction: column;
}
/*noinspection CssUnusedSymbol*/
.status-message {
background-color: #dff0d8;
/* Success background color */
border: 1px solid #3c763d;
/* Success border color */
color: #3c763d;
/* Success text color */
padding: 15px;
margin-bottom: 10px;
opacity: 1;
transition: opacity 0.5s ease-in-out;
}
/*noinspection CssUnusedSymbol*/
.status-message.failure {
background-color: #f2dede;
/* Failure background color */
border: 1px solid #a94442;
/* Failure border color */
color: #a94442;
/* Failure text color */
}
.wrapper-40x .error {
color: var(--error);
}
.wrapper-40x h1 {
font-size: 10rem;
margin: 0;
}
.wrapper-40x .error {
color: var(--error);
}
.wrapper-40x h3 {
margin-bottom: 2rem;
}
.wrapper-40x {
.error-code {
color: var(--primary);
}
.back {
color: var(--primary-text);
text-decoration: none;
background-color: #2a9dd6;
padding: 0.35rem 0.65rem;
transition: all 0.3s ease;
border-radius: 15px;
}
.back:hover {
transition: all 0.3s ease;
background-color: var(--primary-hover);
}
header {
width: 100%;
align-items: center;
text-align: center;
}
header h1 {
margin: 0;
padding: 0;
}
header a {
color: var(--primary);
}
header hr {
border-color: var(--primary);
opacity: 0.5;
width: 30%;
}
.navsite_list{
display: flex;
flex-direction: row;
gap: 3.25rem;
}
.navpage_list{
display: none;
flex-direction: column;
}
.navsite_item {
justify-content: center;
align-items: center;
text-align: center;
width: 120px;
}
.navsite_item:hover .navpage_list{
display: flex !important;
}
.navpage_list {
background-color: var(--third-bg);
margin-top: 10px;
display: flex;
flex-direction: column;
overflow: hidden;
max-height: 0;
border: 0 solid transparent;
transition: max-height 0.3s ease, border 0.325s ease;
position: relative;
z-index: 999;
}
.navsite_item:hover .navpage_list {
max-height: 200px;
border: 4px solid var(--primary-hover);
}
.navsite_item:not(:hover) .navpage_list {
max-height: 0;
border: 0 solid transparent;
transition-delay: 0.1s;
}
li.navpage_item{
padding-left: 20px;
padding-right: 20px;
}
ul.navpage_list{
gap: 10px;
}
header ul {
display: flex;
flex-direction: column;
gap: 0;
text-align: left;
width: fit-content;
}
header ul li {
list-style: circle;
width: fit-content;
}
.feature-list {
margin: auto;
width: fit-content;
display: block;
}
.feature-list-ul {
margin: auto;
}
.feature-list-ul ul {
margin-top: 5px;
margin-left: 20px;
}
#StatusMessage {
display: none;
position: fixed;
top: 20px;
right: 20px;
padding: 10px;
background-color: var(--third-bg);
color: var(--primary-text);
border-radius: 5px;
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
table.list-table > tbody, table.list-table > tbody > th, table.list-table > tbody > tr, table.list-table > tbody > tr > td {
border: 2px solid var(--primary);
border-collapse: collapse;
}
/* <DASHBOARD STYLING> */
/* ZAČÍNAJ VŠETKO S ".dashboard" */
.dashboard {
width: 100%;
height: 100%;
text-align: center;
}
header a,
.error-code {
color: var(--pico-primary);
}
/* </DASHBOARD STYLING> */
input, textarea {
border: 2px solid var(--pico-primary);
}
input{
border-radius: 25px;
}
textarea{
border-radius: 10px;
}
.form-container input {
#loginForm input {
border-radius: 50px;
background: none;
border: 2px solid var(--pico-primary);
border: 2px solid var(--primary);
width: 175px;
}
span#ye-span:hover + body{
background: url('/assets/images/ye.jpg') repeat !important;
background-size: 20% !important;
}
#articlecreate, #memecreate {
border: 5px solid var(--pico-primary);
z-index: 5;
margin: auto;
padding: 40px;
background-color: var(--pico-primary-background);
#register_Form input {
border-radius: 50px;
}
#articlecreate > * {
margin: 10px 0;
}
#articlecreateopen {
display: none;
}
#articlecreatecontainer, #memecreatecontainer{
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 12vh;
left: 0;
width: 100vw;
height: 88vh;
z-index: 4;
backdrop-filter: blur(2px);
}
.hidden {
display: none !important;
}
div#articleslist > article > div.articleinfo > *{
width: fit-content;
}
/*noinspection CssUnusedSymbol*/
div#articleslist > article > div.articleinfo{
display: flex;
flex-direction: row;
}
div#articleslist>article{
border: 4px solid var(--pico-primary);
}
.form-content {
display: flex;
flex-direction: row;
}
.form-container {
display: flex;
flex-direction: column;
}
.meme_image {
max-width: 500px;
max-height: 300px;
width: auto;
height: auto;
}
.meme_link {
width: fit-content;
height: fit-content;
}
.meme_info, .meme_topbar {
display: flex;
flex-direction: row;
height: fit-content;
width: 100%;
justify-content: right;
}
.meme, .meme_body {
display: flex;
flex-direction: column;
padding: 0;
margin: 0;
}
.positive {
color: #008000;
}
.negative {
color: #ff0000;
}
.neutral {
color: var(--pico-color);
}
.visual_hover {
--pico-background-color: var(--pico-primary-hover-background);
--pico-border-color: var(--pico-primary-hover-border);
--pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));
--pico-color: var(--pico-primary-inverse);
}
.visual_hover.meme_upvote {
--pico-background-color: #008000;
--pico-border-color: unset;
}
.visual_hover.meme_downvote {
--pico-background-color: #ff0000;
--pico-border-color: unset;
}
#meme_gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));
grid-auto-rows: 1fr;
gap: 20px
}
@media (max-width: 1050px) {
table .rozvrh {
overflow: auto;
}
.navsite_item .navpage_list {
max-height: unset !important;
}
div#articleslist {
width: 100vw !important;
left: 0 !important;
}
#toggle_button {
display: flex;
}
#navsite_list {
display: none;
position: fixed;
flex-direction: column;
width: 100%;
text-align: center;
padding: 0 8px;
left: 0;
right: 0;
margin: 0;
gap: 1rem;
}
#navsite_list li {
text-align: center;
padding: 0;
}
.navsite_item {
width: inherit;
}
ul.navpage_list {
border: 4px solid var(--pico-primary-hover) !important;
display: flex !important;
max-height: 200px !important;
width: inherit;
box-sizing: content-box;
transition-delay: .1s;
position: unset !important;
}
.navsite_item:not(:hover) .navpage_list {
transition-delay: .1s;
width: inherit;
}
/*noinspection CssUnusedSymbol*/
#navsite_list.active {
display: flex;
-moz-box-shadow: 0 20px 28px 0 var(--dimmer);
-webkit-box-shadow: 0 20px 28px 0 var(--dimmer);
background-color: var(--pico-primary-background);
box-shadow: 0 20px 28px 0 var(--dimmer);
top: 100px;
text-align: center;
left: 0;
}
nav {
flex-direction: column;
align-items: center;
background-color: var(--dimmer);
}
.form-content {
flex-direction: column;
}
.meme_image {
max-width: 200px;
max-height: 200px;
}
.navsite_link {
width: 100%;
}
background: none;
border: 2px solid var(--primary);
width: 175px;
}

@ -1,17 +0,0 @@
<?php
require_once "lib/meme.php";
function endpoint($endpoint_data): array
{
return match ($endpoint_data["action"]) {
"addMeme" => addMeme($endpoint_data['meme_title'], $endpoint_data['meme_text'], $endpoint_data['meme_image_id']),
"getMemes" => getMemeGallery($endpoint_data['offset'], $endpoint_data['meme_author'], $endpoint_data['meme_id'], $endpoint_data['meme_keyword']),
"deleteMeme" => deleteMeme($endpoint_data['meme_id']),
"getMemeVotes" => getMemeVotes($endpoint_data['meme_id']),
"deleteVoteMeme" => deleteVoteMeme($endpoint_data['meme_id']),
"voteMeme" => voteMeme($endpoint_data['meme_id'], $endpoint_data['is_upvote']),
default => ["Status" => "Fail", "Message" => "Invalid action"],
};
}

@ -1,22 +0,0 @@
<?php
require_once "lib/newsarticle.php";
function endpoint($endpoint_data): array
{
return match ($endpoint_data["action"]) {
"getNewsArticles" => getNewsArticles(),
"addNewsArticle" => addNewsArticle(
$endpoint_data["title"],
$endpoint_data["body"]
),
"addNewsComment" => addNewsComment(
$endpoint_data["user_id"],
$endpoint_data['news_article_id'],
$endpoint_data["comment_text"],
$endpoint_data["parent_id"]
),
default => ["Status" => "Fail", "message" => "Invalid action"],
};
}

@ -1,13 +0,0 @@
<?php
require_once "lib/page.php";
require_once "lib/navigation.php";
function endpoint($endpoint_data): array
{
return match ($endpoint_data["action"]) {
"getNavigation" => getNavigationEndpoint(),
"getPage" => getPageEndpoint($endpoint_data["page"], $endpoint_data["site"]),
default => ["Status" => "Fail", "message" => "Invalid action"],
};
}

@ -1,11 +0,0 @@
<?php
require_once "lib/survey.php";
function endpoint($endpoint_data): array
{
return match ($endpoint_data["action"]) {
"surveySubmit" => submitSurvey($endpoint_data["satisfaction"], $endpoint_data["functionality"], $endpoint_data["content"], $endpoint_data["comment"]),
default => ["Status" => "Fail", "message" => "Invalid action"],
};
}

@ -1,18 +0,0 @@
<?php
require_once "lib/upload.php";
function endpoint($endpoint_data): array
{
return match ($endpoint_data["action"]) {
"getMyFiles" => listFiles(),
"getAllFiles" => listFiles(false),
"uploadFiles" => parseIncomingFiles(),
"deleteFile" => deleteFile($endpoint_data['file_id']),
"addToGroup" => addToGroup($endpoint_data['group_id'], $endpoint_data['file_id']),
"myFileExists" => fileExists($endpoint_data['file_id']),
"FileExists" => fileExists($endpoint_data['file_id'], false),
default => ["Status" => "Fail", "message" => "Invalid action"],
};
}

@ -1,40 +1,35 @@
<?php
/** @noinspection PhpIncludeInspection */
// Include essential configuration and function libraries.
require_once 'secrets/config.php'; // Load sensitive configuration such as database credentials.
require_once 'lib/config.php'; // Load general site configuration settings.
require_once 'lib/navigation.php'; // Include functions related to navigation generation.
require_once 'lib/router.php'; // Include routing functionality to manage URL routing.
require_once 'lib/page.php'; // Functions related to page content generation and management.
require_once 'lib/endpoint.php'; // Functions for handling API endpoints.
require_once 'lib/account.php'; // Include user account management functionality.
require_once 'secrets/config.php';
require_once 'lib/config.php';
require_once 'lib/navigation.php';
require_once 'lib/router.php';
require_once 'lib/page.php';
require_once 'lib/endpoint.php';
require_once 'lib/account.php';
// Load configuration for the router from the configuration files.
$routerConfig = loadRouterConfig();
$routerConfig = array();
$routerRequest = array();
// Initialize the router to parse the request URI and determine the requested site/page.
$routerRequest = initRouter();
loadRouterConfig();
$canRender = initRouter();
// Start or resume a session to manage user sessions across requests.
session_start();
if ($canRender) {
/** @noinspection PhpArrayIsAlwaysEmptyInspection */
/** @noinspection PhpArrayIsAlwaysEmptyInspection */
session_set_cookie_params(0, '/', "." . $routerRequest["domain"] . "." . $routerRequest["tld"], true, true);
session_start();
// Set default session data if the user is not logged in.
if (!isLoggedIn()) {
setDefaultSessionData();
}
if (!isLoggedIn()) {
setDefaultSessionData();
}
// Handle requests for the sitemap.
if ($routerRequest["site_name"] == "sitemap.xml") {
require "lib/sitemap.php"; // Include sitemap generation functions.
echo generateSitemap(); // Generate and output the sitemap XML.
exit(); // Stop script execution after sitemap generation.
}
/** @noinspection PhpArrayIsAlwaysEmptyInspection */
if ($routerRequest["type"] == "api") {
/** @noinspection PhpArrayIsAlwaysEmptyInspection */
echo getEndpoint($routerRequest["page_name"]);
// Handle API type requests by fetching and outputting the endpoint response.
if ($routerRequest["type"] == "api") {
echo getEndpoint($routerRequest["site_name"]);
}
// Handle page type requests by fetching and rendering the page content.
elseif ($routerRequest["type"] == "page") {
echo getPage($routerRequest["site_name"], $routerRequest["page_name"]);
}
} /** @noinspection PhpArrayIsAlwaysEmptyInspection */ elseif ($routerRequest["type"] == "page") {
/** @noinspection PhpArrayIsAlwaysEmptyInspection */
echo getPage($routerRequest["page_name"]);
}
}

@ -1,95 +1,52 @@
<?php
use Random\RandomException;
/**
* Checks if the current session represents a logged-in user.
*
* @global array $routerConfig Global configuration array containing permission thresholds.
* @return bool Returns true if the user is logged in and meets the minimum privilege level; otherwise, false.
*/
function isLoggedIn(): bool
{
global $routerConfig;
return $_SESSION["ID"] > 0 && !empty($_SESSION["email"]) && $_SESSION["privilege_level"] >= $routerConfig["permissions"]["logged_in_default"];
return $_SESSION["ID"] > 0 && !empty($_SESSION["email"]) && $_SESSION["privilege_level"] >= $routerConfig["logged_in_default_permission_level"];
}
/**
* Checks if the logged-in user is verified.
*
* @global array $routerConfig Global configuration array containing permission thresholds.
* @return bool Returns true if the user is logged in and verified; otherwise, false.
*/
function isVerified(): bool
{
global $routerConfig;
return isLoggedIn() && $_SESSION["privilege_level"] >= $routerConfig["permissions"]["verified"];
return isLoggedIn() && $_SESSION["privilege_level"] >= $routerConfig["verified_permission_level"];
}
/**
* Checks if the logged-in user is trustworthy.
*
* @global array $routerConfig Global configuration array containing permission thresholds.
* @return bool Returns true if the user is logged in and considered trustworthy; otherwise, false.
*/
function isTrustWorthy(): bool
{
global $routerConfig;
return isLoggedIn() && $_SESSION["privilege_level"] >= $routerConfig["permissions"]["trustworthy"];
return isLoggedIn() && $_SESSION["privilege_level"] >= $routerConfig["trustworthy_permission_level"];
}
/**
* Checks if the logged-in user is a moderator.
*
* @global array $routerConfig Global configuration array containing permission thresholds.
* @return bool Returns true if the user is logged in and a moderator; otherwise, false.
*/
function isModerator(): bool
{
global $routerConfig;
return isLoggedIn() && $_SESSION["privilege_level"] >= $routerConfig["permissions"]["moderator"];
return isLoggedIn() && $_SESSION["privilege_level"] >= $routerConfig["moderator_permission_level"];
}
/**
* Checks if the logged-in user is a user admin.
*
* @global array $routerConfig Global configuration array containing permission thresholds.
* @return bool Returns true if the user is logged in and a user admin; otherwise, false.
*/
function isUserAdmin(): bool
{
global $routerConfig;
return isLoggedIn() && $_SESSION["privilege_level"] >= $routerConfig["permissions"]["user_admin"];
return isLoggedIn() && $_SESSION["privilege_level"] >= $routerConfig["user_admin_permission_level"];
}
/**
* Checks if the logged-in user is an admin.
*
* @global array $routerConfig Global configuration array containing permission thresholds.
* @return bool Returns true if the user is logged in and an admin; otherwise, false.
*/
function isAdmin(): bool
{
global $routerConfig;
return isLoggedIn() && $_SESSION["privilege_level"] >= $routerConfig["permissions"]["admin"];
return isLoggedIn() && $_SESSION["privilege_level"] >= $routerConfig["admin_permission_level"];
}
/**
* Generates a secure token for account activation or other purposes using cryptographic methods.
*
* @return string|null Returns a hexadecimal token or null in case of an error.
*/
function generateActivationToken(): ?string
function generateActivationToken(): string
{
try {
return bin2hex(random_bytes(16));
} catch (RandomException) {
return null;
}
}
/**
* Checks if an email address is available for registration.
*
* @param string $email The email address to check.
* @return bool Returns true if the email is not already registered; otherwise, false.
*@global mysqli $mysqli Global mysqli object for database access.
*/
function isEmailAvailable(string $email): bool
function isEmailAvailable($email): bool
{
global $mysqli;
$stmt = $mysqli->prepare("SELECT COUNT(*) FROM Users WHERE Email = ?");
@ -102,12 +59,7 @@ function isEmailAvailable(string $email): bool
return $count === 0;
}
/**
* Sets default session data typically used for a logged-out user(includes users that have just visited the page).
*
* @global array $routerConfig Global configuration array used for setting initial privilege levels.
* @return void
*/
function setDefaultSessionData(): void
{
global $routerConfig;
@ -117,17 +69,10 @@ function setDefaultSessionData(): void
$_SESSION["nickname"] = "";
$_SESSION["email"] = "";
$_SESSION["minecraft_nickname"] = "";
$_SESSION["privilege_level"] = $routerConfig["permissions"]["logged_out"];
$_SESSION["privilege_level"] = $routerConfig["logged_out_permission_level"];
}
/**
* Verifies if the provided password matches the stored hash for the user.
*
* @param int $userID The user ID whose password is to be verified.
* @param string $password The password to verify.
* @return bool Returns true if the password matches the stored hash; otherwise, false.
*@global mysqli $mysqli Global mysqli object for database access.
*/
function verifyPassword(int $userID, string $password): bool
function verifyPassword($userID, $password): bool
{
global $mysqli;
$stmt = $mysqli->prepare("SELECT PasswordHash FROM Users WHERE ID = ?");
@ -140,22 +85,18 @@ function verifyPassword(int $userID, string $password): bool
return !empty($password_hash) && !empty($password) && password_verify($password, $password_hash);
}
/**
* Updates session data from the database for the logged-in user.
*
* @global mysqli $mysqli Global mysqli object for database access.
* @return void
*/
function UpdateSession(): void
{
function UpdateSession(){
global $mysqli;
$stmt = $mysqli->prepare("SELECT FirstName, LastName, Nickname, Email, MinecraftNick, PrivilegeLevel, LastLoginAt, LoginCount, ClassID, FavoriteColor FROM Users WHERE ID = ? AND isActivated = 1");
$stmt->bind_param("i", $_SESSION["ID"]);
$stmt->execute();
$uid = 0;
$first_name = "";
$last_name = "";
$nickname = "";
$password_hash = "";
$email = "";
$minecraft_nickname = "";
$privilege_level = 0;
@ -166,6 +107,7 @@ function UpdateSession(): void
$stmt->bind_result($first_name, $last_name, $nickname, $email, $minecraft_nickname, $privilege_level, $lastLoginAt, $loginCount, $class_id, $favorite_color);
$stmt->fetch();
$stmt->close();
$_SESSION["first_name"] = $first_name;
$_SESSION["last_name"] = $last_name;
$_SESSION["nickname"] = $nickname;
@ -178,17 +120,10 @@ function UpdateSession(): void
$_SESSION["favorite_color"] = $favorite_color;
}
/**
* Attempts to log in a user with the given credentials.
*
* @param string $email The user's email address.
* @param string $password The user's password.
* @global mysqli $mysqli Global database connection object.
* @return array An array containing the status of the login attempt ('Success' or 'Fail').
*/
function doLogin(string $email, string $password): array
function doLogin($email, $password): array
{
global $mysqli;
global $mysqli, $routerConfig;
$found = false;
if (!empty($email) && !empty($password)) {
$stmt = $mysqli->prepare("SELECT ID, PasswordHash FROM Users WHERE Email = ? AND isActivated = 1");
@ -216,12 +151,7 @@ function doLogin(string $email, string $password): array
}
return $found ? ["Status" => "Success"] : ["Status" => "Fail"];
}
/**
* Logs out the current user by resetting session data.
* Fails when the user wasn't logged in
*
* @return array An array with the logout status ('Success' if logged out, 'Fail' otherwise).
*/
function doLogout(): array
{
if(isLoggedIn()){
@ -231,19 +161,8 @@ function doLogout(): array
return ["Status" => "Fail"];
}
}
/**
* Registers a new user with provided personal details and activation token.
*
* @param string $firstname The user's first name.
* @param string $lastname The user's last name.
* @param string $email The user's email.
* @param string $password The user's password.
* @param string $activation_token The activation token to verify the registration.
* @global mysqli $mysqli Global database connection object.
* @global array $routerConfig Global configuration settings.
* @return array An array with the registration status ('Success' or 'Fail').
*/
function doRegister(string $firstname, string $lastname, string $email, string $password, string $activation_token): array
function doRegister($firstname, $lastname, $email, $password, $activation_token): array
{
global $mysqli, $routerConfig;
$status = ["Status" => "Fail"];
@ -252,7 +171,7 @@ function doRegister(string $firstname, string $lastname, string $email, string $
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $mysqli->prepare("UPDATE Users SET FirstName=?, LastName=?, Email=?, PasswordHash=?, PrivilegeLevel=?, isActivated=1, ActivationToken='', RegisteredAt=NOW() WHERE ActivationToken = ?");
$privilege_level = $routerConfig["permissions"]["logged_in_default"];
$privilege_level = $routerConfig["logged_in_default_permission_level"];
/** @noinspection SpellCheckingInspection */
$stmt->bind_param("ssssis", $firstname, $lastname, $email, $passwordHash, $privilege_level, $activation_token);
@ -269,15 +188,8 @@ function doRegister(string $firstname, string $lastname, string $email, string $
return $status;
}
/**
* Changes the user's password after verifying the old password.
*
* @param string $oldPassword The current password for verification.
* @param string $newPassword The new password to be set.
* @return array An array indicating whether the password change was successful ('Success' or 'Fail').
*@global mysqli $mysqli Global database connection object.
*/
function changePassword(string $oldPassword, string $newPassword): array
function changePassword($oldPassword, $newPassword): array
{
global $mysqli;
$status = ["Status" => "Fail"];
@ -296,34 +208,23 @@ function changePassword(string $oldPassword, string $newPassword): array
}
/**
* Updates user profile information in the database.
*
* @param string $firstName The new first name.
* @param string $lastName The new last name.
* @param string $nickname The new nickname.
* @param string $minecraft_nickname The new Minecraft nickname.
* @return array Status of the profile update ('Success' or 'Fail').
*@global mysqli $mysqli Global database connection object.
*/
function updateUserProfile(string $firstName, string $lastName, string $nickname, string $minecraft_nickname): array
// Function to update user profile
function updateUserProfile($firstName, $lastName, $nickname, $minecraft_nickname): array
{
global $mysqli;
$status = ["Status" => "Fail"];
if (isLoggedIn() && !empty($firstName) && !empty($lastName) && !empty($nickname) && !empty($minecraft_nickname)) {
$userID = $_SESSION["ID"];
$stmt = $mysqli->prepare("UPDATE Users SET FirstName = ?, LastName = ?, Nickname = ?, MinecraftNick = ? WHERE ID = ?");
/** @noinspection SpellCheckingInspection */
$stmt->bind_param("ssssi", $firstName, $lastName, $nickname, $minecraft_nickname, $_SESSION["ID"]);
$stmt->bind_param("ssssi", $firstName, $lastName, $nickname, $minecraft_nickname, $userID);
$stmt->execute();
if ($stmt->affected_rows > 0) {
$status["Status"] = "Success";
}
else {
$status["Status"] = "$firstName $lastName $nickname $minecraft_nickname";
}
$stmt->close();
}
@ -331,14 +232,8 @@ function updateUserProfile(string $firstName, string $lastName, string $nickname
return $status;
}
/**
* Updates the email address of the logged-in user after validation.
*
* @param string $email The new email address to update.
* @return array Status of the email update ('Success' or 'Fail').
*@global mysqli $mysqli Global database connection object.
*/
function updateUserEmail(string $email): array
// Function to update user email
function updateUserEmail($email): array
{
global $mysqli;
$status = ["Status" => "Fail"];
@ -381,11 +276,7 @@ function updateUserEmail(string $email): array
return $status;
}
/**
* Retrieves and updates the current session with user information from the database.
*
* @return array Contains user information and status if the user is logged in.
*/
function getUserInfo(): array
{
$output = ["Status" => "Fail"];
@ -421,16 +312,10 @@ function getUserInfo(): array
return $output;
}
/**
* Generates a specified number of activation codes for user registration and adds them to the database.
*
* @param int $count Number of activation codes to generate.
* @return array An array containing the generated codes and status ('Success' or 'Fail').
*@global mysqli $mysqli Global database connection object.
*/
function addActivationCodes(int $count): array
function addActivationCodes($count): array
{
global $mysqli;
global $mysqli, $routerConfig;
$activationCodes = [];
$output = ["Status" => "Fail"]; // Default Status is "Fail"
@ -459,20 +344,15 @@ function addActivationCodes(int $count): array
return $output;
}
/**
* Lists all registered users, available only to user admins.
*
* @global mysqli $mysqli Global database connection object.
* @return array An array containing user data and status.
*/
function listUsers(): array
{
global $mysqli;
global $mysqli, $routerConfig;
$output = ["Status" => "Fail"]; // Default Status is "Fail"
if (isUserAdmin()) {
$users = [];
$result = $mysqli->query("SELECT ID, FirstName, LastName, Nickname, Email, MinecraftNick, PrivilegeLevel, CreatedAt, RegisteredAt, LastLoginAt, LoginCount, CreatedBy FROM Users WHERE isActivated = 1");
$result = $mysqli->query("SELECT ID, FirstName, LastName, Nickname, Email, MinecraftNick, PrivilegeLevel, CreatedAt, RegisteredAt, LastLoginAt, LoginCount, CreatedBy FROM Users");
// Check if the query executed Successfully
if ($result) {
@ -486,15 +366,10 @@ function listUsers(): array
return $output;
}
/**
* Lists activation codes available for assigning to new users, available only for user admins.
*
* @global mysqli $mysqli Global database connection object.
* @return array An array containing activation codes and status.
*/
function listActivationCodes(): array
{
global $mysqli;
global $mysqli, $routerConfig;
$output = ["Status" => "Fail"]; // Default Status is "Fail"
if (isUserAdmin()) {
@ -536,16 +411,10 @@ function listActivationCodes(): array
return $output;
}
/**
* Deletes a user by their ID, available only to user admins.
*
* @param int $userID The ID of the user to delete.
* @return array Status of the delete operation ('Success' or 'Fail').
*@global mysqli $mysqli Global database connection object.
*/
function deleteUser(int $userID): array
function deleteUser($userID): array
{
global $mysqli;
global $mysqli, $routerConfig;
$status = ["Status" => "Fail"];
if (!empty($userID) && isUserAdmin()) {
$stmt = $mysqli->prepare("DELETE FROM Users WHERE ID = ?");
@ -558,16 +427,10 @@ function deleteUser(int $userID): array
}
return $status;
}
/**
* Deletes an activation code, available only to user admins.
*
* @param string $activationCode The activation code to delete.
* @return array Status of the delete operation ('Success' or 'Fail').
*@global mysqli $mysqli Global database connection object.
*/
function deleteActivationCode(string $activationCode): array
function deleteActivationCode($activationCode): array
{
global $mysqli;
global $mysqli, $routerConfig;
$status = ["Status" => "Fail"];
if (!empty($activationCode) && isUserAdmin()) {
$stmt = $mysqli->prepare("DELETE FROM Users WHERE ActivationToken = ?");

@ -1,68 +1,35 @@
<?php
/**
* Loads and returns the configuration settings for the router.
*
* This configuration includes various paths, default settings, security levels, SEO settings,
* and other parameters essential for the operation of the router and the website's page management.
* The configuration array is structured to provide easy access to paths, protocols, permissions,
* and other critical settings that define how the router handles requests and serves content.
*
* @return array Returns an associative array containing all router configuration settings, such as:
* - 'inlining': Boolean value determining if CSS/JS should be inlined.
* - 'domain': The primary domain name of the website.
* - 'tld': Top-level domain for the website.
* - 'default_page': Default page to load if no specific page is requested.
* - 'default_site': Default site to load if no specific site is requested.
* - 'template_dir': Directory path where templates are stored.
* - 'endpoint_dir': Directory path for endpoint scripts.
* - 'page_dir': Directory path where site pages are stored.
* - 'protocol': Protocol to be used (e.g., 'https://').
* - 'site_prefix': Prefix for the site title.
* - 'permissions': Associative array of user permissions by role.
* - 'page': Default settings for pages including secret status and permissions.
* - 'newsarticle': Default permissions for news articles.
* - 'seo': Search engine optimization settings like author, description, and keywords.
*/
function loadRouterConfig(): array
function loadRouterConfig(): void
{
global $routerConfig;
$routerConfig["default_page"] = "index";
return [
'inlining' => false,
'domain' => 'adlerka',
'tld' => 'top',
'default_page' => 'index',
'default_site' => 'home',
'template_dir' => 'templates/',
'endpoint_dir' => 'endpoints/',
'page_dir' => 'pages/',
'protocol' => 'https://',
'site_prefix' => 'Adlerka',
'permissions' => [
'logged_out' => 1,
'logged_in_default' => 2,
'verified' => 3,
'trustworthy' => 4,
'moderator' => 5,
'user_admin' => 254,
'admin' => 255,
],
'page' => [
'default_secret' => 1,
'default_permissions' => 255,
$routerConfig["default_site"] = "home";
],
'newsarticle' => [
'default_permissions' => 255,
],
'meme' => [
'per_page' => 10
],
'seo' => [
'author' => 'Tím AdlerkaTop',
'description' => 'Toto je neoficiánla študentská stránka pre Adlerku, kde môžete nájsť plno zaujímavostí.',
'keywords' => 'adlerka, alderka, studenti, studentska stranka, web, dev, webdev, web dev, skola, zabava',
'generator' => 'TurboRoute',
'robots' => 'follow, index, max-snippet:-1, max-video-preview:-1, max-image-preview:large'
]
];
$routerConfig["template_dir"] = "templates/";
$routerConfig["endpoint_dir"] = "endpoints/";
$routerConfig["page_dir"] = "pages/";
$routerConfig["protocol"] = "https://";
$routerConfig["logged_out_permission_level"] = 1;
$routerConfig["logged_in_default_permission_level"] = 2;
$routerConfig["verified_permission_level"] = 3;
$routerConfig["trustworthy_permission_level"] = 4;
$routerConfig["moderator_permission_level"] = 5;
$routerConfig["user_admin_permission_level"] = 254;
$routerConfig["admin_permission_level"] = 255;
$routerConfig["default_page_permission_level"] = 255;
$routerConfig["default_page_secret"] = 1;
}

@ -1,20 +0,0 @@
<?php
/**
* Generates dynamic CSS styling based on user preferences stored in the session.
* Specifically, it creates a CSS rule for the user's favorite color if it's specified in their session data.
*
* @return string Returns a string containing a `<style>` tag with custom CSS if a favorite color is set
* and the user is logged in. Returns an empty string if no conditions are met.
*/
function doDynamicStyling() :string
{
$dynamic_style = "";
if(isLoggedIn() && !empty($_SESSION["favorite_color"]) && is_int($_SESSION["favorite_color"]) && $_SESSION["favorite_color"] <= 4294967295){
$dynamic_style = "<style>";
$color = dechex($_SESSION["favorite_color"]);
$dynamic_style .= "--root{ --favorite-color: #$color;";
$dynamic_style .= "</style>";
}
return $dynamic_style;
}

@ -1,16 +1,6 @@
<?php
/**
* Executes an endpoint script and returns the results.
*
* This function includes an endpoint PHP file that defines a function named `endpoint` which
* is expected to accept an array parameter and return an array result.
* It simply scopes an external file into a function to prevent variable conflicts.
*
* @param string $endpoint_file The path to the endpoint PHP file.
* @return array|null Returns the result of the endpoint function if successful, or null if the
* endpoint function or file does not behave as expected.
*/
function runEndpoint(string $endpoint_file): ?array
function runEndpoint($endpoint_file): ?array
{
$endpoint_data = $_POST;
@ -19,20 +9,8 @@ function runEndpoint(string $endpoint_file): ?array
return endpoint($endpoint_data);
}
/**
* Retrieves and processes the output of a specified endpoint.
*
* This function determines the appropriate endpoint PHP file based on the provided endpoint name,
* executes the endpoint, and returns its results as a JSON-encoded string. It handles and
* translates different return types into a JSON format and manages HTTP response codes based on
* success or failure of the endpoint execution.
*
* @param string $endpoint_name The name of the endpoint, which is used to construct the file path to the endpoint script.
* @return string A JSON-encoded string representing the result of the endpoint, including a status indicator and any relevant data or error messages.
*@global array $routerRequest Current request data that might influence the endpoint processing.
* @global array $routerConfig Global configuration that contains paths and settings.
*/
function getEndpoint(string $endpoint_name): string
function getEndpoint($endpoint_name): string
{
$output = array();
$output["Status"] = "Fail";
@ -40,28 +18,28 @@ function getEndpoint(string $endpoint_name): string
global $routerRequest;
if(!$endpoint_name){
$endpoint_name = $routerRequest["site_name"];
$endpoint_name = $routerRequest["page_name"];
}
$endpoint_file = $routerConfig["endpoint_dir"] . $endpoint_name . ".php";
if (file_exists($endpoint_file)){
$output_tmp = runEndpoint($endpoint_file);
$output["Endpoint"] = $endpoint_name;
$type = gettype($output_tmp);
switch ($type) {
case 'array':
$output = $output_tmp;
break;
default:
$output['Status'] = 'Fail';
$output["Error"] = "Endpoint error";
$output["Type"] = $type;
http_response_code(500);
}
if($routerRequest["isToApex"]){
$subdomain_part = "";
}
else{
$output["Error"] = "Not found";
$subdomain_part = $routerRequest["subdomain"] . "/";
}
$endpoint_file = $routerConfig["endpoint_dir"] . $subdomain_part . $endpoint_name . ".php";
$endpoint_file_global = $routerConfig["endpoint_dir"] . "global/" . $endpoint_name . ".php";
if (file_exists($endpoint_file_global)){
$output = runEndpoint($endpoint_file_global);
}
elseif (file_exists($endpoint_file)){
$output = runEndpoint($endpoint_file);
}
else{
$output["error"] = "Not found";
http_response_code(404);
}

@ -1,17 +1,9 @@
<?php
/**
* Processes an HTML string to inline all linked stylesheets by replacing <link> tags
* with corresponding <style> tags containing the CSS content.
* Might be broken, currently disabled in the config
*
* @param string $inputString The HTML content containing <link> tags to stylesheets.
* @return string The modified HTML content with stylesheets inlined within <style> tags.
*/
function inlineLocalStylesFromHref(string $inputString): string
{
function inlineLocalStylesFromHref($inputString) {
$pattern = '/<link[^>]*?\srel=["\']?stylesheet["\'].*?\shref=["\']?\/(.*?)["\'][^>]*?>/i';
return preg_replace_callback($pattern, function($match) {
$outputString = preg_replace_callback($pattern, function($match) {
$href = $match[1];
$cssFilePath = $_SERVER['DOCUMENT_ROOT'] . '/' . $href;
$cssContent = file_get_contents($cssFilePath);
@ -34,39 +26,28 @@ function inlineLocalStylesFromHref(string $inputString): string
// Minify the CSS content
$cssContent = minifyCss($cssContent);
return "<style>$cssContent</style>";
return "<style>{$cssContent}</style>";
}, $inputString);
return $outputString;
}
/**
* Processes an HTML string to inline all external JavaScript files by replacing <script src="..."> tags
* with <script> tags containing the JavaScript content.
* Might be broken, currently disabled in the config
*
* @param string $inputString The HTML content containing <script src=""> tags.
* @return string The modified HTML content with external scripts inlined within <script> tags.
*/
function inlineScriptFromSrc(string $inputString): string
{
function inlineScriptFromSrc($inputString) {
$pattern = '/<script.*?src=["\']\/(.*?)["\'].*?>\s*<\/script>/i';
return preg_replace_callback($pattern, function($match) {
$outputString = preg_replace_callback($pattern, function($match) {
$src = $match[1];
$jsContent = file_get_contents($_SERVER['DOCUMENT_ROOT'] . '/' . $src);
// Minify the JavaScript content
$jsContent = minifyJs($jsContent);
return "<script>$jsContent</script>";
return "<script>{$jsContent}</script>";
}, $inputString);
return $outputString;
}
/**
* Minifies CSS content by removing comments, unnecessary whitespaces, semicolons, and optimizing other aspects of the stylesheet.
* Might be broken, currently disabled in the config
*
* @param string $css The original CSS content.
* @return string The minified CSS content.
*/
function minifyCss(string $css): string
{
function minifyCss($css) {
// Remove comments
$css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
@ -84,15 +65,8 @@ function minifyCss(string $css): string
return trim($css);
}
/**
* Minifies JavaScript content by removing comments, unnecessary whitespaces, and optimizing spaces around operators.
* Might be broken, currently disabled in the config
*
* @param string $js The original JavaScript content.
* @return string The minified JavaScript content.
*/
function minifyJs(string $js): string
{
function minifyJs($js) {
// Remove newlines and tabs
$js = str_replace("\t", '', $js);

@ -1,316 +0,0 @@
<?php
require_once "lib/upload.php";
require_once "lib/account.php";
/**
* Adds a meme to the database with associated image and text content.
*
* @param string $title The title of the meme.
* @param string $memeText The text content of the meme.
* @param int $imageID The ID of the image associated with the meme.
* @return array Returns an associative array with the operation status and a message.
* @global mysqli $mysqli The database connection object.
*/
function addMeme(string $title, string $memeText, int $imageID): array
{
global $mysqli;
$output = ["Status" => "Fail"];
if (isLoggedIn() && fileExists($imageID, false) && !empty($title) && !empty($memeText) && !empty($imageID) && $imageID > 0) {
$stmtMemeAdd = $mysqli->prepare('INSERT INTO Memes (AuthorID, Title, TextContent, FileID) VALUES (?, ?, ?, ?)');
$stmtMemeAdd->bind_param('issi', $_SESSION['ID'], htmlspecialchars($title), htmlspecialchars($memeText), $imageID);
if ($stmtMemeAdd->execute() && $stmtMemeAdd->affected_rows > 0) {
$output["Status"] = "Success";
$output["Meme"] = "Funny";
}
}
return $output;
}
function executeAndRenderMemes(mysqli_stmt $stmt): string {
global $routerConfig;
$stmt->execute();
$stmt->store_result();
$stmt->bind_result($memeID, $title, $textContent, $createdAt, $authorID, $filePath, $imageWidth, $imageHeight, $userNickname);
$memes_out = '';
$meme_template = file_get_contents($routerConfig['template_dir'] . "meme.html");
$meme_gallery_template = file_get_contents($routerConfig['template_dir'] . 'meme_gallery.html');
while ($stmt->fetch()) {
$memes_out .= renderMeme($memeID, $authorID, $title, $textContent, $createdAt, $filePath, $imageWidth, $imageHeight, $userNickname, $meme_template);
}
$meme_add = isLoggedIn() ? file_get_contents($routerConfig['template_dir'] . 'meme_add.html') : '';
$meme_gallery_out = str_replace('__TEMPLATE_MEMES_HERE__', $memes_out, $meme_gallery_template);
$meme_gallery_out = str_replace('__TEMPLATE_MEME_ADD__', $meme_add, $meme_gallery_out);
$stmt->close();
return $meme_gallery_out;
}
/**
* Renders a meme into HTML based on provided data and a template.
*
* @param int $id The ID of the meme.
* @param int $authorId The author's user ID.
* @param string $title The title of the meme.
* @param string $textContent The text content of the meme.
* @param string $createdAt The creation timestamp of the meme.
* @param string $filePath The file path of the associated image.
* @param int $imageWidth The width of the image.
* @param int $imageHeight The height of the image.
* @param string $userNickname The nickname of the meme's author.
* @param string $meme_template The HTML template for a meme. (used to not read the template over and over when rendering more memes)
* @return string Returns the rendered HTML of the meme.
*/
function renderMeme(int $id, int $authorId, string $title, string $textContent, string $createdAt, string $filePath, int $imageWidth, int $imageHeight, string $userNickname, string $meme_template): string
{
$meme_out = str_replace('__TEMPLATE_MEME_TITLE__', htmlspecialchars($title), $meme_template);
$meme_out = str_replace('__TEMPLATE_MEME_AUTHOR__', htmlspecialchars($userNickname), $meme_out);
$meme_out = str_replace('__TEMPLATE_MEME_DATE__', htmlspecialchars($createdAt), $meme_out);
$meme_out = str_replace('__TEMPLATE_MEME_IMAGE__', '/' . htmlspecialchars($filePath), $meme_out);
$meme_out = str_replace('__TEMPLATE_MEME_IMAGE_WIDTH__', strval($imageWidth), $meme_out);
$meme_out = str_replace('__TEMPLATE_MEME_IMAGE_HEIGHT__', strval($imageHeight), $meme_out);
$meme_out = str_replace('__TEMPLATE_MEME_DELETE_BUTTON__', (isModerator() || $_SESSION['ID'] == $authorId) ? "<button onclick=\"deleteMeme($id);\"><i class='ri-delete-bin-line'></i></button>" : '', $meme_out);
$meme_votes = calculateNetVotes($id);
$meme_net_votes = $meme_votes['NetVotes'];
if ($meme_votes['UserVote'] > 0) {
$meme_upvote_active = 'fill';
$meme_downvote_active = 'line';
$meme_vote_counter_class = 'positive';
$meme_upvote_button_class = ' visual_hover';
$meme_downvote_button_class = '';
} elseif (($meme_votes['UserVote'] < 0)) {
$meme_upvote_active = 'line';
$meme_downvote_active = 'fill';
$meme_vote_counter_class = 'negative';
$meme_upvote_button_class = '';
$meme_downvote_button_class = ' visual_hover';
} else {
$meme_downvote_active = 'line';
$meme_upvote_active = 'line';
$meme_vote_counter_class = 'neutral';
$meme_upvote_button_class = '';
$meme_downvote_button_class = '';
}
$meme_upvote = isLoggedIn() ? "<button id='meme_votes_upvote_button_$id' class='meme_upvote$meme_upvote_button_class' onclick=\"voteMeme($id, 1);\"> <i id='meme_votes_upvote_$id' class=\"ri-arrow-up-circle-$meme_upvote_active\"></i></button>" : '';
$meme_downvote = isLoggedIn() ? "<button id='meme_votes_downvote_button_$id' class='meme_downvote$meme_downvote_button_class' onclick=\"voteMeme($id, 0);\"> <i id='meme_votes_downvote_$id' class=\"ri-arrow-down-circle-$meme_downvote_active\"></i></button>" : '';
$meme_out = str_replace('__TEMPLATE_MEME_VOTES_NUMBER__', strval($meme_net_votes), $meme_out);
$meme_out = str_replace('__TEMPLATE_MEME_UPVOTE__', $meme_upvote, $meme_out);
$meme_out = str_replace('__TEMPLATE_MEME_DOWNVOTE__', $meme_downvote, $meme_out);
$meme_out = str_replace('__TEMPLATE_MEME_ID__', strval($id), $meme_out);
$meme_out = str_replace('__TEMPLATE_MEME_VOTE_COUNTER_CLASS__', $meme_vote_counter_class, $meme_out);
return str_replace('__TEMPLATE_MEME_TEXT__', htmlspecialchars($textContent), $meme_out);
}
/**
* Renders a gallery of memes, optionally filtered by author ID, meme ID, or a keyword.
*
* This function retrieves memes from the database and returns an HTML string representation.
* It supports filtering by author ID, meme ID, or a keyword that is searched in titles and text content.
* It also supports pagination through an offset parameter.
*
* @param int|null $offset Pagination offset, used to calculate the starting point for records to return.
* @param int|null $authorId Optional author ID for filtering memes by a specific author.
* @param int|null $memeId Optional meme ID for rendering a single meme.
* @param string|null $keyword Optional keyword for full-text search in meme titles and content.
* @return string Returns the complete HTML content of the meme gallery, optionally filtered.
*/
function getMemeGallery(?int $offset = null, ?int $authorId = null, ?int $memeId = null, ?string $keyword = null): array {
return [
"Status" => "Success",
"Output" => renderMemeGallery($offset, $authorId, $memeId, $keyword)
];
}
function renderMemeGallery(?int $offset = null, ?int $authorId = null, ?int $memeId = null, ?string $keyword = null): string {
global $mysqli, $routerConfig;
// Start building the SQL query
$query = 'SELECT Memes.ID, Memes.Title, Memes.TextContent, Memes.CreatedAt, Memes.AuthorID,
Files.Path, Files.Width, Files.Height, Users.Nickname
FROM Memes
INNER JOIN Users ON Memes.AuthorID = Users.ID
INNER JOIN Files ON Memes.FileID = Files.ID';
$conditions = [];
$params = [];
$types = '';
// Add conditions based on provided parameters
if ($authorId !== null) {
$conditions[] = 'Memes.AuthorID = ?';
$params[] = $authorId;
$types .= 'i';
}
if ($memeId !== null) {
$conditions[] = 'Memes.ID = ?';
$params[] = $memeId;
$types .= 'i';
}
if ($keyword !== null) {
$conditions[] = '(Memes.Title LIKE CONCAT("%", ?, "%") OR Memes.TextContent LIKE CONCAT("%", ?, "%"))';
$params[] = $keyword;
$params[] = $keyword;
$types .= 'ss';
}
// Append conditions to the query
if (!empty($conditions)) {
$query .= ' WHERE ' . join(' AND ', $conditions);
}
if($offset == null) {
$offset = 0;
}
// Add pagination and limit
$query .= ' LIMIT ? OFFSET ?';
$params[] = $routerConfig['meme']['per_page'];
$params[] = $routerConfig['meme']['per_page'] * $offset;
$types .= 'ii';
$stmt = $mysqli->prepare($query);
$stmt->bind_param($types, ...$params);
return executeAndRenderMemes($stmt);
}
/**
* Deletes a meme from the database if the current user has the right permissions.
*
* @param int $memeID The ID of the meme to delete.
* @return array Returns an associative array with the status of the operation.
* @global mysqli $mysqli The database connection object.
*/
function deleteMeme(int $memeID): array
{
global $mysqli;
$out = ["Status" => "Fail"];
if (isLoggedIn()) {
$query = !isModerator() ? 'DELETE FROM Memes WHERE ID = ? AND AuthorID = ?' : 'DELETE FROM Memes WHERE ID = ?';
$stmtDelete = $mysqli->prepare($query);
if (!isModerator()) {
$stmtDelete->bind_param('ii', $memeID, $_SESSION['ID']);
} else {
$stmtDelete->bind_param('i', $memeID);
}
$stmtDelete->execute();
if ($stmtDelete->affected_rows > 0) {
$out['Status'] = 'Success';
}
$stmtDelete->close();
}
return $out;
}
/**
* Records or updates a vote on a meme by the current user.
*
* @param int $memeID The ID of the meme to be voted on.
* @param int $isUpvote Indicates whether the vote is an upvote (1) or downvote (0).
* @return array Returns an associative array with the status of the vote operation.
* @global mysqli $mysqli The database connection object.
*/
function voteMeme(int $memeID, int $isUpvote): array
{
global $mysqli;
$out = ["Status" => "Fail"];
if ($isUpvote != 1) {
$isUpvote = 0;
}
$memeVoteConn = $mysqli->prepare('INSERT INTO MemeVotes (MemeID, UserID, isUpvote) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE isUpvote = VALUES(isUpvote)');
$memeVoteConn->bind_param('iii', $memeID, $_SESSION['ID'], $isUpvote);
$memeVoteConn->execute();
if ($memeVoteConn->affected_rows > 0) {
$out['Status'] = 'Success';
}
$memeVoteConn->close();
return $out;
}
/**
* Deletes a vote previously made by the current user on a meme.
*
* @param int $memeID The ID of the meme whose vote is to be deleted.
* @return array Returns an associative array with the status of the deletion.
* @global mysqli $mysqli The database connection object.
*/
function deleteVoteMeme(int $memeID): array
{
global $mysqli;
$out = ["Status" => "Fail"];
$memeVoteConn = $mysqli->prepare('DELETE FROM MemeVotes WHERE MemeID = ? AND UserID = ?');
$memeVoteConn->bind_param('ii', $memeID, $_SESSION['ID']);
$memeVoteConn->execute();
if ($memeVoteConn->affected_rows > 0) {
$out['Status'] = 'Success';
}
$memeVoteConn->close();
return $out;
}
/**
* Calculates the net votes for a meme and determines if the current user has voted on it.
* The array has both the net votes and the user vote(0 when the user hasn't voted)
*
* @param int $memeID The ID of the meme for which votes are being calculated.
* @return array Returns an array with net votes and the user's vote status.
* @global mysqli $mysqli The database connection object.
*/
function calculateNetVotes(int $memeID): array
{
global $mysqli;
// Adjusted query to calculate net votes and get the user's vote in one go
$query = "
SELECT
SUM(CASE WHEN isUpvote = 1 THEN 1 ELSE -1 END) AS NetVotes,
(
SELECT CASE WHEN isUpvote = 1 THEN 1 ELSE -1 END
FROM MemeVotes
WHERE MemeID = ? AND UserID = ?
) AS UserVote
FROM MemeVotes
WHERE MemeID = ?";
$stmt = $mysqli->prepare($query);
$userID = $_SESSION['ID'];
$stmt->bind_param('iii', $memeID, $userID, $memeID);
$stmt->execute();
$result = $stmt->get_result();
$data = $result->fetch_assoc();
$netVotes = $data['NetVotes'] ?? 0; // Null coalescing operator in case no votes are found
$userVote = $data['UserVote'] ?? 0; // Default to 0 if the user hasn't voted
$stmt->close();
return [
"NetVotes" => $netVotes,
"UserVote" => $userVote
];
}
/**
* Fetches the net votes and user's vote status for a specific meme.
* Essentially just a wrapper of getMemeVotes for an API call
*
* @param int $memeID The ID of the meme to fetch votes for.
* @return array Returns an array with the net votes and the user's vote status, along with operation status.
*/
function getMemeVotes(int $memeID): array
{
$voteData = calculateNetVotes($memeID);
return [
"Status" => "Success",
"NetVotes" => $voteData['NetVotes'],
"UserVote" => $voteData['UserVote']
];
}

@ -1,53 +1,27 @@
<?php
/**
* Includes a PHP file that returns metadata associated with a dynamic page.
* It simply scopes an external file into a function to prevent variable conflicts.
*
* @param string $file The file path to the PHP file that contains metadata.
* @return array Returns an associative array of metadata from the included PHP file.
*/
function getDynamicMetadata(string $file): array{
return include($file);
}
/**
* Extracts and validates the minimal permission level required to access certain content,
* defaulting to system configuration if not properly set or in case of an error.
*
* @param array $metadata Metadata array that should include a 'parameters' key with 'minimal_permission_level'.
* @return int Returns the minimal permission level required to access a page.
*@global array $routerConfig Global router configuration settings.
*/
function getDynamicPermission(array $metadata): int {
function getDynamicPermission($file): int {
global $routerConfig;
$params = $metadata["parameters"];
try {
$permission_level = $params["minimal_permission_level"];
$page_tmp = include($file);
$permission_level = $page_tmp["parameters"]["minimal_permission_level"];
if (!is_numeric($permission_level) || $permission_level <= 0) {
$permission_level = $routerConfig["page"]["default_permissions"];
$permission_level = $routerConfig["default_page_permission_level"];
}
}
catch (Exception){
$permission_level = $routerConfig["page"]["default_permissions"];
$permission_level = $routerConfig["default_page_permission_level"];
} finally {
return $permission_level;
}
}
/**
* Generates HTML navigation links for all sites and pages configured in the router,
* adjusting active states based on current request and filtering links by user permissions.
*
* @global array $routerConfig Global configuration that includes directory paths and default settings.
* @global array $routerRequest Current request details including site and page name.
* @return string Returns the HTML string of the navigation menu.
*/
function generateNavigation(): string
{
global $routerConfig;
global $routerRequest;
$nav = file_get_contents($routerConfig["template_dir"] . "nav.html");
$site_dirs = array_diff(scandir($routerConfig["page_dir"]), array('.', '..'));
$nav_out = "";
@ -57,11 +31,16 @@ function generateNavigation(): string
$site_name = str_replace("_", " ", $site_dir);
$site_subdomain = $site_dir . ".";
if ($site_name == $routerConfig["default_site"]) {
$site_subdomain = "";
}
$site_name = ucfirst($site_name);
$site_location = "/" . $site_dir . "/" . $routerConfig["default_page"];
$site_location = $routerConfig["protocol"] . $site_subdomain . $routerRequest["domain"] . "." . $routerRequest["tld"] . "/" . $routerConfig["default_page"];
if ($routerRequest["site_name"] == $site_dir) {
if ($routerRequest["subdomain"] == $site_dir) {
//this is the current page
$site_class = "class=\"navsite_link active\"";
}
@ -69,22 +48,21 @@ function generateNavigation(): string
$site_class = "class=\"navsite_link\"";
}
$navigation_pages = "";
$navpages = "";
foreach ($pages_dir as $page_file) {
$page_file_tmp = explode(".", $page_file);
$page_basename = $page_file_tmp[0];
$page_class = "class=\"navpage_link\"";
if ($routerRequest["site_name"] == $site_dir && $routerRequest["page_name"] == $page_basename) {
if ($routerRequest["subdomain"] == $site_dir && $routerRequest["page_name"] == $page_basename) {
$page_class = "class=\"navpage_link active\"";
}
$page_location = "/" . $site_dir . "/" . $page_basename;
$page_location = $routerConfig["protocol"] . $site_subdomain . $routerRequest["domain"] . "." . $routerRequest["tld"] . "/" . $page_basename;
$page_name = str_replace("_", " ", $page_basename);
$page_name = explode(".", $page_name)[0];
$page_name = ucfirst($page_name);
$page_file_path = $routerConfig["page_dir"] . $site_dir . "/" . $page_file ;
if($page_file_tmp[1] == "html"){
$page_tmp = file_get_contents($page_file_path);
@ -94,48 +72,26 @@ function generateNavigation(): string
$page_required_permission = intval($pageMetadata["parameters"]["minimal_permission_level"]);
}
else{
$page_required_permission = $routerConfig["page"]["default_permissions"];
}
if(!empty($pageMetadata["parameters"]["page_title"])){
$page_name = $pageMetadata["parameters"]["page_title"];
$page_required_permission = $routerConfig["default_page_permission_level"];
}
}
elseif($page_file_tmp[1] == "php"){
$pageMetadata = getDynamicMetadata($page_file_path);
$page_required_permission = getDynamicPermission($pageMetadata);
if(!empty($pageMetadata["parameters"]["page_title"])){
$page_name = $pageMetadata["parameters"]["page_title"];
}
$page_required_permission = getDynamicPermission($page_file_path);
}
else{
$page_required_permission = $routerConfig["page"]["default_permissions"];
$page_required_permission = $routerConfig["default_page_permission_level"];
}
if($page_required_permission <= $_SESSION["privilege_level"]) {
$navpage_attributes = "data-site='$site_dir' data-page='$page_basename'";
$navigation_pages .= "<li class='navpage_item' $navpage_attributes ><a $navpage_attributes href='$page_location' $page_class>$page_name</a></li>";
$navpages .= "<li class='navpage_item' data-page='$page_basename'><a href='$page_location' $page_class>$page_name</a></li>";
}
}
if(!empty($navigation_pages)){
$default_page = $routerConfig["default_page"];
$navsite_attributes = "data-page='$default_page' data-site='$site_dir'";
$nav_out .= "<li class='navsite_item' ><a $navsite_attributes href='$site_location' $site_class>$site_name</a><ul class='navpage_list'>$navigation_pages</ul></li>";
if(!empty($navpages)){
$nav_out .= "<li class='navsite_item' data-site='$site_dir'><a href='$site_location' $site_class>$site_name</a><ul class='navpage_list'>$navpages</ul></li>";
}
}
return str_replace("__NAV_PAGES__", $nav_out, $nav);
return $nav_out;
}
/**
* Provides a simple API endpoint-like response for fetching generated navigation HTML.
* Wraps generateNavigation for an API.
*
* @return array Returns an associative array with the navigation HTML and a status indicating success.
*/
function getNavigationEndpoint() :array{
return [
"Status" => "Success",
"Navigation" => generateNavigation(),
];
}

@ -1,130 +0,0 @@
<?php
require_once "lib/account.php";
/**
* Retrieves news articles based on the current user's privilege level.
* The function queries the NewsArticles and Users tables to fetch articles
* that the user has the privilege to view. Articles are joined with user
* information to include the author's nickname.
*
* @global mysqli $mysqli The mysqli database connection object.
* @return array Returns an associative array with a status key indicating the success or failure,
* and an 'Articles' key containing an array of articles if successful.
*/
function getNewsArticles() :array
{
global $mysqli;
$output = ["Status" => "Fail"]; // Default Status is "Fail"
$articles = [];
$stmt = $mysqli->prepare("SELECT NewsArticles.ID, NewsArticles.WrittenAt, NewsArticles.WrittenBy, NewsArticles.Title, NewsArticles.Body, NewsArticles.FileList, Users.Nickname FROM NewsArticles INNER JOIN Users ON NewsArticles.WrittenBy = Users.ID WHERE NewsArticles.PrivilegeLevel <= ?;");
$id = 0;
$writtenAt = "";
$writtenBy = 0;
$title = "";
$body = "";
$filelist = 0;
$writtenByName = "";
$stmt->bind_param("i", $_SESSION["privilege_level"]);
$stmt->bind_result($id, $writtenAt, $writtenBy, $title, $body, $filelist, $writtenByName);
$stmt->execute();
while ($stmt->fetch()) {
$articles[] = [
'ID' => $id,
'WrittenAt' => $writtenAt,
'Title' => $title,
'Body' => $body,
'WrittenByName' =>$writtenByName
];
}
// Check if any results were fetched
if (!empty($articles)) {
$output["Status"] = "Success";
$output["Articles"] = $articles;
}
return $output;
}
/**
* Adds a new news article to the database if the user is logged in and has the appropriate
* privilege level. The function sanitizes the title and body of the article to prevent XSS attacks.
*
* @global mysqli $mysqli The mysqli database connection object.
* @global array $routerConfig Configuration array that includes default permission settings.
* @param string $title The title of the news article. Default value is "Nazov".
* @param string $body The body of the news article. Default value is "Obsah".
* @param int $privilegeLevel The privilege level required to view the article. If set to 0, uses default from configuration.
* @return array Returns an associative array with a status key that indicates the success or failure of the operation.
*/
function addNewsArticle(string $title="Nazov", string $body="Obsah", int $privilegeLevel=0) :array
{
global $mysqli;
global $routerConfig;
if ($privilegeLevel == 0){
$privilegeLevel = $routerConfig['newsarticle']['default_permissions'];
}
$output = ["Status" => "Fail"]; // Default Status is "Fail"
if (isLoggedIn() && $privilegeLevel <= $_SESSION["privilege_level"]) {
$query = $mysqli->prepare("INSERT INTO NewsArticles (WrittenBy, Title, Body, FileList, PrivilegeLevel) VALUES (?, ?, ?, 0, ?);");
$minpriv = intval($privilegeLevel);
$query->bind_param("issi", $_SESSION["ID"], htmlspecialchars($title), htmlspecialchars($body), $minpriv);
$query->execute();
if ($query->affected_rows > 0) {
$output["Status"] = "Success";
}
$query->close();
}
return $output;
}
/**
* Adds a comment to a news article.
*
* @param int $userId User who is commenting.
* @param int $newsArticleId ID of the news article.
* @param string $commentText The content of the comment.
* @param int|null $parentId ID of the parent comment if it's a reply.
* @return array Status array indicating success or failure.
* @global mysqli $mysqli The mysqli database connection object.
*/
function addNewsComment(int $userId, int $newsArticleId, string $commentText, ?int $parentId = null): array {
global $mysqli;
$output = ["Status" => "Fail"]; // Default Status is "Fail"
if (!isLoggedIn()) {
$output['Error'] = "User must be logged in.";
return $output;
}
// Prepare the SQL statement to prevent SQL injection
$stmt = $mysqli->prepare("INSERT INTO NewsComments (ParentID, UserID, NewsArticleID, CommentText) VALUES (?, ?, ?, ?);");
// Bind parameters. 'i' denotes an integer and 's' denotes a string.
$stmt->bind_param("iiis", $parentId, $userId, $newsArticleId, $commentText);
// Execute the query
if ($stmt->execute()) {
// Check if any rows were affected
if ($stmt->affected_rows > 0) {
$output["Status"] = "Success";
} else {
$output["Error"] = "No rows affected.";
}
} else {
$output["Error"] = $stmt->error;
}
// Close statement
$stmt->close();
return $output;
}

@ -1,37 +1,11 @@
<?php
require_once "lib/dynamic_style.php";
require_once "lib/script_data.php";
/**
* Loads and returns the result of a PHP file.
* This function is typically used to process dynamic content of a page.
* It simply scopes an external file into a function to prevent variable conflicts.
*
* @param string $page_file The file path to the dynamic page.
* @return array Returns the array of data generated by including the PHP file.
*/
function renderDynamicPage(string $page_file): array
require_once "lib/inliner.php";
function renderDynamicPage($page_file): array
{
return require $page_file;
}
/**
* Removes all HTML comments from the provided content string.
*
* @param string $content The HTML content from which to remove comments.
* @return string The content without any HTML comments.
*/
function removeHtmlComments(string $content = '') :string {
return preg_replace('/<!--(.|\s)*?-->/', '', $content);
}
/**
* Parses custom `<page>` tags from the given input string and extracts parameters.
* Returns the input string with `<page>` tags removed and a list of parameters.
*
* @param string $input The input HTML or text containing `<page>` tags.
* @return array Returns an associative array with 'parameters' (parsed from the tag)
* and 'output' (the modified input string with `<page>` tags removed).
*/
function parsePageTag(string $input): array
function parsePageTag($input): array
{
// Define the pattern for the tag
$pattern = '/<page\s+([^>]+)><\/page>/i';
@ -55,33 +29,21 @@ function parsePageTag(string $input): array
// If no match is found, return the original input
return ['parameters' => [], 'output' => $input];
}
/**
* Renders a page based on specified page and site names, handling dynamic and static content,
* permissions, and error pages.
*
* @param string|null $page_name The name of the page to render. If null, uses default from request.
* @param string|null $site_name The name of the site to render. If null, uses default from request.
* @return array Returns an associative array containing the rendered page content, page name, site name, and page title.
*/
function renderPage(string $page_name = null, string $site_name = null): array
function getPage($page_name = null): array|false|string
{
global $routerConfig;
global $routerRequest;
if(!$site_name) {
$site_name = $routerRequest["site_name"];
}
$site_title = str_replace("_", " ", $site_name);
$site_title = ucfirst($site_title);
if(!$page_name){
$page_name = $routerRequest["page_name"];
}
$dynamic_page_file = $routerConfig["page_dir"] . $site_name . "/" . $page_name . ".php";
$page_file = $routerConfig["page_dir"] . $site_name . "/" . $page_name . ".html";
$dynamic_page_file = $routerConfig["page_dir"] . $routerRequest["subdomain"] . "/" . $page_name . ".php";
$page_file = $routerConfig["page_dir"] . $routerRequest["subdomain"] . "/" . $page_name . ".html";
$skeleton = file_get_contents($routerConfig["template_dir"] . "skeleton.html");
$nav = file_get_contents($routerConfig["template_dir"] . "nav.html");
if (file_exists($dynamic_page_file)){
$pageMetadata = renderDynamicPage($dynamic_page_file);
@ -105,7 +67,7 @@ function renderPage(string $page_name = null, string $site_name = null): array
$page_required_permission = intval($pageMetadata["parameters"]["minimal_permission_level"]);
}
else{
$page_required_permission = $routerConfig["page"]["default_permissions"];
$page_required_permission = $routerConfig["default_page_permission_level"];
}
if(!empty($pageMetadata["parameters"]["secret"])){
@ -117,11 +79,11 @@ function renderPage(string $page_name = null, string $site_name = null): array
$is_secret_page = 0;
}
else{
$is_secret_page = $routerConfig["page"]["default_secret"];
$is_secret_page = $routerConfig["default_page_secret"];
}
}
else{
$is_secret_page = $routerConfig["page"]["default_secret"];
$is_secret_page = $routerConfig["default_page_secret"];
}
@ -140,7 +102,6 @@ function renderPage(string $page_name = null, string $site_name = null): array
}
}
$page = str_replace("__DEFAULT_LINK__", "/" . $routerConfig["default_site"] . "/" . $routerConfig["default_page"], $page);
if(!is_string($page)){
$page = "";
}
@ -152,129 +113,22 @@ function renderPage(string $page_name = null, string $site_name = null): array
$page_title = $page_name;
}
$page_title = $routerConfig['site_prefix'] . " " . $site_title . " " . $page_title;
$page = str_replace("__TEMPLATE_PAGE_TITLE__", $page_title, $page);
$page = removeHtmlComments($page);
return [
"PageContent" => $page,
"PageName" => $page_name,
"SiteName" => $site_name,
"PageTitle" => $page_title,
];
}
/**
* Compiles a complete web page by injecting dynamic elements into a template skeleton,
* including headers, footers, and SEO tags.
* It is used when not going to a page by AJAX to initialize everything.
*
* @param string|null $site_name_in The site name to be used; defaults from global configuration if null.
* @param string|null $page_name_in The page name to be used; defaults from global configuration if null.
* @return string The complete HTML content of the web page ready for display.
*/
function getPage(string $site_name_in = null, string $page_name_in = null): string
{
$page_tmp = renderPage($page_name_in, $site_name_in);
$page = $page_tmp["PageContent"];
$page_name = $page_tmp["PageName"];
$site_name = $page_tmp["SiteName"];
global $routerConfig;
$skeleton = file_get_contents($routerConfig["template_dir"] . "skeleton.html");
$footer = file_get_contents($routerConfig["template_dir"] . "footer.html");
$page_title = $page_tmp["PageTitle"];
$dynamic_style = doDynamicStyling();
$dynamic_script_data = [
"currentPage" => $page_name,
"currentSite" => $site_name,
"currentTitle" => $page_title,
"defaultPage" => $routerConfig["default_page"],
"defaultSite" => $routerConfig["default_site"],
"UserInfo_Privileges" => $_SESSION["privilege_level"],
];
if(isLoggedIn()){
$dynamic_script_data += [
"UserInfo_FirstName" => $_SESSION["first_name"],
"UserInfo_LastName" => $_SESSION["last_name"],
"UserInfo_Nickname" => $_SESSION["nickname"],
"UserInfo_Email" => $_SESSION["email"],
"UserInfo_MinecraftNick" => $_SESSION["minecraft_nickname"],
];
$dynamic_style = "<style>";
if(isLoggedIn() && !empty($_SESSION["favorite_color"]) && is_int($_SESSION["favorite_color"]) && $_SESSION["favorite_color"] <= 4294967295){
$color = dechex($_SESSION["favorite_color"]);
$dynamic_style .= "--root{ --favorite-color: #$color;";
}
$dynamic_script = generateScriptData($dynamic_script_data);
$dynamic_style .= "</style>";
$navigation = generateNavigation();
$navpages = generateNavigation();
$seo = array();
if(!empty($pageMetadata["parameters"]["author"])){
$seo["author"] = htmlspecialchars($pageMetadata["parameters"]["author"]);
}
else{
$seo["author"] = $routerConfig["seo"]["author"];
}
if(!empty($pageMetadata["parameters"]["description"])){
$seo["description"] = htmlspecialchars($pageMetadata["parameters"]["description"]);
}
else{
$seo["description"] = $routerConfig["seo"]["description"];
}
if(!empty($pageMetadata["parameters"]["keywords"])){
$seo["keywords"] = $routerConfig["seo"]["keywords"] . ", " . htmlspecialchars($pageMetadata["parameters"]["keywords"]);
}
else{
$seo["keywords"] = $routerConfig["seo"]["keywords"];
}
$seo["generator"] = $routerConfig["seo"]["generator"];
$seo["robots"] = $routerConfig["seo"]["robots"];
$seo_stuff = "";
foreach ($seo as $key => $value){
$seo_stuff .= "<meta name='$key' content='$value'>\n";
}
$nav = str_replace("__NAV_PAGES__", $navpages, $nav);
$out = $skeleton;
$out = str_replace("__TEMPLATE__NAV__", $navigation, $out);
$out = str_replace("__TEMPLATE__NAV__", $nav, $out);
$out = str_replace("__TEMPLATE__PAGE__", $page, $out);
$out = str_replace("__TEMPLATE__FOOTER__", $footer, $out);
$out = str_replace("__TEMPLATE__DYNAMIC__SCRIPT__", $dynamic_script, $out);
$out = str_replace("__TEMPLATE__DYNAMIC__STYLE__", $dynamic_style, $out);
$out = str_replace("__TEMPLATE_SEO_STUFF__", $seo_stuff, $out);
if($routerConfig["inlining"]) {
require_once "lib/inliner.php";
$out = inlineLocalStylesFromHref($out);
$out = inlineScriptFromSrc($out);
}
$out = str_replace("__TEMPLATE__DYNASTYLE__", $dynamic_style, $out);
$out = inlineLocalStylesFromHref($out);
$out = inlineScriptFromSrc($out);
return str_replace("__TEMPLATE_PAGE_TITLE__", $page_title, $out);
}
/**
* Provides an API interface to get page details including content and meta-information for routing purposes.
* This is what enables the page to never refresh.
*
* @param string $page_name The name of the page.
* @param string $site_name The name of the site.
* @return array Returns an array with status, page content, location URL, and title for the requested page.
*/
function getPageEndpoint(string $page_name, string $site_name) :array
{
$page_location = "/" . $site_name . "/" . $page_name;
$page_tmp = renderPage($page_name, $site_name);
return [
"Status" => "Success",
"Page" => $page_tmp["PageContent"],
"PageLocation" => $page_location,
"PageTitle" => $page_tmp["PageTitle"],
];
}

@ -1,46 +1,68 @@
<?php
/**
* Initializes the routing system for a web application. This function processes the incoming
* URL and determines the site name and page name based on the configuration and the URL structure.
* It handles default configurations and supports different types of requests like API or page requests.
*
* @global array $routerConfig The global configuration array that includes default site and page settings.
* @return array Returns an associative array containing the routing information, including the site name,
* page name, request type, and parsed request address from the HTTP host.
*/
function initRouter(): array
function initRouter(): bool
{
global $routerRequest;
global $routerConfig;
$routerRequest = array();
$routerRequest["requestAddress"] = array_slice(explode('.', $_SERVER['HTTP_HOST']), -3, 3); //get the last 3 elements
$request_uri = explode("/", $_SERVER["QUERY_STRING"]);
$needsRedirect = false;
$request_uri = array_slice($request_uri, -3, 3);
$routerRequest["site_name"] = $routerConfig["default_site"];
$routerRequest["page_name"] = $routerConfig["default_page"];
if(count($request_uri) > 2){
$routerRequest["page_name"] = basename($request_uri[2]);
}
if(count($request_uri) > 1){
$routerRequest["site_name"] = basename($request_uri[1]);
if(count($routerRequest["requestAddress"]) < 3){
// Root domain accessed directly
$routerRequest["subdomain"] = $routerConfig["default_site"];
$routerRequest["domain"] = basename($routerRequest["requestAddress"][0]);
$routerRequest["tld"] = basename($routerRequest["requestAddress"][1]);
$routerRequest["isToApex"] = true;
}
else {
$routerRequest["subdomain"] = basename($routerRequest["requestAddress"][0]);
$routerRequest["domain"] = basename($routerRequest["requestAddress"][1]);
$routerRequest["tld"] = basename($routerRequest["requestAddress"][2]);
if($_SERVER["REQUEST_METHOD"] == "POST"){
$routerRequest["type"] = "api";
if($routerRequest["subdomain"] == $routerConfig["default_site"]){
$routerRequest["subdomain"] = "";
$needsRedirect = true;
}
}
if(empty($routerRequest["type"])) {
$routerRequest["type"] = "page";
$routerRequest["page_name"] = basename($_SERVER["QUERY_STRING"]);
if (empty($routerRequest["page_name"])) {
// Page name is empty
$needsRedirect = true;
$routerRequest["page_name"] = $routerConfig["default_page"];
}
return $routerRequest;
if ($needsRedirect) {
if(!empty($routerRequest["subdomain"])){
$sub_domain = $routerRequest["subdomain"] . ".";
}
else{
$sub_domain = "";
}
$redirectAddress = $routerConfig["protocol"] .
$sub_domain .
$routerRequest["domain"] . "." .
$routerRequest["tld"] . "/" .
$routerRequest["page_name"];
// Redirect with default page name
header("Location: $redirectAddress");
return false;
}
else{
if($_SERVER["REQUEST_METHOD"] == "POST"){
$routerRequest["type"] = "api";
}
if(empty($routerRequest["type"])){
$routerRequest["type"] = "page";
}
return true;
}
}

@ -1,28 +0,0 @@
<?php
/**
* Generates a JavaScript script tag containing commands to store PHP array key-value pairs in local storage.
* This function is designed to translate PHP associative array data into JavaScript local storage items.
* It ensures that the array is associative and single-level before proceeding with the JavaScript code generation.
* This is used when dumping session data into local storage for use by the client script.
*
* @param array $phpArray The associative array whose data will be converted into JavaScript local storage setItem calls.
* @return string Returns a script tag with JavaScript code. If the input is not a valid single-level associative array,
* it returns a script with an error logged to the console.
*/
function generateScriptData(array $phpArray):string {
// Check if the array is associative and single-level
if (is_array($phpArray) && count($phpArray) > 0 && count(array_filter(array_keys($phpArray), 'is_string')) === count($phpArray)) {
// Generate JavaScript code to save each array element to local storage
$out = "<script>";
foreach ($phpArray as $key => $value) {
$escapedKey = addslashes(strval($key)); // Escape special characters in the key
$escapedValue = addslashes(strval($value)); // Escape special characters in the value
$out .= "localStorage.setItem('$escapedKey', '$escapedValue');";
}
$out.= "</script>";
} else {
$out = "<script>console.error('Invalid PHP array. Must be single-level and associative.');</script>";
}
return $out;
}

@ -1,66 +0,0 @@
<?php
require_once "lib/account.php";
/**
* Generates an XML sitemap as a string for a website, considering only pages that the current session
* has sufficient privileges to access. It scans directories specified in the router configuration
* for .html and .php files, and constructs a sitemap entry for each accessible page based on their
* required permission levels. This function returns the sitemap as a string and
* sets the appropriate header for XML content.
*
* @global array $routerConfig The global configuration array containing directory paths and default settings.
* @return string The XML sitemap content, properly formatted in accordance with the sitemap protocol.
*/
function generateSitemap(): string{
global $routerConfig;
$site_dirs = array_diff(scandir($routerConfig["page_dir"]), array('.', '..'));
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http";
$domain = $_SERVER['HTTP_HOST'];
$subdomain = ""; // You may need to modify this based on your subdomain logic
$sitemap = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
$sitemap .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . PHP_EOL;
foreach ($site_dirs as $site_dir) {
$pages_dir = array_diff(scandir($routerConfig["page_dir"] . $site_dir), array('.', '..'));
foreach ($pages_dir as $page_file) {
$page_file_tmp = explode(".", $page_file);
$page_basename = $page_file_tmp[0];
$page_file_path = $routerConfig["page_dir"] . $site_dir . "/" . $page_file;
$page_location = $protocol . "://" . $subdomain . $domain . "/" . $site_dir . "/" . $page_basename;
if ($page_file_tmp[1] == "html") {
$page_tmp = file_get_contents($page_file_path);
$pageMetadata = parsePageTag($page_tmp);
if (!empty($pageMetadata["parameters"]["minimal_permission_level"])) {
$page_required_permission = intval($pageMetadata["parameters"]["minimal_permission_level"]);
} else {
$page_required_permission = $routerConfig["page"]["default_permissions"];
}
} elseif ($page_file_tmp[1] == "php") {
$pageMetadata = getDynamicMetadata($page_file_path);
$page_required_permission = getDynamicPermission($pageMetadata);
} else {
$page_required_permission = $routerConfig["page"]["default_permissions"];
}
// Check if the user is authorized to access the page
if ($page_required_permission <= $_SESSION["privilege_level"]) {
$sitemap .= '<url>' . PHP_EOL;
$sitemap .= '<loc>' . htmlspecialchars($page_location) . '</loc>' . PHP_EOL;
// You can add other optional tags like lastmod, changefreq, priority here if needed
$sitemap .= '</url>' . PHP_EOL;
}
}
}
$sitemap .= '</urlset>' . PHP_EOL;
header('Content-type: application/xml');
return $sitemap;
}

@ -1,21 +0,0 @@
<?php
require_once "lib/account.php";
function submitSurvey(int $satisfaction, int $functionality, int $content, string $comment): array
{
global $mysqli;
$output = ["Status" => "Fail", "Opinion" => "Ignored unsuccessfully!"];
if (isLoggedIn()
&& $satisfaction >= 1 && $satisfaction <= 5
&& $functionality >= 1 && $functionality <= 5
&& $content >= 1 && $content <= 5
&& !empty($comment)) {
$stmtMemeAdd = $mysqli->prepare('INSERT INTO Survey (AuthorID, Satisfaction, Functionality, Content, Comment) VALUES (?, ?, ?, ?, ?)');
$stmtMemeAdd->bind_param('iiiis', $_SESSION['ID'], $satisfaction, $functionality, $content, htmlspecialchars($comment));
if ($stmtMemeAdd->execute() && $stmtMemeAdd->affected_rows > 0) {
$output["Status"] = "Success";
$output["Opinion"] = "Ignored successfully!";
}
}
return $output;
}

@ -1,350 +0,0 @@
<?php
/**
* Sanitizes user input to be used as a part of a file path to prevent security vulnerabilities such as directory traversal.
*
* @param string $userInput The input string to be sanitized.
* @return string Returns a safe string where only alphanumeric characters, underscores, and hyphens are retained.
* Special characters and potential path traversal payloads are removed or replaced.
*/
function makePathSafe(string $userInput): string
{
// Keep only alphanumeric characters, underscores, and hyphens
$safeString = preg_replace('/[^\w\-]/', '', $userInput);
// Ensure no path traversal
$safeString = str_replace('..', '_', $safeString);
// Trim leading/trailing underscores
$safeString = trim($safeString, '_');
// Replace directory separator characters with underscores
$safeString = str_replace(['/', '\\'], '_', $safeString);
// Limit length for safety
return substr($safeString, 0, 255);
}
/**
* Automatically rotates an image based on its EXIF data to adjust its orientation.
*
* @param Imagick $imagick An Imagick object representing the image to be rotated.
* @return void
*/
function autoRotateImage(Imagick $imagick): void {
// Get the current orientation of the image
try {
$orientation = $imagick->getImageOrientation();
switch ($orientation) {
case Imagick::ORIENTATION_BOTTOMRIGHT: // upside down
$imagick->rotateimage("#000", 180); // rotate 180 degrees
break;
case Imagick::ORIENTATION_RIGHTTOP: // 90 degrees CW
$imagick->rotateimage("#000", 90); // rotate 90 degrees CW
break;
case Imagick::ORIENTATION_LEFTBOTTOM: // 90 degrees CCW
$imagick->rotateimage("#000", -90); // rotate 90 degrees CCW
break;
}
// Reset orientation to normal after the correction
$imagick->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
} catch (ImagickException) {
}
}
/**
* Processes the global $_FILES array to normalize the structure and filter out any files with errors.
*
* @return array Returns an array of files that are ready for further processing, structured uniformly.
*/
function getIncomingFiles(): array
{
$files = $_FILES;
$files2 = [];
foreach ($files as $infoArr) {
$filesByInput = [];
foreach ($infoArr as $key => $valueArr) {
if (is_array($valueArr)) { // file input "multiple"
foreach ($valueArr as $i => $value) {
$filesByInput[$i][$key] = $value;
}
} else { // -> string, normal file input
$filesByInput[] = $infoArr;
break;
}
}
$files2 = array_merge($files2, $filesByInput);
}
$files3 = [];
foreach ($files2 as $file) { // let's filter empty & errors
if (!$file['error']) $files3[] = $file;
}
return $files3;
}
/**
* Saves file metadata in the database.
* This creates the only record of the file existing.
*
* @param string $filePath The path where the file is stored.
* @param string $fileType The MIME type of the file.
* @param int $width The width of the image file.
* @param int $height The height of the image file.
* @return bool Returns true if the file metadata was successfully saved to the database, false otherwise.
*/
function saveUploadedFileInDatabase(string $filePath, string $fileType, int $width, int $height): bool
{
global $mysqli;
$stmt = $mysqli->prepare("INSERT INTO Files (Path, Type, UploadedBy, UploadedAt, Width, Height) VALUES (?, ?, ?, NOW(), ?, ?)");
$stmt->bind_param("ssiii", $filePath, $fileType, $_SESSION["ID"], $width, $height);
$stmt->execute();
$stat = $stmt->affected_rows > 0;
$stmt->close();
return $stat;
}
/**
* Handles the uploading process of an image, including its conversion to webp format, rotation based on orientation, and saving.
*
* @param string $inFile The temporary file path of the uploaded file.
* @param string $outFile The target file path where the processed image should be saved.
* @return bool Returns true if the file was successfully processed and saved, false otherwise.
*/
function doImageUpload(string $inFile, string $outFile): bool
{
// Create Imagick object
$width = 0;
$height = 0;
try {
$imagick = new Imagick($inFile);
$imagick->setImageFormat('webp');
autoRotateImage($imagick);
$imagick->stripImage();
$imagick->writeImage($outFile);
$width = $imagick->getImageWidth();
$height = $imagick->getImageHeight();
$imagick->destroy();
} catch (ImagickException) {
}
// Check if the reencoding was successful, if yes, save into the database.
if (file_exists($outFile)) {
return saveUploadedFileInDatabase($outFile, 'image/webp', $width, $height);
} else {
return false;
}
}
/**
* Retrieves a list of files from the database, optionally filtered to include only files uploaded by the current user.
* Access to an unfiltered list (files of all users) is only available to moderators.
*
* @param bool $onlyMine Whether to retrieve only files uploaded by the logged-in user. If false, files from all users are returned if the user is a moderator.
* @return array Returns an array containing file data along with a status message.
*/
function listFiles(bool $onlyMine = true): array
{
$output = ["Status" => "Success", "Files" => []];
require_once "lib/account.php";
if (isLoggedIn()) {
global $mysqli;
if (!$onlyMine && !isModerator()) {
$onlyMine = true;
}
$query = "SELECT Files.ID, Files.Path, Files.Type, Files.UploadedAt, Files.UploadedBy, Users.Nickname FROM Files INNER JOIN Users ON Files.UploadedBy = Users.ID ORDER BY UploadedAt DESC";
if ($onlyMine) {
$query .= " WHERE UploadedBy = ?";
}
$stmt = $mysqli->prepare($query);
if ($onlyMine) {
$stmt->bind_param("i", $_SESSION["ID"]);
}
$id = 0;
$path = "";
$type = "";
$uploadedAt = "";
$uploadedByID = 0;
$uploadedBy = "";
$stmt->bind_result($id, $path, $type, $uploadedAt, $uploadedByID, $uploadedBy);
$stmt->execute();
$files = array();
// Fetch the results into the bound variables
while ($stmt->fetch()) {
$files[] = [
'ID' => $id,
'Path' => $path,
'Type' => $type,
'UploadedAt' => $uploadedAt,
'UploadedByID' => $uploadedByID,
'UploadedBy' => $uploadedBy,
];
}
// Check if any results were fetched
$output["Files"] = $files;
$stmt->close();
}
return $output;
}
/**
* Processes incoming files from the $_FILES global (after processed by getIncomingFiles), performs checks, and attempts to upload a file based on its type.
* Currently only supports valid image files.
*
* @return array Returns an array indicating the success or failure ('Status' key) of the file processing operations.
*/
function parseIncomingFiles(): array
{
$incomingFiles = getIncomingFiles();
$success = true;
foreach ($incomingFiles as $incomingFile) {
if ($incomingFile["error"] == 0 && is_file($incomingFile["tmp_name"])) {
$type = explode("/", $incomingFile["type"]);
if ($type[0] == "image") {
$imgFname = pathinfo($incomingFile["name"], PATHINFO_FILENAME);
$uploadPath = getUploadPath("image", $imgFname);
if (!empty($uploadPath)) {
if (!doImageUpload($incomingFile["tmp_name"], $uploadPath)) {
$success = false;
}
} else {
$success = false;
}
}
}
}
$output = ["Status" => "Fail"];
if ($success) {
$output["Status"] = "Success";
}
return $output;
}
/**
* Generates a file path for uploading based on the type of the file, the ID of the uploader and the date and time of uploading.
*
* @param string $type The type of the file, typically used to categorize the file.
* @param string $filename The base name of the file, used in generating the final path.
* @return string Returns the full path for storing the file or an empty string if the type is not recognized.
*/
function getUploadPath(string $type = "unknown", string $filename = "hehe"): string
{
$type = makePathSafe($type);
$id = makePathSafe($_SESSION["ID"]);
$date = makePathSafe(date("YmdHis"));
$filename = makePathSafe($filename);
$extension = match ($type) {
'image' => 'webp',
default => 'dummy',
};
if ($extension != "dummy") {
$basepath = "uploads/$type/$id/$date";
mkdir($basepath, 755, true);
return $basepath . "/$filename.$extension";
} else {
return "";
}
}
/**
* Checks if a file with a given ID exists in the database and does permission checks.
* Access is granted to only the user's files, in order to access all files the onlyMine parameter
* must be false and the user must be a moderator.
*
* @param int $fileId The ID of the file to check.
* @param bool $onlyMine Whether to limit the search to files uploaded by the logged-in user.
* @return bool|string Returns the path of the file if it exists and meets the criteria, false otherwise.
*/
function fileExists(int $fileId, bool $onlyMine = true): bool|string
{
if (!$fileId) {
return false;
}
global $mysqli;
if (!$onlyMine && !isModerator()) {
$onlyMine = true;
}
$query = 'SELECT ID, Path FROM Files WHERE ID = ?' . ($onlyMine ? ' AND UploadedBy = ?' : '');
$stmtfileexists = $mysqli->prepare($query);
if ($onlyMine) {
$stmtfileexists->bind_param('ii', $fileId, $_SESSION['ID']);
} else {
$stmtfileexists->bind_param('i', $fileId);
}
$filePath = "";
$id = null;
$stmtfileexists->bind_result($id, $filePath);
$stmtfileexists->execute();
$stmtfileexists->fetch();
if ($id != null) {
return $filePath;
} else {
return false;
}
}
/**
* Adds a file to a specified group, if the user created the group or creates a new group if
* a group with a specified ID does not exist yet
*
* @param int $groupId The ID of the group to which the file should be added.
* @param int $fileId The ID of the file to add to the group.
* @return array Returns an associative array with a 'Status' key indicating success or failure.
*/
function addToGroup(int $groupId, int $fileId): array
{
$output = ["Status" => "Fail"];
if (!$groupId || !$fileId) {
return $output;
}
global $mysqli;
$stmtcheck = $mysqli->prepare('SELECT ID FROM FileGroups WHERE CreatorID != ? AND ID = ?');
$stmtcheck->bind_param('ii', $_SESSION['ID'], $groupId);
$stmtcheck->execute();
if ($stmtcheck->affected_rows == 0) {
if (fileExists($fileId, false)) {
$stmtadd = $mysqli->prepare('INSERT INTO FileGroups (FileID, CreatorID, ID) VALUES (?, ?, ?)');
$stmtadd->bind_param('iii', $fileId, $_SESSION['ID'], $groupId);
$stmtadd->execute();
if ($stmtadd->affected_rows > 0) {
$output["Status"] = "Success";
}
}
}
return $output;
}
/**
* Deletes a file entry from the database and the file system, a user can only delete his own files,
* except when he is a moderator, in that case he can delete all files.
*
* @param int $fileID The ID of the file to be deleted.
* @return array Returns an array with a 'Status' key indicating the success or failure of the deletion operation.
*/
function deleteFile(int $fileID): array
{
global $mysqli;
$out = ["Status" => "Fail"];
if (isLoggedIn()) {
$file_location = fileExists($fileID, !isModerator());
$query = !isModerator() ? 'DELETE FROM Files WHERE ID = ? AND UploadedBy = ?' : 'DELETE FROM Files WHERE ID = ?';
$stmtDelete = $mysqli->prepare($query);
if (!isModerator()) {
$stmtDelete->bind_param('ii', $fileID, $_SESSION['ID']);
} else {
$stmtDelete->bind_param('i', $fileID);
}
$stmtDelete->execute();
if ($file_location) {
if (unlink($file_location) && $stmtDelete->affected_rows > 0) {
$out['Status'] = 'Success';
}
}
}
return $out;
}

7
pages/News/index.html Normal file

@ -0,0 +1,7 @@
<page minimal_permission_level="2" secret="no" page_title="Novinky"></page>
<header>
<h1 class="title"></h1>
<p>Adlerka študentské news</p>
<hr>
</header>

@ -1,8 +0,0 @@
<!--suppress HtmlUnknownTag, HtmlUnknownTag -->
<page minimal_permission_level="2" secret="yes" page_title="Súbory"></page>
<div id="filelist"></div>
<form id="uploadForm" enctype="multipart/form-data" method="POST">
<label for="fileInput">Send this file: </label>
<input name="userfile" type="file" id="fileInput" multiple />
<input type="button" value="Send File" onclick="uploadFile()" />
</form>

@ -1,47 +0,0 @@
<!--suppress HtmlUnknownTag, HtmlUnknownTag -->
<page minimal_permission_level="2" secret="yes" page_title="Prieskum"></page>
<form id="surveyForm">
<h3>Spokojnosť so stránkou:</h3>
<input type="radio" name="satisfaction" value="5" id="spokojnost_super">
<label for="spokojnost_super">Super</label>
<input type="radio" name="satisfaction" value="4" id="spokojnost_dobre">
<label for="spokojnost_dobre">Dobre</label>
<input type="radio" name="satisfaction" value="3" id="spokojnost_da_sa">
<label for="spokojnost_da_sa">Dá sa</label>
<input type="radio" name="satisfaction" value="2" id="spokojnost_zle">
<label for="spokojnost_zle">Zle</label>
<input type="radio" name="satisfaction" value="1" id="spokojnost_nanic">
<label for="spokojnost_nanic">Nanič</label>
<br>
<br>
<h3>Funkčnosť stránky:</h3>
<input type="radio" name="functionality" value="5" id="funkcnost_super">
<label for="funkcnost_super">Super</label>
<input type="radio" name="functionality" value="4" id="funkcnost_dobre">
<label for="funkcnost_dobre">Dobre</label>
<input type="radio" name="functionality" value="3" id="funkcnost_da_sa">
<label for="funkcnost_da_sa">Dá sa</label>
<input type="radio" name="functionality" value="2" id="funkcnost_zle">
<label for="funkcnost_zle">Zle</label>
<input type="radio" name="functionality" value="1" id="funkcnost_nanic">
<label for="funkcnost_nanic">Nanič</label>
<br>
<br>
<h3>Obsah stránky:</h3>
<input type="radio" name="content" value="5" id="content_super">
<label for="content_super">Super</label>
<input type="radio" name="content" value="4" id="content_dobre">
<label for="content_dobre">Dobre</label>
<input type="radio" name="content" value="3" id="content_da_sa">
<label for="content_da_sa">Dá sa</label>
<input type="radio" name="content" value="2" id="content_zle">
<label for="content_zle">Zle</label>
<input type="radio" name="content" value="1" id="content_nanic">
<label for="content_nanic">Nanič</label>
<br>
<br>
<textarea name="comment" placeholder="Komentár" cols="80" rows="10" required></textarea>
<br>
<br>
<button type="button" onclick="surveySubmit()">Odoslať</button>
</form>

@ -17,6 +17,6 @@ return [
[
"minimal_permission_level" => 1,
"secret" => "no",
"page_title" => "Účet"
"page_title" => "Account"
]
];

@ -1,7 +1,6 @@
<!--suppress HtmlUnknownTag, HtmlUnknownTag -->
<page minimal_permission_level="1" secret="no" page_title="Domov"></page>
<header>
<h1 class="title">Vitaj, na tejto úžasnej stránke</h1>
<p>Neoficiálna študentská stránka pre Adlerku</p>
</header>
<hr>
<hr>
</header>

@ -16,6 +16,6 @@ return [
[
"minimal_permission_level" => 2,
"secret" => "no",
"page_title" => "Nastavenia"
"page_title" => "Account"
]
];

6
pages/memes/index.html Normal file

@ -0,0 +1,6 @@
<page minimal_permission_level="1" secret="no" page_title="Memečká"></page>
<header>
<h1 class="title">Adlerka Memes</h1>
<p>Skoro ako <a href="https://reddit.com/r/adlerka" target="_blank">r/adlerka</a> - ale lepšie.</p>
<hr>
</header>

@ -1,18 +0,0 @@
<?php
require_once "lib/router.php";
require_once "lib/meme.php";
global $routerConfig;
$output = renderMemeGallery();
return [
"output" => $output,
"parameters" =>
[
"minimal_permission_level" => 1,
"secret" => "no",
"page_title" => "Galéria"
]
];

2
pages/memes/info.html Normal file

@ -0,0 +1,2 @@
<page minimal_permission_level="1" secret="no" page_title="Memečká info"></page>
<h1>Vitaj na oficiálnej stránke Memeov o AdlerkaSMP</h1>

@ -1,44 +0,0 @@
<?php
require_once "lib/router.php";
require_once "lib/newsarticle.php";
global $routerConfig;
$output = file_get_contents($routerConfig["template_dir"] . "newsArticles.html");
$articles_out = "";
$articles = [];
$articles_tmp = getNewsArticles();
if($articles_tmp['Status'] == "Success"){
$articles = $articles_tmp["Articles"];
}
$articleTemplate = file_get_contents($routerConfig["template_dir"] . "newsArticle.html");
$output = str_replace("__TEMPLATE_FOR_ARTICLE_CONTENT__", $articleTemplate, $output);
foreach ($articles as $article){
$articleTitle = htmlspecialchars($article["Title"]);
$articleBody = htmlspecialchars($article["Body"]);
//$articleFileList = $article["FileList"];
//$articleWrittenBy = $article["WrittenBy"];
$articleWrittenAt = htmlspecialchars($article["WrittenAt"]);
$articleWrittenByName = htmlspecialchars($article["WrittenByName"]);
$articleTemplate = str_replace("__TEMPLATE_ARTICLE_TITLE__", $articleTitle, $articleTemplate);
$articleTemplate = str_replace("__TEMPLATE_ARTICLE_AUTHOR__", $articleWrittenByName, $articleTemplate);
$articleTemplate = str_replace("__TEMPLATE_ARTICLE_DATE__", $articleWrittenAt, $articleTemplate);
$articleTemplate = str_replace("__TEMPLATE_ARTICLE_BODY__", $articleBody, $articleTemplate);
$articles_out .= $articleTemplate;
}
$output = str_replace("__TEMPLATE__ARTICLES_HERE__", $articles_out, $output);
return [
"output" => $output,
"parameters" =>
[
"minimal_permission_level" => 1,
"secret" => "no",
"page_title" => "Novinky"
]
];

@ -1,8 +1,5 @@
<!--suppress HtmlUnknownTag, HtmlUnknownTag -->
<page minimal_permission_level="2" secret="no" page_title="Zošit"></page>
<header>
<h1 class="title">Adlerka Zošit</h1>
</header>
<hr>
<h2>Čoskoro bude v prevádzke</h2>
<h3>Nájdete(a pridáte) tu poznámky a úlohy zo školy</h3>
<hr>
</header>

@ -1,5 +0,0 @@
<page minimal_permission_level="2" secret="no" page_title="INFO"></page>
<header>
<h1>Toto sú rozvrhy niektorých Adlerákov</h1>
<h2>Zatiaľ hardcoded, potom dorobíme funkcionalitu zbierania dát z Edupage</h2>
</header>

@ -1,62 +0,0 @@
<page minimal_permission_level="2" secret="no" page_title="1.C 2.skupina"></page>
<header>
<h1>Rozvrh 1.C 2.Skupina</h1>
</header>
<main>
<table class="rozvrh">
<tbody>
<tr>
<th>0 (7:10 - 7:55)</th>
<th>1 (8:00 - 8:45)</th>
<th>2 (8:50 - 9:35)</th>
<th>3 (9:45 - 10:30)</th>
<th>4 (10:50 - 11:35)</th>
<th>5 (11:45 - 12:30)</th>
<th>6 (12:40 - 13:25)</th>
<th>7 (13:30 - 14:15)</th>
</tr>
<tr>
<td></td>
<td>MAT</td>
<td>ELK</td>
<td>SJL</td>
<td colspan="2">ZER</td>
<td>ANJ</td>
<td>MAT</td>
</tr>
<tr>
<td colspan="3">PRX</td>
<td>SJL</td>
<td colspan="2">INF</td>
<td>ELK</td>
</tr>
<tr>
<td></td>
<td>PRO</td>
<td>SJL</td>
<td>FYZ</td>
<td>ANJ</td>
<td>MAT</td>
<td>ELK</td>
<td>TSV</td>
</tr>
<tr>
<td colspan="2"></td>
<td colspan="2">PRO</td>
<td>ANJ</td>
<td>TDK</td>
<td>MAT</td>
<td>ETV</td>
</tr>
<tr>
<td></td>
<td>TDK</td>
<td>OBN</td>
<td>TSV</td>
<td>FYZ</td>
<td>PRO</td>
<td>DEJ</td>
</tr>
</tbody>
</table>
</main>

@ -1,62 +0,0 @@
<page minimal_permission_level="2" secret="no" page_title="1.C 1.skupina"></page>
<header>
<h1>Rozvrh 1.C 1.Skupina</h1>
</header>
<main>
<table class="rozvrh">
<tbody>
<tr>
<th>0 (7:10 - 7:55)</th>
<th>1 (8:00 - 8:45)</th>
<th>2 (8:50 - 9:35)</th>
<th>3 (9:45 - 10:30)</th>
<th>4 (10:50 - 11:35)</th>
<th>5 (11:45 - 12:30)</th>
<th>6 (12:40 - 13:25)</th>
<th>7 (13:30 - 14:15)</th>
</tr>
<tr>
<td></td>
<td>MAT</td>
<td>ELK</td>
<td>SJL</td>
<td colspan="2">ZER</td>
<td>MAT</td>
<td>ANJ</td>
</tr>
<tr>
<td colspan="3">PRX</td>
<td>SJL</td>
<td colspan="2">INF</td>
<td>ELK</td>
</tr>
<tr>
<td></td>
<td>PRO</td>
<td>SJL</td>
<td>TSV</td>
<td>FYZ</td>
<td>ANJ</td>
<td>MAT</td>
<td>ELK</td>
</tr>
<tr>
<td colspan="2"></td>
<td colspan="2">PRO</td>
<td>TDK</td>
<td>TSV</td>
<td>MAT</td>
<td>ETV</td>
</tr>
<tr>
<td></td>
<td>ANJ</td>
<td>OBN</td>
<td>TDK</td>
<td>FYZ</td>
<td>PRO</td>
<td>DEJ</td>
</tr>
</tbody>
</table>
</main>

@ -1,64 +0,0 @@
<page minimal_permission_level="2" secret="no" page_title="1.D 1.skupina"></page>
<header>
<h1>Rozvrh 1.D 1.Skupina</h1>
</header>
<main>
<table class="rozvrh">
<tbody>
<tr>
<th>0 (7:10 - 7:55)</th>
<th>1 (8:00 - 8:45)</th>
<th>2 (8:50 - 9:35)</th>
<th>3 (9:45 - 10:30)</th>
<th>4 (10:50 - 11:35)</th>
<th>5 (11:45 - 12:30)</th>
<th>6 (12:40 - 13:25)</th>
<th>7 (13:30 - 14:15)</th>
</tr>
<tr>
<td></td>
<td>TSV</td>
<td colspan="2">PRO</td>
<td>MAT</td>
<td>TDK</td>
<td>PRO</td>
<td>SJL</td>
</tr>
<tr>
<td></td>
<td>FYZ</td>
<td>TDK</td>
<td>MAT</td>
<td>ELK</td>
<td>OBN</td>
<td>ANJ</td>
<td>ETV</td>
</tr>
<tr>
<td></td>
<td>ELK</td>
<td>MAT</td>
<td>SJL</td>
<td>ANJ</td>
<td>ELK</td>
<td colspan="2">INF</td>
</tr>
<tr>
<td colspan="2"></td>
<td colspan="2">ZER</td>
<td>MAT</td>
<td>FYZ</td>
<td>DEJ</td>
<td>TSV</td>
</tr>
<tr>
<td></td>
<td>SJL</td>
<td>ANJ</td>
<td>PRO</td>
<td colspan="3">PRX</td>
</tr>
</tbody>
</table>
</main>

@ -1,33 +1,35 @@
<!--suppress HtmlUnknownTag, HtmlUnknownTag -->
<page minimal_permission_level="1" secret="no" page_title="Domov"></page>
<header>
<h1 class="title">Vitaj, na oficiálnej AdlerkaSMP stránke</h1>
<p>Najlepší <a href="https://minecraft.net" style="text-decoration: underline; color: #fff;" target="_blank">Minecraft®™</a>
server na Adlerke</p>
</header>
<hr>
<main>
<h1 class="title">Vitaj na oficiálnej AdlerkaSMP stránke</h1>
<p>Najlepší <a href="https://minecraft.net" style="text-decoration: underline; color: #fff;" target="_blank">Minecraft®™</a> server na Adlerke</p>
<hr>
<div class="wrapper feature-list">
<h2>Čo môžeš očakávať od AdlerkaSMP:</h2>
<ul class="feature-list-ul">
<li>Módovaný gameplay bez nutnosti sťahovať módy (niektoré módy však vylepšia zážitok)</li>
<ul class="feature-list-ul">
<li>
<strong>Vlastné pluginy:</strong>
<ul>
<li>Plugin 1</li>
<li>Plugin 2</li>
</ul>
</li>
<li>Jednoduché pripojenie, hostname je <strong>adlerka.top</strong></li>
<li>
<strong>Super admini:</strong>
<ul>
<li>Starajú sa o server</li>
<li>Udržiavajú ho bezpečným miestom</li>
<li>
<strong>Zoznam adminov:</strong>
<ul>
<li>BRNSystems</li>
<li>YeahAkis_</li>
</ul>
</li>
</ul>
</li>
</ul>
<li>
<strong>Kvalitné pluginy:</strong>
<ul>
<li>Oraxen</li>
<li>ModelManager</li>
</ul>
</li>
<li>
<strong>Super admini:</strong>
<ul>
<li>Robíme celý server.</li>
<li>Vďaka nám je AdlerkaSMP<br>bezpečné miesto.</li>
</ul>
</li>
</ul>
</div>
</main>
</header>

2
pages/smp/info.html Normal file

@ -0,0 +1,2 @@
<page minimal_permission_level="1" secret="no" page_title="AdlerkaSMP info"></page>
<h1>Vitaj na oficiálnej stránke Informácii o AdlerkaSMP</h1>

@ -1,61 +0,0 @@
<!--suppress HtmlUnknownTag, HtmlUnknownTag -->
<page minimal_permission_level="1" secret="no" page_title="Modlist"></page>
<main>
<h2>Launcher:</h2>
<a href="https://prismlauncher.org/download/">Prism Launcher <b>(ODPORÚČANÝ)</b></a><br>
<h2>Módy pre klienta:</h2>
<a href="/assets/adlerka_client.mrpack">Komplet ako MrPack <b>(ODPORÚČANÉ)</b></a><br>
<h3>Vizuálne:</h3>
<ol>
<li><a href="https://cdn.modrinth.com/data/zV5r3pPn/versions/kJmEO0xO/skinlayers3d-fabric-1.6.2-mc1.20.4.jar">3D Skin Layers</a></li>
<li><a href="https://cdn.modrinth.com/data/wdLuzzEP/versions/rNWb9ncY/Gamma-Utils-1.7.19-mc1.20.4.jar">Gamma Utils</a></li>
<li><a href="https://cdn.modrinth.com/data/w7ThoJFB/versions/BalILUb7/Zoomify-2.13.2.jar">Zoomify</a></li>
<li><a href="https://cdn.modrinth.com/data/M08ruV16/versions/jGGumR4a/bobby-5.1.0%2Bmc1.20.4.jar">Bobby</a></li>
<li><a href="https://cdn.modrinth.com/data/1IjD5062/versions/JXhQlDZl/continuity-3.0.0-beta.4%2B1.20.2.jar">Continuity</a></li>
<li><a href="https://cdn.modrinth.com/data/ZcR9weSm/versions/UrOG4IKT/dynamiccrosshair-7.7%2B1.20.4-fabric.jar">Dynamic Crosshair</a></li>
<li><a href="https://cdn.modrinth.com/data/rUgZvGzi/versions/AqXSvu6M/eating-animation-1.20%2B1.9.61.jar">Eating Animation</a></li>
<li><a href="https://cdn.modrinth.com/data/WhbRG4iK/versions/PNG0Dp43/fallingleaves-1.15.5%2B1.20.1.jar">Falling Leaves</a></li>
<li><a href="https://cdn.modrinth.com/data/YL57xq9U/versions/kGdJ11Rt/iris-mc1.20.4-1.6.17.jar">Iris</a></li>
<li><a href="https://cdn.modrinth.com/data/yBW8D80W/versions/mrQ8ZiyU/lambdynamiclights-2.3.4%2B1.20.4.jar">Lambdynamiclights</a></li>
<li><a href="https://cdn.modrinth.com/data/MPCX6s5C/versions/ZLjUeuU8/notenoughanimations-fabric-1.7.1-mc1.20.4.jar">Not Enough Animations</a></li>
</ol>
<h3>Performance:</h3>
<ol>
<li><a href="https://cdn.modrinth.com/data/NNAgCjsB/versions/7JR5qJ8f/entityculling-fabric-1.6.4-mc1.20.4.jar">Entity Culling</a></li>
<li><a href="https://cdn.modrinth.com/data/uXXizFIs/versions/pguEMpy9/ferritecore-6.0.3-fabric.jar">FerriteCore</a></li>
<li><a href="https://cdn.modrinth.com/data/Orvt0mRa/versions/Aouse6P7/indium-1.0.30%2Bmc1.20.4.jar">Indium</a></li>
<li><a href="https://cdn.modrinth.com/data/fQEb0iXm/versions/bRcuOnao/krypton-0.2.6.jar">Krypton</a></li>
<li><a href="https://cdn.modrinth.com/data/gvQqBUqZ/versions/nMhjKWVE/lithium-fabric-mc1.20.4-0.12.1.jar">Lithium</a></li>
<li><a href="https://cdn.modrinth.com/data/Bh37bMuy/versions/fkLiGoHs/reeses_sodium_options-1.7.2%2Bmc1.20.4-build.102.jar">Reese's Sodium Options</a></li>
<li><a href="https://cdn.modrinth.com/data/AANobbMI/versions/4GyXKCLd/sodium-fabric-0.5.8%2Bmc1.20.4.jar">Sodium</a></li>
<li><a href="https://cdn.modrinth.com/data/PtjYWJkn/versions/M0ndiav7/sodium-extra-0.5.4%2Bmc1.20.4-build.116.jar">Sodium-extra</a></li>
</ol>
<h3>Utility:</h3>
<ol>
<li><a href="https://cdn.modrinth.com/data/DFqQfIBR/versions/rGQZ2yBQ/CraftPresence-2.3.5%2B1.20.4.jar">CraftPresence</a></li>
<li><a href="https://cdn.modrinth.com/data/8shC1gFX/versions/AkivIlyi/BetterF3-9.0.2-Fabric-1.20.4.jar">BetterF3</a></li>
<li><a href="https://cdn.modrinth.com/data/nvQzSEkH/versions/fNHCa6bl/Jade-1.20.4-fabric-13.3.1.jar">Jade</a></li>
<li><a href="https://cdn.modrinth.com/data/aC3cM3Vq/versions/m0Dd8Cjy/MouseTweaks-fabric-mc1.20-2.25.jar">Mouse Tweaks</a></li>
<li><a href="https://cdn.modrinth.com/data/qQyHxfxd/versions/tfv6A4l5/NoChatReports-FABRIC-1.20.4-v2.5.0.jar">No Chat Reports</a></li>
<li><a href="https://cdn.modrinth.com/data/nfn13YXA/versions/Jhw0fDTs/RoughlyEnoughItems-14.0.688-fabric.jar">Roughly Enough Items</a></li>
<li><a href="https://cdn.modrinth.com/data/V8XJ8f5f/versions/q9eTEsvC/RoughlyEnoughProfessions-fabric-1.20.4-2.2.0.jar">Roughly Enough Professions</a></li>
<li><a href="https://cdn.modrinth.com/data/NcUtCpym/versions/2lbtkEPK/XaerosWorldMap_1.38.1_Fabric_1.20.4.jar">Xaero's WorldMap</a></li>
<li><a href="https://cdn.modrinth.com/data/1bokaNcj/versions/N5jBKzC0/Xaeros_Minimap_24.0.3_Fabric_1.20.4.jar">Xaero's_Minimap</a></li>
<li><a href="https://cdn.modrinth.com/data/EsAfCjCV/versions/pmFyu3Sz/appleskin-fabric-mc1.20.3-2.5.1.jar">Appleskin</a></li>
<li><a href="https://cdn.modrinth.com/data/mOgUt4GM/versions/sjtVVlsA/modmenu-9.0.0.jar">Modmenu</a></li>
<li><a href="https://cdn.modrinth.com/data/Nv2fQJo5/versions/TGJXKoTQ/replaymod-1.20.4-2.6.15.jar">Replaymod</a></li>
<li><a href="https://cdn.modrinth.com/data/njGhQ4fN/versions/4tIwORrJ/roughly-searchable-2.6.1%2B1.20.4.jar">Roughly Searchable</a></li>
<li><a href="https://cdn.modrinth.com/data/9eGKb6K1/versions/r7e564VW/voicechat-fabric-1.20.4-2.5.11.jar">Simple Voice Chat</a></li>
</ol>
<h3>Dependecies:</h3>
<ol>
<li><a href="https://cdn.modrinth.com/data/lhGA9TYQ/versions/kVjQWX0l/architectury-11.1.17-fabric.jar">Architectury</a></li>
<li><a href="https://cdn.modrinth.com/data/gu7yAYhd/versions/augSR8tA/cc-tweaked-1.20.4-fabric-1.110.0.jar">CC Tweaked</a></li>
<li><a href="https://cdn.modrinth.com/data/9s6osm5g/versions/eBZiZ9NS/cloth-config-13.0.121-fabric.jar">Cloth Config</a></li>
<li><a href="https://cdn.modrinth.com/data/P7dR8mSH/versions/htRy7kbI/fabric-api-0.96.11%2B1.20.4.jar">Fabric Api</a></li>
<li><a href="https://cdn.modrinth.com/data/Ha28R6CL/versions/ZMokinzs/fabric-language-kotlin-1.10.19%2Bkotlin.1.9.23.jar">Fabric Language Kotlin</a></li>
<li><a href="https://cdn.modrinth.com/data/hvFnDODi/versions/0.1.3/lazydfu-0.1.3.jar">Lazydfu</a></li>
<li><a href="https://cdn.modrinth.com/data/xGdtZczs/versions/Kk7rWLSf/polymer-bundled-0.7.7%2B1.20.4.jar">Polymer</a></li>
<li><a href="https://cdn.modrinth.com/data/1eAoo2KR/versions/StXMrAsz/yet-another-config-lib-fabric-3.3.2%2B1.20.4.jar">Yet Another Config Lib</a></li>
</ol>
</main>

@ -2,6 +2,5 @@
<h2>TY KÁR KAM TO DEŠ</h2>
<h1 class="error-code">403</h1>
<h3><i class="ri-error-warning-line"></i> Našli sme stránku ktorú hľadáš, ale nemáš práva na ňu pristupovať: <span class="error">__TEMPLATE_PAGE_TITLE__</span>. <i class="ri-error-warning-line"></i></h3>
<!--suppress HtmlUnknownTarget -->
<a href="__DEFAULT_LINK__" role="button" class="back"><i class="ri-arrow-left-line"></i> SPÄŤ DOMOV</a>
<a href="/index" class="back"><i class="ri-arrow-left-line"></i> SPÄŤ DOMOV</a>
</div>

@ -2,6 +2,5 @@
<h2>TY KÁR KAM TO DEŠ</h2>
<h1 class="error-code">404</h1>
<h3><i class="ri-error-warning-line"></i> Nenašli sme stránku ktorú hľadáš: <span class="error">__TEMPLATE_PAGE_TITLE__</span>. <i class="ri-error-warning-line"></i></h3>
<!--suppress HtmlUnknownTarget -->
<a href="__DEFAULT_LINK__" role="button" class="back"><i class="ri-arrow-left-line"></i> SPÄŤ DOMOV</a>
<a href="/index" class="back"><i class="ri-arrow-left-line"></i> SPÄŤ DOMOV</a>
</div>

@ -2,6 +2,5 @@
<h2>Niekto to pobabral</h2>
<h1 class="error-code">500</h1>
<h3><i class="ri-error-warning-line"></i> Nejaký neschopný vývojár nevedel robiť túto stránku. <i class="ri-error-warning-line"></i></h3>
<!--suppress HtmlUnknownTarget -->
<a href="__DEFAULT_LINK__" role="button" class="back"><i class="ri-arrow-left-line"></i> SPÄŤ DOMOV</a>
<a href="/index" class="back"><i class="ri-arrow-left-line"></i> SPÄŤ DOMOV</a>
</div>

@ -1,25 +1,85 @@
<div id="admin-settings">
<div class="form-container" id="addActivationCodesForm">
<h1>Activation Codes</h1>
<script>
async function addActivationCodes() {
const count = document.getElementById("activationCodeCount").value;
<h2>List Activation Codes</h2>
<button type="button" onclick="listActivationCodes()">List Activation Codes</button><br>
<h2>Add Activation Codes</h2>
<label for="activationCodeCount">Activation Code Count:</label>
<input type="text" id="activationCodeCount" name="activationCodeCount" value="1" required><br>
<button type="button" onclick="addActivationCodes()">Add Activation Codes</button>
<br>
<table id="codeListTable"></table>
const data = new URLSearchParams();
data.append("action", "add_activation_codes");
data.append("count", count);
</div>
const result = await doAccountAction(data, "Activation codes added Successfully!", "Activation codes addition failed.");
<hr>
displayList(result.ActivationCodes, "codeListTable", deleteActivationCode);
}
async function listUsers() {
const data = new URLSearchParams();
data.append("action", "list_users");
const result = await doAccountAction(data, "User list retrieved Successfully!", "User list retrieval failed.");
if (result && result.Status === "Success") {
displayList(result.Users, "userListTable", deleteUser);
}
}
async function listActivationCodes() {
const data = new URLSearchParams();
data.append("action", "list_activation_codes");
const result = await doAccountAction(data, "Activation code list retrieved Successfully!", "Activation code list retrieval failed.");
displayList(result.ActivationCodes, "codeListTable", deleteActivationCode);
}
function deleteUser(userId) {
const data = new URLSearchParams();
data.append("action", "delete_user");
data.append("user_id", userId);
doAccountAction(data, "User deleted Successfully!", "User deletion failed.");
listUsers();
}
function deleteActivationCode(activationCode) {
const data = new URLSearchParams();
data.append("action", "delete_activation_code");
data.append("activation_code", activationCode);
doAccountAction(data, "Activation code deleted Successfully!", "Activation code deletion failed.");
listActivationCodes();
}
</script>
<script defer>
listActivationCodes();
listUsers();
</script>
<div class="form-container" id="addActivationCodesForm">
<h1>Activation Codes</h1>
<h2>List Activation Codes</h2>
<button type="button" onclick="listActivationCodes()">List Activation Codes</button><br>
<h2>Add Activation Codes</h2>
<label for="activationCodeCount">Activation Code Count:</label>
<input type="text" id="activationCodeCount" name="activationCodeCount" value="1" required><br>
<button type="button" onclick="addActivationCodes()">Add Activation Codes</button>
<br>
<table id="codeListTable"></table>
<div class="form-container" id="listUsersForm">
<h1>List Users</h1>
<button type="button" onclick="listUsers()">List Users</button><br>
<br>
<table id="userListTable"></table>
</div>
</div>
<hr>
<div class="form-container" id="listUsersForm">
<h1>List Users</h1>
<button type="button" onclick="listUsers()">List Users</button><br>
<br>
<table id="userListTable"></table>
</div>
<hr>

@ -1,5 +1,19 @@
<script defer>
async function getUserInfo() {
const data = new URLSearchParams();
data.append("action", "get_user_info");
const result = await doAccountAction(data, null, null, true);
if (result && result.Status === "Success") {
document.getElementById("welcomeMsg").innerText = `Ahoj, ${result.UserInfo.FirstName}.`;
}
}
getUserInfo();
</script>
<div class="dashboard">
<header>
<h1 id="welcomeMsg"></h1>
</header>
<h1 id="welcomeMsg"></h1>
</div>

@ -1 +0,0 @@
<p>Toto nie je oficiálna stránka <span id="ye-span">Adlerky</span>, jedná sa o neoficiálnu študentskú stránku</p>

@ -1,28 +1,77 @@
<script>
function login() {
const email = document.getElementById("login_email").value;
const password = document.getElementById("login_password").value;
doLogin(email, password);
}
function doLogin(email, password) {
const data = new URLSearchParams();
data.append("action", "login");
data.append("email", email);
data.append("password", password);
doAccountAction(data, "Login Successful!", "Login failed. Please check your credentials.");
}
function register() {
const firstName = document.getElementById("register_firstName").value;
const lastName = document.getElementById("register_lastName").value;
const email = document.getElementById("register_email").value;
const password = document.getElementById("register_password").value;
const activationToken = document.getElementById("register_activationToken").value;
const data = new URLSearchParams();
data.append("action", "register");
data.append("firstname", firstName);
data.append("lastname", lastName);
data.append("email", email);
data.append("password", password);
data.append("activation_token", activationToken);
doRegister(data);
}
function doRegister(requestData) {
doAccountAction(requestData, "Registration Successful!", "Registration failed.");
}
</script>
<!-- Centralized Status Message -->
<p id="StatusMessage"></p>
<main class="login-file">
<div class="container" id="container">
<div class="form-container sign-in" id="sign_in_form">
<h1>Login</h1>
<form class="form-content sign-in">
<input type="email" name="email" id="login_email" required placeholder="Email">
<input type="password" name="password" id="login_password" required placeholder="Password">
<button type="button" onclick="login()">Login</button>
</form>
<p onclick="toggleRegister()">Don't have an account</p>
</div>
<div class="form-container sign-up hidden" id="sign_up_form">
<h1>Create Account</h1>
<form class="form-content sign-up">
<input type="text" name="firstName" id="register_firstName" required placeholder="First name">
<input type="text" name="lastName" id="register_lastName" required placeholder="Last name">
<input type="email" name="email" id="register_email" required placeholder="Email">
<input type="password" name="password" id="register_password" required placeholder="Password">
<input type="text" name="activationToken" id="register_activationToken" required
placeholder="Activation Token">
<button type="button">Register</button>
</form>
<p onclick="toggleRegister()">Already have an account</p>
</div>
</div>
</main>
<div class="form-container" id="loginForm">
<h1>Login</h1>
<form>
<label for="login_email">Email:</label>
<input type="email" id="login_email" name="email" required><br>
<label for="login_password">Password:</label>
<input type="password" id="login_password" name="password" required><br>
<button type="button" onclick="login()">Login</button>
</form>
</div>
<div class="form-container" id="register_Form">
<h1>Register</h1>
<form>
<label for="register_firstName">First Name:</label>
<input type="text" id="register_firstName" name="firstName" required><br>
<label for="register_lastName">Last Name:</label>
<input type="text" id="register_lastName" name="lastName" required><br>
<label for="register_email">Email:</label>
<input type="email" id="register_email" name="email" required><br>
<label for="register_password">Password:</label>
<input type="password" id="register_password" name="password" required><br>
<label for="register_activationToken">Activation Token:</label>
<input type="text" id="register_activationToken" name="activationToken" required><br>
<button type="button" onclick="register()">Register</button>
</form>
</div>

@ -1,27 +0,0 @@
<article class="meme" id="meme___TEMPLATE_MEME_ID__">
<div class="meme_header" id="meme_header___TEMPLATE_MEME_ID__">
<h2 class='meme_title' id="meme_title___TEMPLATE_MEME_ID__">__TEMPLATE_MEME_TITLE__</h2>
<div class="meme_topbar" id="meme_info___TEMPLATE_MEME_ID__">
<div class="meme_voting" id="meme_voting___TEMPLATE_MEME_ID__">
__TEMPLATE_MEME_UPVOTE__
<p class="votes_counter __TEMPLATE_MEME_VOTE_COUNTER_CLASS__"
id="meme_votes_counter___TEMPLATE_MEME_ID__">__TEMPLATE_MEME_VOTES_NUMBER__</p>
__TEMPLATE_MEME_DOWNVOTE__
</div>
<div class="meme_info">
<p class='meme_author' id="meme_author___TEMPLATE_MEME_ID__"><i class="ri-user-line"></i>__TEMPLATE_MEME_AUTHOR__
</p>
<p class='meme_date' id="meme_date___TEMPLATE_MEME_ID__"><i class="ri-calendar-line"></i>__TEMPLATE_MEME_DATE__
</p>
__TEMPLATE_MEME_DELETE_BUTTON__
</div>
</div>
</div>
<div class="meme_body" id="meme_body___TEMPLATE_MEME_ID__">
<a class="meme_link" href="__TEMPLATE_MEME_IMAGE__" download>
<img id="meme_image___TEMPLATE_MEME_ID__" src="__TEMPLATE_MEME_IMAGE__" width="__TEMPLATE_MEME_IMAGE_WIDTH__"
height="__TEMPLATE_MEME_IMAGE_HEIGHT__" alt="meme image"
class="meme_image"></a>
<p class="meme_text" id="meme_text___TEMPLATE_MEME_ID__">__TEMPLATE_MEME_TEXT__</p>
</div>
</article>

@ -1,15 +0,0 @@
<div id="memecreatecontainer" class="hidden">
<div id="memecreate">
<label for="meme_title_input">Názov meme-u: </label>
<input type="text" id="meme_title_input" placeholder="Názov meme-u"/>
<label for="meme_text_input">Text meme-u: </label>
<input type="text" id="meme_text_input" placeholder="Text meme-u"/>
<label for="meme_image_input">Obrázok meme-u</label>
<select id="meme_image_input"></select>
<button id="memecreatebutton" onclick="addMeme()"><i class="ri-add-circle-line"></i></button>
<button id="memecreateclose" onclick="togglememecreate()"><i class="ri-close-line"></i></button>
</div>
</div>

@ -1,12 +0,0 @@
<header>
<h1 class="title">Adlerka Memes</h1>
<p>Skoro, ako <a href="https://reddit.com/r/adlerka" target="_blank">r/adlerka</a>, ale lepšie.</p>
<button id="memecreateopen" onclick="togglememecreate()"><i class="ri-add-circle-line"></i></button>
<hr>
</header>
<main>
<div id="meme_gallery">
__TEMPLATE_MEMES_HERE__
</div>
__TEMPLATE_MEME_ADD__
</main>

@ -1,13 +1,6 @@
<div class="logo">
<a href="/home/index">
<picture id="standard-logo">
<source media="(min-width:4200px)" srcset="/assets/images/adlerka_256.png">
<source media="(min-width:2100px)" srcset="/assets/images/adlerka_128.png">
<img src="/assets/images/adlerka_64.png" alt="Adlerka logo" style="width:auto;">
</picture>
</a>
</div>
<i class="ri-menu-line" id="toggle_button"></i>
<ul id="navsite_list">
__NAV_PAGES__
</ul>
<nav>
<div class="logo"><img alt="Adlerka logo" class="standard-logo" src="https://www.adlerka.sk/wp-content/uploads/2021/09/Logo_text_Adlerka_modro_cerveno_biele-e1652431356820.png" title="Adlerka"></div>
<ul class="navsite_list">
__NAV_PAGES__
</ul>
</nav>

@ -1,11 +0,0 @@
<article>
<h2 class='newstitle'>__TEMPLATE_ARTICLE_TITLE__</h2>
<div class="articleinfo">
<p class='newsauthor'><i class="ri-user-line"></i>__TEMPLATE_ARTICLE_AUTHOR__</p>
<p class='newsdate'><i class="ri-calendar-line"></i>__TEMPLATE_ARTICLE_DATE__</p>
</div>
<hr>
<div class='newsbody'>
__TEMPLATE_ARTICLE_BODY__
</div>
</article>

@ -1,24 +0,0 @@
<header>
<h1 class="title"></h1>
<p>Adlerka študentské news</p>
<button id="articlecreateopen" onclick="togglearticlecreate()"><i class="ri-add-circle-line"></i></button>
</header>
<template data-template-name="article">
__TEMPLATE_FOR_ARTICLE_CONTENT__
</template>
<div id="articleslist">
__TEMPLATE__ARTICLES_HERE__
</div>
<div id="articlecreatecontainer" class="hidden">
<div id="articlecreate">
<input type="text" placeholder="Article Title" id="articletitleinput"><br>
<textarea id="articlebodyinput" placeholder="Article Body" rows="10" cols="80"></textarea><br>
<label for="articleprivilegeinput">Oprávnenie na pozretie článku:</label>
<input type="number" id="articleprivilegeinput" min="1" value="1" step="1">
<button id="articlesubmit" onclick="submitarticle()"><i class="ri-add-circle-line"></i></button>
<button id="articlecreateclose" onclick="togglearticlecreate()"><i class="ri-close-line"></i></button>
</div>
</div>

@ -1,29 +1,17 @@
<!DOCTYPE html>
<html lang="sk" data-theme="dark">
<html lang="sk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/assets/3rdparty/fonts/remixicon/remixicon.css">
<link rel="stylesheet" href="/assets/3rdparty/pico.min.css">
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="/assets/images/favicon.png" type="image/png">
__TEMPLATE__DYNAMIC__SCRIPT__
__TEMPLATE__DYNAMIC__STYLE__
<script src="/assets/script.js"></script>
<script async src="https://umami.brn.systems/script.js" data-website-id="95e93885-5c19-4cab-ba9b-2f746a316a2a"></script>
<script async src="/assets/script.js"></script>
<title>__TEMPLATE_PAGE_TITLE__</title>
__TEMPLATE_SEO_STUFF__
<title>Adlerka __TEMPLATE_PAGE_TITLE__</title>
__TEMPLATE__DYNASTYLE__
</head>
<body>
<nav id="navbar_container">
__TEMPLATE__NAV__
</nav>
<div id="statusMessageContainer"></div>
<main id="page_container">
__TEMPLATE__PAGE__
</main>
<footer id="footer_container">
__TEMPLATE__FOOTER__
</footer>
__TEMPLATE__NAV__
__TEMPLATE__PAGE__
</body>
</html>

@ -1,50 +1,114 @@
<script>
function changePassword() {
const oldPassword = document.getElementById("changeOldPassword").value;
const newPassword = document.getElementById("changeNewPassword").value;
const data = new URLSearchParams();
data.append("action", "change_password");
data.append("old_password", oldPassword);
data.append("new_password", newPassword);
doChangePassword(data, "Password change Successful!", "Password change failed.");
}
function doChangePassword(requestData, successMessage, failureMessage) {
doAccountAction(requestData, successMessage, failureMessage);
}
function updateUserProfile() {
const firstName = document.getElementById("updateFirstName").value;
const lastName = document.getElementById("updateLastName").value;
const nickname = document.getElementById("updateNickname").value;
const minecraftNick = document.getElementById("updateMinecraftNick").value;
const data = new URLSearchParams();
data.append("action", "update_user_profile");
data.append("first_name", firstName);
data.append("last_name", lastName);
data.append("nickname", nickname);
data.append("minecraft_nick", minecraftNick);
doAccountAction(data, "Profile update Successful!", "Profile update failed.");
}
function updateEmail() {
const newEmail = document.getElementById("updateNewEmail").value;
const data = new URLSearchParams();
data.append("action", "update_user_email");
data.append("email", newEmail);
doAccountAction(data, "Email update Successful!", "Email update failed.");
}
function populateUserInfoFields(userData) {
document.getElementById("updateFirstName").value = userData.FirstName || "";
document.getElementById("updateLastName").value = userData.LastName || "";
document.getElementById("updateNickname").value = userData.Nickname || "";
document.getElementById("updateMinecraftNick").value = userData.MinecraftNick || "";
document.getElementById("updateNewEmail").value = userData.Email || "";
}
async function getUserInfo() {
const data = new URLSearchParams();
data.append("action", "get_user_info");
const result = await doAccountAction(data, "User info retrieved Successfully!", "User info retrieval failed.", true);
if (result && result.Status === "Success") {
populateUserInfoFields(result.UserInfo);
}
}
</script>
<script defer>
getUserInfo();
</script>
<!-- Centralized Status Message -->
<p id="StatusMessage"></p>
<div id="user-settings">
<button type="button" onclick="logout()">Logout</button>
<br>
<div class="form-container" id="updateUserProfileForm">
<h1>Update User</h1>
<button type="button" onclick="logout()">Logout</button><br>
<h2>Profile</h2>
<div class="form-content" id="profile-form">
<label for="updateFirstName">First Name:</label>
<input type="text" id="updateFirstName" name="updateFirstName" required><br>
<div class="form-container" id="updateUserProfileForm">
<h1>Update User</h1>
<label for="updateLastName">Last Name:</label>
<input type="text" id="updateLastName" name="updateLastName" required><br>
<h2>Profile</h2>
<label for="updateFirstName">First Name:</label>
<input type="text" id="updateFirstName" name="updateFirstName" required><br>
<label for="updateNickname">Nickname:</label>
<input type="text" id="updateNickname" name="updateNickname" required><br>
<label for="updateLastName">Last Name:</label>
<input type="text" id="updateLastName" name="updateLastName" required><br>
<label for="updateMinecraftNick">Minecraft Nick:</label>
<input type="text" id="updateMinecraftNick" name="updateMinecraftNick" required><br>
<label for="updateNickname">Nickname:</label>
<input type="text" id="updateNickname" name="updateNickname" required><br>
<button type="button" onclick="updateUserProfile()">Update Profile</button>
</div>
<br><br>
<label for="updateMinecraftNick">Minecraft Nick:</label>
<input type="text" id="updateMinecraftNick" name="updateMinecraftNick" required><br>
<h2>Email</h2>
<div class="form-content" id="email-form">
<label for="updateNewEmail">New Email:</label>
<input type="email" id="updateNewEmail" name="updateNewEmail" required><br>
<button type="button" onclick="updateUserProfile()">Update Profile</button>
<button type="button" onclick="updateEmail()">Update Email</button>
</div>
<br><br>
<br><br>
<h2>Password</h2>
<div class="form-content" id="password-form">
<label for="changeOldPassword">Old Password:</label>
<input type="password" id="changeOldPassword" name="changeOldPassword" required><br>
<h2>Email</h2>
<label for="changeNewPassword">New Password:</label>
<input type="password" id="changeNewPassword" name="changeNewPassword" required><br>
<label for="updateNewEmail">New Email:</label>
<input type="email" id="updateNewEmail" name="updateNewEmail" required><br>
<button type="button" onclick="changePassword()">Change Password</button>
</div>
</div>
<button type="button" onclick="updateEmail()">Update Email</button>
<br><br>
<h2>Password</h2>
<label for="changeOldPassword">Old Password:</label>
<input type="password" id="changeOldPassword" name="changeOldPassword" required><br>
<label for="changeNewPassword">New Password:</label>
<input type="password" id="changeNewPassword" name="changeNewPassword" required><br>
<button type="button" onclick="changePassword()">Change Password</button>
</div>
<hr>