diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/accounts.php | 101 | ||||
| -rw-r--r-- | lib/alert.php | 40 | ||||
| -rw-r--r-- | lib/captcha.php | 151 | ||||
| -rw-r--r-- | lib/config.sample.php | 74 | ||||
| -rw-r--r-- | lib/emote.php | 332 | ||||
| -rw-r--r-- | lib/images.php | 78 | ||||
| -rw-r--r-- | lib/partials.php | 179 | ||||
| -rw-r--r-- | lib/user.php | 102 | ||||
| -rw-r--r-- | lib/utils.php | 68 | ||||
| -rw-r--r-- | lib/version.php | 11 |
10 files changed, 1136 insertions, 0 deletions
diff --git a/lib/accounts.php b/lib/accounts.php new file mode 100644 index 0000000..51cb3f6 --- /dev/null +++ b/lib/accounts.php @@ -0,0 +1,101 @@ +<?php +include_once "config.php"; + +function authorize_user(bool $required = false): bool +{ + session_start(); + + if (!isset($_COOKIE["secret_key"]) && !isset($_SERVER["HTTP_AUTHORIZATION"])) { + if (isset($_SESSION["user_id"])) { + session_unset(); + } + + if ($required) { + if (isset($_SERVER["HTTP_ACCEPT"]) && $_SERVER["HTTP_ACCEPT"] == "application/json") { + http_response_code(401); + echo json_encode([ + "status_code" => 401, + "message" => "Unauthorized", + "data" => null + ]); + } else { + header("Location: /account"); + } + } + + return false; + } + + include_once "config.php"; + + $db = new PDO(DB_URL, DB_USER, DB_PASS); + + $key = $_SERVER["HTTP_AUTHORIZATION"] ?? $_COOKIE["secret_key"]; + + $stmt = $db->prepare("SELECT id, username FROM users WHERE secret_key = ?"); + $stmt->execute([$key]); + + if ($row = $stmt->fetch()) { + $_SESSION["user_id"] = $row["id"]; + $_SESSION["user_name"] = $row["username"]; + + $stmt = $db->prepare("UPDATE users SET last_active_at = UTC_TIMESTAMP WHERE id = ?"); + $stmt->execute([$row["id"]]); + + // fetching role + $stmt = $db->prepare("SELECT * FROM roles r + INNER JOIN role_assigns ra ON ra.user_id = ? + WHERE r.id = ra.role_id + "); + $stmt->execute([$row["id"]]); + + $_SESSION["user_role"] = null; + + if ($role_row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $_SESSION["user_role"] = $role_row; + } + + $stmt = $db->prepare("SELECT es.*, aes.is_default FROM emote_sets es + INNER JOIN acquired_emote_sets aes ON aes.emote_set_id = es.id + WHERE aes.user_id = ? + ORDER BY + CASE WHEN es.id = ? THEN 0 ELSE 1 END, + es.id + "); + $stmt->execute([$row["id"], $_SESSION["user_active_emote_set_id"] ?? ""]); + + $emote_sets = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (!isset($_SESSION["user_active_emote_set_id"])) { + foreach ($emote_sets as $es) { + if ($es["is_default"]) { + $_SESSION["user_active_emote_set"] = $es; + $_SESSION["user_active_emote_set_id"] = $es["id"]; + } + } + } + + $_SESSION["user_emote_sets"] = $emote_sets; + } else { + session_regenerate_id(); + session_unset(); + setcookie("secret_key", "", time() - 1000); + + if ($required) { + if (isset($_SERVER["HTTP_ACCEPT"]) && $_SERVER["HTTP_ACCEPT"] == "application/json") { + http_response_code(401); + echo json_encode([ + "status_code" => 401, + "message" => "Unauthorized", + "data" => null + ]); + } else { + header("Location: /account"); + } + } + } + + $db = null; + $stmt = null; + return isset($_SESSION["user_name"]); +}
\ No newline at end of file diff --git a/lib/alert.php b/lib/alert.php new file mode 100644 index 0000000..823a97a --- /dev/null +++ b/lib/alert.php @@ -0,0 +1,40 @@ +<?php +function generate_alert(string $path, string $error, int $status = 400) +{ + http_response_code($status); + + if (isset($_SERVER["HTTP_ACCEPT"]) && $_SERVER["HTTP_ACCEPT"] == "application/json") { + header("Content-Type: application/json"); + echo json_encode([ + "status_code" => $status, + "message" => $error, + "data" => null + ]); + } else { + header("Location: $path" . (str_contains($path, "?") ? "&" : "?") . "error_status=$status&error_reason=$error"); + } +} + +function display_alert() +{ + if (!isset($_GET["error_status"], $_GET["error_reason"])) { + return; + } + + $status = $_GET["error_status"]; + $reason = str_safe($_GET["error_reason"], 100); + $ok = substr($status, 0, 1) == '2'; + + echo '' ?> + <div class="box row alert <?php echo $ok ? '' : 'red' ?>" style="gap:8px;" id="alert-box"> + <img src="/static/img/icons/<?php echo $ok ? 'yes' : 'no' ?>.png" alt=""> + <p><b><?php echo $reason ?></b></p> + </div> + <script> + setTimeout(() => { + const alertBox = document.getElementById("alert-box"); + alertBox.remove(); + }, 5000); + </script> + <?php +}
\ No newline at end of file diff --git a/lib/captcha.php b/lib/captcha.php new file mode 100644 index 0000000..d6d2547 --- /dev/null +++ b/lib/captcha.php @@ -0,0 +1,151 @@ +<?php +function generate_image_captcha(int $width, int $height, int $difficulty, string $file_name, string $file_folder): string +{ + $image = imagecreatetruecolor($width, $height); + + $background = imagecolorallocate($image, 0xDD, 0xDD, 0xDD); + imagefilledrectangle($image, 0, 0, $width, $height, $background); + + $files = scandir($file_folder); + array_splice($files, 0, 2); + + for ($i = 0; $i < 50 * $difficulty; $i++) { + $unprocessed = imagecreatefrompng("$file_folder/" . $files[random_int(0, count($files) - 1)]); + + $oldw = imagesx($unprocessed); + $oldh = imagesy($unprocessed); + + $w = random_int(round($oldw / 4), round($oldw / 2)); + $h = random_int(round($oldh / 4), round($oldh / 2)); + + $file = imagecreatetruecolor($w, $h); + imagealphablending($file, false); + $transparent = imagecolorallocatealpha($file, 0, 0, 0, 127); + imagefill($file, 0, 0, $transparent); + imagesavealpha($file, true); + + imagecopyresampled($file, $unprocessed, 0, 0, 0, 0, $w, $h, $oldw, $oldh); + + $angle = random_int(0, 360); + + $file = imagerotate($file, $angle, $transparent); + + for ($j = 0; $j < random_int(2, 5 * $difficulty); $j++) { + imagefilter($file, IMG_FILTER_GAUSSIAN_BLUR); + } + + if (random_int(0, 15) % 3 == 0) { + imagefilter($file, IMG_FILTER_NEGATE); + } + + if (random_int(0, 20) % 4 == 0) { + imagefilter($file, IMG_FILTER_PIXELATE, 4); + } + + $w = imagesx($file); + $h = imagesy($file); + + imagecopy( + $image, + $file, + random_int(0, $width - $w), + random_int(0, $height - $h), + 0, + 0, + $w, + $h + ); + } + + $foreground = imagecreatefrompng("$file_folder/$file_name.png"); + $transparent = imagecolorallocatealpha($foreground, 0, 0, 0, 127); + $angle = random_int(0, max: 180); + $foreground = imagerotate($foreground, $angle, $transparent); + $w = imagesx($foreground); + $h = imagesy($foreground); + imagecopy( + $image, + $foreground, + random_int(0, $width - $w), + random_int(0, $height - $h), + 0, + 0, + $w, + $h + ); + + ob_start(); + imagepng($image); + $source = ob_get_contents(); + ob_clean(); + + return "data:image/png;base64," . base64_encode($source); +} + +function html_captcha_form() +{ + echo '' ?> + <div class="box" id="form-captcha-wrapper" style="display: none;"> + <div class="box navtab"> + Solve captcha + </div> + <div class="box content"> + <noscript>JavaScript is required for captcha!</noscript> + <form id="form-captcha"> + <img src="" alt="Generating captcha..." id="form-captcha-img" width="256"> + <div class="column small-gap"> + <div class="row small-gap"> + <input type="text" name="answer" placeholder="Enter emote name..." class="grow" + id="form-captcha-answer"> + <button type="submit" class="green" id="form-captcha-solve">Solve</button> + </div> + </div> + </form> + </div> + </div> + <script> + const formElement = document.getElementById("form-captcha"); + const formWrapper = document.getElementById("form-captcha-wrapper"); + + function get_captcha() { + fetch("/captcha.php") + .then((response) => response.json()) + .then((json) => { + if (json.data == null) { + formWrapper.style.display = "none"; + return; + } + + document.getElementById("form-captcha-answer").value = null; + + formWrapper.style.display = "flex"; + + document.getElementById("form-captcha-img").setAttribute("src", json.data); + }); + } + + get_captcha(); + + formElement.addEventListener("submit", (e) => { + e.preventDefault(); + + const answer = document.getElementById("form-captcha-answer"); + const body = new FormData(formElement); + + fetch("/captcha.php", { + "method": "POST", + "body": body + }) + .then((response) => response.json()) + .then((json) => { + if (json.status_code == 200 && json.data == null) { + formWrapper.style.display = "none"; + return; + } + + get_captcha(); + }); + }); + </script> + <?php ; +}
\ No newline at end of file diff --git a/lib/config.sample.php b/lib/config.sample.php new file mode 100644 index 0000000..3d30044 --- /dev/null +++ b/lib/config.sample.php @@ -0,0 +1,74 @@ +<?php +// INSTANCE +define("INSTANCE_NAME", "TinyEmotes"); +define("INSTANCE_STATIC_FOLDER", "static"); // Static folder. Used only in /404.php. + +// DATABASE +define("DB_USER", "ENTER_DATABASE_USER"); // Database user. MANDATORY! +define("DB_PASS", "ENTER_DATABASE_PASSWORD"); // Database password. MANDATORY! +define("DB_HOST", "ENTER_DATABASE_HOST"); // Database host. Can be 'localhost' if it's on the same machine as Tinyemotes. +define("DB_NAME", "ENTER_DATABASE_NAME"); // Database name. +define("DB_URL", 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';port=3306'); // Database URL. Change it if you don't use MySQL/MariaDB. + +// RATINGS +define("RATING_ENABLE", true); // Enable ratings for emotes. +define("RATING_NAMES", [ + "-1" => "COAL", + "1" => "GEM", +]); // Rating names. The schema is [ "id/rating_point" => "name" ]. +define("RATING_EMOTE_MIN_VOTES", 10); // Minimal amount of votes to display emote rating. + +// UPLOADS +define("ANONYMOUS_UPLOAD", false); // Allow anonymous upload for emotes. +define("ANONYMOUS_DEFAULT_NAME", "Anonymous"); // Default uploader name for anonymous emotes. It's also used when original uploader has been deleted. + +// EMOTES +define("EMOTE_UPLOAD", true); // Enable emote upload. +define("EMOTE_NAME_MAX_LENGTH", 100); // Max length for emote name. +define("EMOTE_COMMENT_MAX_LENGTH", 100); // Max length for emote comment. +define("EMOTE_VISIBILITY_DEFAULT", 2); // Default visibility for emotes. 0 - unlisted, 1 - public, 2 - pending approval (same as unlisted). +define("EMOTE_MAX_SIZE", [128, 128]); // Max size of emote. +define("EMOTE_NAME_REGEX", "/^[A-Za-z0-9_]+$/"); // RegEx filter for emote names. +define("EMOTE_STORE_ORIGINAL", true); // Store original uploads of emotes. + +// TAGS +define("TAGS_ENABLE", true); // Allow emote tagging. +define("TAGS_CODE_REGEX", "/^[A-Za-z0-9_]+$/"); +define("TAGS_MAX_COUNT", 10); // Maximum tags per emote. Set -1 for unlimited amount. + +// EMOTESETS +define("EMOTESET_PUBLIC_LIST", true); // Show emotesets public. + +// MODERATION +define("MOD_SYSTEM_DASHBOARD", true); // Enable system dashboard for moderators (/system). +define("MOD_EMOTES_APPROVE", true); // Enable manual emote approval (/system/emotes). + +// REPORTS +define("REPORTS_ENABLE", true); // Enable emote, user reports. + +// ACCOUNTS +define("ACCOUNT_REGISTRATION_ENABLE", true); // Enable account registration. +define("ACCOUNT_COOKIE_MAX_LIFETIME", 86400 * 30); // Remember user for a month. +define("ACCOUNT_USERNAME_REGEX", "/^[A-Za-z0-9_]+$/"); // RegEx filter for account usernames. +define("ACCOUNT_USERNAME_LENGTH", [2, 20]); // [Min, Max] length for account usernames. +define("ACCOUNT_PASSWORD_MIN_LENGTH", 10); // Minimal length for passwords. +define("ACCOUNT_SECRET_KEY_LENGTH", 32); // The length for secret keys. +define("ACCOUNT_PFP_MAX_SIZE", [128, 128]); // Max dimensions for account pictures. +define("ACCOUNT_BANNER_MAX_SIZE", [1920, 1080]); // Max dimensions for account banners. +define("ACCOUNT_BADGE_MAX_SIZE", [72, 72]); // Max dimensions for account badges. +define("ACCOUNT_PUBLIC_LIST", true); // The public list of accounts. +define("ACCOUNT_LOG_ACTIONS", true); // Log user's actions (emote addition, etc.). + +// TWITCH +define("TWITCH_REGISTRATION_ENABLE", false); // Enable account registration via Twitch. +define("TWITCH_CLIENT_ID", "AAAAAAAAA"); // Client ID of your Twitch application. +define("TWITCH_SECRET_KEY", "BBBBBBBBB"); // Secret key of your Twitch application. +define("TWITCH_REDIRECT_URI", ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http") . "://$_SERVER[HTTP_HOST]/account/login/twitch.php"); // Redirect URI of your Twitch application. + +// CAPTCHA +define("CAPTCHA_ENABLE", true); // Enable built-in captcha. +define("CAPTCHA_SIZE", [580, 220]); // Captcha size. +define("CAPTCHA_FORCE_USERS", false); // Force authorized users to solve captcha. + +// FOR DEVELOPERS +define("CLIENT_REQUIRES_JSON", isset($_SERVER["HTTP_ACCEPT"]) && $_SERVER["HTTP_ACCEPT"] == "application/json");
\ No newline at end of file diff --git a/lib/emote.php b/lib/emote.php new file mode 100644 index 0000000..ce39c09 --- /dev/null +++ b/lib/emote.php @@ -0,0 +1,332 @@ +<?php +include_once "user.php"; + +class Emote +{ + public string $id; + public string $code; + public string $ext; + public mixed $uploaded_by; + public int $created_at; + public mixed $rating; + public bool $is_in_user_set; + public int $visibility; + + public string|null $source; + + public array $tags; + + public static function from_array(array $arr): Emote + { + $e = new Emote(); + + $e->id = $arr["id"]; + $e->code = $arr["code"]; + $e->ext = $arr["ext"] ?? "webp"; + $e->uploaded_by = $arr["uploaded_by"]; + $e->created_at = strtotime($arr["created_at"] ?? 0); + $e->is_in_user_set = $arr["is_in_user_set"] ?? false; + $e->visibility = $arr["visibility"]; + $e->source = $arr["source"] ?? null; + $e->tags = $arr["tags"] ?? []; + + if (isset($arr["total_rating"], $arr["average_rating"])) { + $e->rating = [ + "total" => $arr["total_rating"], + "average" => $arr["average_rating"] + ]; + } else { + $e->rating = $arr["rating"] ?? null; + } + + return $e; + } + + public static function from_array_with_user(array $arr, PDO &$db): Emote + { + if ($arr["uploaded_by"]) { + $arr["uploaded_by"] = User::get_user_by_id($db, $arr["uploaded_by"]); + } + + return Emote::from_array($arr); + } + + function get_id() + { + return $this->id; + } + + function get_code() + { + return $this->code; + } + + function get_ext() + { + return $this->ext; + } + + function get_created_at() + { + return $this->created_at; + } + + function get_uploaded_by() + { + return $this->uploaded_by; + } + + function is_added_by_user() + { + return $this->is_in_user_set; + } + + function get_rating() + { + return $this->rating; + } + + function get_visibility() + { + return $this->visibility; + } + + function get_source() + { + return $this->source; + } + + function get_tags(): array + { + return $this->tags; + } +} + +class Emoteset +{ + public string $id; + public string $name; + public User|null $owner; + public array $emotes; + + public bool $is_default; + + public static function from_array(array $arr): Emoteset + { + $s = new Emoteset(); + + $s->id = $arr["id"]; + $s->name = $arr["name"]; + $s->owner = $arr["owner_id"]; + $s->emotes = $arr["emotes"] ?? []; + $s->is_default = $arr["is_default"] ?? false; + + return $s; + } + + public static function from_array_extended(array $arr, string $user_id, PDO &$db): Emoteset + { + if ($arr["owner_id"]) { + $arr["owner_id"] = User::get_user_by_id($db, $arr["owner_id"]); + } + + $arr["emotes"] = fetch_all_emotes_from_emoteset($db, $arr["id"], $user_id); + + return Emoteset::from_array($arr); + } + + public static function get_all_user_emotesets(PDO &$db, string $user_id): array + { + $stmt = $db->prepare("SELECT es.*, aes.is_default FROM emote_sets es + INNER JOIN acquired_emote_sets aes ON aes.emote_set_id = es.id + WHERE aes.user_id = ? + "); + $stmt->execute([$user_id]); + + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + $emote_sets = []; + + foreach ($rows as $row) { + array_push($emote_sets, Emoteset::from_array_extended($row, $user_id, $db)); + } + + return $emote_sets; + } +} + +function fetch_all_emotes_from_emoteset(PDO &$db, string $emote_set_id, string $user_id, int|null $limit = null): array +{ + // fetching emotes + $sql = "SELECT + e.id, e.created_at, e.visibility, + CASE + WHEN esc.code IS NOT NULL THEN esc.code + ELSE e.code + END AS code, + CASE + WHEN esc.code IS NOT NULL THEN e.code + ELSE NULL + END AS original_code, + CASE WHEN up.private_profile = FALSE OR up.id = ? THEN e.uploaded_by ELSE NULL END AS uploaded_by + FROM emotes e + LEFT JOIN user_preferences up ON up.id = e.uploaded_by + INNER JOIN emote_set_contents AS esc + ON esc.emote_set_id = ? + WHERE esc.emote_id = e.id"; + + if ($limit) { + $sql .= " LIMIT $limit"; + } + + $stmt = $db->prepare($sql); + $stmt->execute([$user_id, $emote_set_id]); + + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + $emotes = []; + + // fetching uploaders + foreach ($rows as $row) { + if ($row["uploaded_by"]) { + $row["uploaded_by"] = User::get_user_by_id($db, $row["uploaded_by"]); + } + + array_push($emotes, Emote::from_array($row)); + } + + return $emotes; +} + +function html_random_emote(PDO &$db) +{ + $stmt = $db->prepare("SELECT id, code FROM emotes WHERE visibility = 1 ORDER BY RAND() LIMIT 1"); + $stmt->execute(); + + if ($row = $stmt->fetch()) { + echo '' + ?> + <section class="box" id="box-random-emote"> + <div class="box navtab"> + <p>Random emote</p> + </div> + <div class="box content center"> + <a href="/emotes?id=<?php echo $row["id"] ?>"> + <img src="/static/userdata/emotes/<?php echo $row["id"] ?>/3x.webp" alt="<?php echo $row["code"] ?>" + width="192"> + </a> + </div> + </section> + <?php + ; + } +} + +function html_featured_emote(PDO &$db) +{ + $stmt = $db->prepare("SELECT e.id, e.code FROM emotes e + INNER JOIN emote_sets es ON es.is_featured = TRUE + INNER JOIN emote_set_contents esc ON es.id = esc.emote_set_id + WHERE e.visibility = 1 AND e.id = esc.emote_id ORDER BY esc.added_at DESC LIMIT 1"); + $stmt->execute(); + + if ($row = $stmt->fetch()) { + echo '' + ?> + <section class="box" id="box-featured-emote"> + <div class="box navtab"> + <p>Featured emote</p> + </div> + <div class="box content center"> + <a href="/emotes?id=<?php echo $row["id"] ?>"> + <img src="/static/userdata/emotes/<?php echo $row["id"] ?>/3x.webp" alt="<?php echo $row["code"] ?>" + width="192"> + </a> + </div> + </section> + <?php + ; + } +} + +function html_display_emotes(array $emotes, int $scale = 3) +{ + $emote_wall = intval($_COOKIE["emotelist_wall"] ?? "0") == 1; + + foreach ($emotes as $e) { + echo "<a class='box emote column justify-center items-center' href='/emotes?id={$e->id}'>"; + + if ($e->is_added_by_user()) { + echo '<img src="/static/img/icons/yes.png" class="emote-check" />'; + } + + // icon + echo '<div class="flex justify-center items-center grow emote-icon">'; + $scale = $emote_wall ? "3" : ((string) $scale); + echo "<img src='/static/userdata/emotes/{$e->id}/{$scale}x.webp' alt='{$e->code}' />"; + echo '</div>'; + + // info + echo '<div class="flex column justify-bottom items-center emote-desc'; + if ($emote_wall) { + echo ' none'; + } + echo '">'; + + echo "<h1 title='{$e->code}'>{$e->code}</h1>"; + if ($e->get_uploaded_by()) { + echo "<p>{$e->uploaded_by->username}</p>"; + } + + echo '</div></a>'; + } +} + +function html_display_emoteset(array $emotesets) +{ + foreach ($emotesets as $es) { + echo "<a href='/emotesets.php?id={$es->id}' class='box column small-gap'>"; + + echo '<div>'; + echo "<p>$es->name</p>"; + echo '</div>'; + + echo '<div class="small-gap row">'; + + foreach ($es->emotes as $e) { + echo "<img src='/static/userdata/emotes/{$e->id}/1x.webp' alt='{$e->code}' title='{$e->code}' height='16' />"; + } + + echo '</div></a>'; + + } +} + +function html_emotelist_mode() +{ + echo '' ?> + <div class="row small-gap" id="control-emotelist" style="display: none;"> + <a href="#" onclick="set_emotelist_mode('wall')"> + <img src="/static/img/icons/emotes/emote.png" alt="Emote Wall" title="Emote Wall"> + </a> + <a href="#" onclick="set_emotelist_mode('table')"> + <img src="/static/img/icons/table.png" alt="Emote Descriptions" title="Emote Descriptions"> + </a> + </div> + <script> + document.getElementById("control-emotelist").style.display = "flex"; + + function set_emotelist_mode(mode) { + const elements = document.querySelectorAll(".emote .emote-desc"); + + for (const element of elements) { + if (mode == "wall") { + element.classList.add("none"); + document.cookie = "emotelist_wall = 1; expires=Tue, 19 Jan 2038 00:00:00 UTC; path=/"; + } else { + element.classList.remove("none"); + document.cookie = "emotelist_wall = 0; expires=Tue, 19 Jan 2038 00:00:00 UTC; path=/"; + } + } + } + </script> + <?php ; +}
\ No newline at end of file diff --git a/lib/images.php b/lib/images.php new file mode 100644 index 0000000..9ec0301 --- /dev/null +++ b/lib/images.php @@ -0,0 +1,78 @@ +<?php +function resize_image(string $src_path, string $dst_path, int $max_width, int $max_height, bool $set_format = true, bool $stretch = false): int|null +{ + if ($src_path == "") { + return -2; + } + + $image = getimagesize($src_path); + + if ($image == false) { + return -1; + } + + $format = $set_format ? ".webp" : ""; + + $width = $image[0]; + $height = $image[1]; + $ratio = min($max_width / $width, $max_height / $height); + $new_width = $stretch ? $max_width : (int) ($width * $ratio); + $new_height = $stretch ? $max_height : (int) ($height * $ratio); + + $input_path = escapeshellarg($src_path); + $output_path = escapeshellarg("$dst_path$format"); + + $result_code = null; + + exec(command: "magick convert $input_path -coalesce -resize {$new_width}x{$new_height}! -loop 0 $output_path", result_code: $result_code); + + return $result_code; +} + +function create_image_bundle(string $src_path, string $dst_path, int $max_width, int $max_height, bool $set_format = true, bool $stretch = false): int|null +{ + if (!is_dir($dst_path)) { + if (!mkdir($dst_path, 0777, true)) { + return -3; + } + } + + if ($err = resize_image($src_path, "$dst_path/3x", $max_width, $max_height, $set_format, $stretch)) { + array_map("unlink", glob("$dst_path/*.*")); + rmdir($dst_path); + return $err; + } + + if ($err = resize_image($src_path, "$dst_path/2x", round($max_width / 2), round($max_height / 2), $set_format, $stretch)) { + array_map("unlink", glob("$dst_path/*.*")); + rmdir($dst_path); + return $err; + } + + if ($err = resize_image($src_path, "$dst_path/1x", round($max_width / 4), round($max_height / 4), $set_format, $stretch)) { + array_map("unlink", glob("$dst_path/*.*")); + rmdir($dst_path); + return $err; + } + + return null; +} + +function get_file_extension(string $path): string|null +{ + if ($file = getimagesize($path)) { + return image_type_to_extension(intval($file[2]), false); + } + + return null; +} + +function does_file_meet_requirements(string $path, int $max_width, int $max_height): array +{ + $file = getimagesize($path); + if (!$file) { + return [false, null]; + } + + return [$file[0] <= $max_width && $file[1] <= $max_height, image_type_to_extension(intval($file[2]), false)]; +}
\ No newline at end of file diff --git a/lib/partials.php b/lib/partials.php new file mode 100644 index 0000000..760923a --- /dev/null +++ b/lib/partials.php @@ -0,0 +1,179 @@ +<?php +function html_navigation_bar() +{ + include_once "config.php"; + + echo '' ?> + <section class="navbar"> + <a href="/" class="brand" style="color:black;text-decoration:none;"> + <img src="/static/img/brand/mini.webp" alt=""> + <h2 style="margin-left:8px;font-size:24px;"><b><?php echo INSTANCE_NAME ?></b></h2> + </a> + <div class="links"> + <a href="/emotes" class="button">Emotes</a> + + <?php if (EMOTESET_PUBLIC_LIST): ?> + <a href="/emotesets.php" class="button">Emotesets</a> + <?php endif; ?> + + <?php if (ACCOUNT_PUBLIC_LIST): ?> + <a href="/users.php" class="button">Users</a> + <?php endif; ?> + + <?php if (EMOTE_UPLOAD && (ANONYMOUS_UPLOAD || (isset($_SESSION["user_role"]) && $_SESSION["user_role"]["permission_upload"]))) { + echo '<a href="/emotes/upload.php" class="button">Upload</a>'; + } ?> + <a href="/account" class="button">Account</a> + <?php + if (isset($_SESSION["user_id"])) { + $db = new PDO(DB_URL, DB_USER, DB_PASS); + + // getting inbox + $stmt = $db->prepare("SELECT COUNT(*) FROM inbox_messages WHERE recipient_id = ? AND has_read = false"); + $stmt->execute([$_SESSION["user_id"]]); + $unread_count = intval($stmt->fetch()[0]); + echo '' ?> + <a href="/inbox.php" class="button"> + Inbox <?php echo $unread_count > 0 ? "($unread_count)" : "" ?> + </a> + <?php ; + $stmt = null; + + if (isset($_SESSION["user_role"])) { + if (REPORTS_ENABLE && $_SESSION["user_role"]["permission_report"]) { + // getting reports + $stmt = $db->prepare("SELECT COUNT(*) FROM reports WHERE sender_id = ? AND resolved_by IS NULL"); + $stmt->execute([$_SESSION["user_id"]]); + $unread_count = intval($stmt->fetch()[0]); + + echo '' ?> + <a href="/report/list.php" class="button"> + Reports <?php echo $unread_count > 0 ? "($unread_count)" : "" ?> + </a> + <?php ; + } + + if (MOD_SYSTEM_DASHBOARD && ($_SESSION["user_role"]["permission_approve_emotes"] || $_SESSION["user_role"]["permission_report_review"])) { + $system_count = 0; + + if ($_SESSION["user_role"]["permission_approve_emotes"] && MOD_EMOTES_APPROVE) { + $system_count += intval($db->query("SELECT COUNT(*) FROM emotes WHERE visibility = 2")->fetch()[0]); + } + + if ($_SESSION["user_role"]["permission_report_review"]) { + $system_count += intval($db->query("SELECT COUNT(*) FROM reports WHERE resolved_by IS NULL")->fetch()[0]); + } + + echo '<a href="/system" class="button">System'; + if ($system_count > 0) { + echo " ($system_count)"; + } + echo '</a>'; + } + } + + $stmt = null; + $db = null; + } + ?> + </div> + <?php if (isset($_SESSION["user_id"])): ?> + <div class="flex items-bottom small-gap" style="margin-left: auto;"> + <?php if (isset($_SESSION["user_emote_sets"])): ?> + <form action="/account/change_emoteset.php" method="POST" id="form-change-emoteset"> + <input type="text" name="redirect" value="<?php echo $_SERVER["REQUEST_URI"] ?>" style="display: none;"> + <div class="row small-gap"> + <label for="id">Current emote set: </label> + <select name="id" onchange="send_change_emoteset(event)"> + <?php + foreach ($_SESSION["user_emote_sets"] as $es) { + echo '<option value="' . $es["id"] . '">' . $es["name"] . '</option>'; + } + ?> + </select> + </div> + </form> + <script> + function send_change_emoteset(e) { + document.getElementById("form-change-emoteset").submit(); + } + </script> + <?php endif; ?> + + <a href="/users.php?id=<?php echo $_SESSION["user_id"] ?>" class="flex items-bottom small-gap"> + Signed in as <?php echo $_SESSION["user_name"] ?> + <?php + echo '<img src="/static/'; + if (is_dir($_SERVER['DOCUMENT_ROOT'] . "/static/userdata/avatars/" . $_SESSION["user_id"])) { + echo 'userdata/avatars/' . $_SESSION["user_id"] . "/1x.webp"; + } else { + echo 'img/defaults/profile_picture.png'; + } + echo '" width="24" height="24" />'; + ?> + </a> + <a href="/account/signout.php?local"> + <img src="/static/img/icons/door_out.png" alt="[ Log out ]" title="Log out"> + </a> + </div> + <?php endif; ?> + </section> + <?php ; +} + +function html_navigation_search() +{ + echo '' ?> + <section class="box"> + <div class="box navtab"> + Search... + </div> + <div class="box content"> + <form action="<?php echo $_SERVER["REQUEST_URI"] ?>" method="GET"> + <input type="text" name="q" style="padding:4px;" value="<?php echo $_GET["q"] ?? "" ?>"><br> + <?php + if (str_starts_with($_SERVER["REQUEST_URI"], "/emotes")) { + ?> + <label for="sort_by">Sort by</label> + <select name="sort_by"> + <option value="high_ratings" <?php echo ($_GET["sort_by"] ?? "") == "high_ratings" ? "selected" : "" ?>> + High ratings</option> + <option value="low_ratings" <?php echo ($_GET["sort_by"] ?? "") == "low_ratings" ? "selected" : "" ?>>Low + ratings</option> + <option value="recent" <?php echo ($_GET["sort_by"] ?? "") == "recent" ? "selected" : "" ?>>Recent + </option> + <option value="oldest" <?php echo ($_GET["sort_by"] ?? "") == "oldest" ? "selected" : "" ?>>Oldest + </option> + </select> + <?php + } + ?> + <button type="submit" style="width:100%;margin-top:6px;">Find</button> + </form> + </div> + </section> + <?php ; +} + +function html_pagination(int $total_pages, int $current_page, string $redirect) +{ + if (str_contains($redirect, "?")) { + $redirect .= "&p="; + } else { + $redirect .= "?p="; + } + + if ($total_pages > 1) { + echo '' ?> + <div class="pagination"> + <?php if ($current_page > 1): ?> + <a href="<?php echo $redirect . ($current_page - 1) ?>">[ prev ]</a> + <?php endif; ?> + <?php if ($current_page < $total_pages): ?> + <a href="<?php echo $redirect . ($current_page + 1) ?>">[ next ]</a> + <?php endif; ?> + + </div> + <?php ; + } +}
\ No newline at end of file diff --git a/lib/user.php b/lib/user.php new file mode 100644 index 0000000..d22eeb4 --- /dev/null +++ b/lib/user.php @@ -0,0 +1,102 @@ +<?php +class Badge +{ + public string $id; + + public static function from_array(array $arr, string $prefix = ""): Badge|null + { + if (!empty($prefix)) { + $prefix .= "_"; + } + if (!isset($arr["{$prefix}badge_id"])) { + return null; + } + + $b = new Badge(); + $b->id = $arr["{$prefix}badge_id"]; + + return $b; + } +} + +class Role +{ + public string $name; + public Badge|null $badge; + + public static function from_array(array $arr): Role|null + { + if (!isset($arr["role_name"])) { + return null; + } + + $r = new Role(); + + $r->name = $arr["role_name"]; + $r->badge = Badge::from_array($arr, "role"); + + return $r; + } +} + +class User +{ + public string $id; + public string $username; + public int $joined_at; + public int $last_active_at; + + public Badge|null $custom_badge; + + public Role|null $role; + + public bool $private_profile; + + public static function from_array(array $arr): User + { + $u = new User(); + + $u->id = $arr["id"]; + $u->username = $arr["username"]; + $u->joined_at = strtotime($arr["joined_at"] ?? "0"); + $u->last_active_at = strtotime($arr["last_active_at"] ?? "0"); + + $u->private_profile = $row["private_profile"] ?? false; + + $u->custom_badge = Badge::from_array($arr, "custom"); + + $u->role = Role::from_array($arr); + + return $u; + } + + public static function get_user_by_id(PDO &$db, string $user_id): User|null + { + $stmt = $db->prepare("SELECT + u.id, + u.username, + u.joined_at, + u.last_active_at, + + up.private_profile, + r.name AS role_name, + r.badge_id AS role_badge_id, + ub.badge_id AS custom_badge_id + FROM users u + INNER JOIN user_preferences up ON up.id = u.id + LEFT JOIN role_assigns ra ON ra.user_id = u.id + LEFT JOIN roles r ON r.id = ra.role_id + LEFT JOIN user_badges ub ON ub.user_id = u.id + WHERE u.id = ? + "); + $stmt->execute([$user_id]); + + $u = null; + + if ($uploader_row = $stmt->fetch()) { + $u = User::from_array($uploader_row); + } + + return $u; + } +}
\ No newline at end of file diff --git a/lib/utils.php b/lib/utils.php new file mode 100644 index 0000000..87d96c6 --- /dev/null +++ b/lib/utils.php @@ -0,0 +1,68 @@ +<?php +function json_response(mixed $response, int $status = 200) +{ + http_response_code($status); + header("Content-Type: application/json"); + echo json_encode($response); +} + +function generate_random_string(int $length): string +{ + $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + $output = ""; + + for ($i = 0; $i < $length; $i++) { + $charindex = random_int(0, strlen($chars) - 1); + $output .= $chars[$charindex]; + } + + return $output; +} + +function str_safe(string $s, int|null $max_length, bool $remove_new_lines = true): string +{ + $output = $s; + + if ($remove_new_lines) { + $output = str_replace(PHP_EOL, "", $output); + } + + $output = htmlspecialchars($output); + $output = strip_tags($output); + + if ($max_length) { + $output = substr($output, 0, $max_length); + } + + $output = trim($output); + + return $output; +} + +function format_timestamp(int $timestamp_secs) +{ + $days = (int) floor($timestamp_secs / (60.0 * 60.0 * 24.0)); + $hours = (int) floor(round($timestamp_secs / (60 * 60)) % 24); + $minutes = (int) floor(round($timestamp_secs % (60 * 60)) / 60); + $seconds = (int) floor($timestamp_secs % 60); + + if ($days == 0 && $hours == 0 && $minutes == 0) { + return "$seconds second" . ($seconds > 1 ? "s" : ""); + } else if ($days == 0 && $hours == 0) { + return "$minutes minute" . ($minutes > 1 ? "s" : ""); + } else if ($days == 0) { + return "$hours hour" . ($hours > 1 ? "s" : ""); + } else { + return "$days day" . ($days > 1 ? "s" : ""); + } +} + +function clamp(int $current, int $min, int $max): int +{ + return max($min, min($max, $current)); +} + +function in_range(float $value, float $min, float $max): bool +{ + return $min <= $value && $value <= $max; +}
\ No newline at end of file diff --git a/lib/version.php b/lib/version.php new file mode 100644 index 0000000..d666783 --- /dev/null +++ b/lib/version.php @@ -0,0 +1,11 @@ +<?php +// please leave it as it is ;) +define("TINYEMOTES_NAME", "TinyEmotes"); +define("TINYEMOTES_VERSION", "0.3.1"); +define("TINYEMOTES_LINK", "https://git.ilt.su/tiny/emotes.git"); + +if ($s = file_get_contents("{$_SERVER['DOCUMENT_ROOT']}/.git/refs/heads/master")) { + define("TINYEMOTES_COMMIT", $s); +} else { + define("TINYEMOTES_COMMIT", null); +}
\ No newline at end of file |
