diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/admin.php | 82 | ||||
-rw-r--r-- | lib/ansi.php | 32 | ||||
-rw-r--r-- | lib/cli.php | 8 | ||||
-rw-r--r-- | lib/config.php | 46 | ||||
-rw-r--r-- | lib/ext/MyParsedown.php (renamed from lib/MyParsedown.php) | 10 | ||||
-rw-r--r-- | lib/markup.php | 2 | ||||
-rw-r--r-- | lib/pages.php | 58 | ||||
-rw-r--r-- | lib/posts.php | 306 | ||||
-rw-r--r-- | lib/stored_config.php | 14 | ||||
-rw-r--r-- | lib/tags.php | 87 | ||||
-rw-r--r-- | lib/themes.php | 41 | ||||
-rw-r--r-- | lib/uploads.php | 215 |
12 files changed, 681 insertions, 220 deletions
diff --git a/lib/admin.php b/lib/admin.php index 91aa620..116ee3c 100644 --- a/lib/admin.php +++ b/lib/admin.php @@ -1,55 +1,51 @@ <?php -class admin { +require_once 'lib/stored_config.php'; - const SESSION_TIMEOUT = 86400 * 14; - const COOKIE_NAME = 'admin_key'; +const ADMIN_SESSION_TIMEOUT = 86400 * 14; +const ADMIN_COOKIE_NAME = 'admin_key'; - protected static ?bool $isAdmin = null; - - public static function isAdmin(): bool { - if (is_null(self::$isAdmin)) - self::$isAdmin = self::_verifyKey(); - return self::$isAdmin; - } - - protected static function _verifyKey(): bool { - if (isset($_COOKIE[self::COOKIE_NAME])) { - $cookie = (string)$_COOKIE[self::COOKIE_NAME]; - if ($cookie !== self::getKey()) - self::unsetCookie(); - return true; - } - return false; - } +function is_admin(): bool { + static $is_admin = null; + if (is_null($is_admin)) + $is_admin = _admin_verify_key(); + return $is_admin; +} - public static function checkPassword(string $pwd): bool { - return salt_password($pwd) === config::get('admin_pwd'); +function _admin_verify_key(): bool { + if (isset($_COOKIE[ADMIN_COOKIE_NAME])) { + $cookie = (string)$_COOKIE[ADMIN_COOKIE_NAME]; + if ($cookie !== _admin_get_key()) + admin_unset_cookie(); + return true; } + return false; +} - protected static function getKey(): string { - global $config; - $admin_pwd_hash = config::get('admin_pwd'); - return salt_password("$admin_pwd_hash|{$_SERVER['REMOTE_ADDR']}"); - } +function admin_check_password(string $pwd): bool { + return salt_password($pwd) === scGet('admin_pwd'); +} - public static function setCookie(): void { - global $config; - $key = self::getKey(); - setcookie(self::COOKIE_NAME, $key, time() + self::SESSION_TIMEOUT, '/', $config['cookie_host']); - } +function _admin_get_key(): string { + $admin_pwd_hash = scGet('admin_pwd'); + return salt_password("$admin_pwd_hash|{$_SERVER['REMOTE_ADDR']}"); +} - public static function unsetCookie(): void { - global $config; - setcookie(self::COOKIE_NAME, '', 1, '/', $config['cookie_host']); - } +function admin_set_cookie(): void { + global $config; + $key = _admin_get_key(); + setcookie(ADMIN_COOKIE_NAME, $key, time() + ADMIN_SESSION_TIMEOUT, '/', $config['cookie_host']); +} - public static function logAuth(): void { - getDb()->insert('admin_log', [ - 'ts' => time(), - 'ip' => ip2ulong($_SERVER['REMOTE_ADDR']), - 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '', - ]); - } +function admin_unset_cookie(): void { + global $config; + setcookie(ADMIN_COOKIE_NAME, '', 1, '/', $config['cookie_host']); +} +function admin_log_auth(): void { + DB()->insert('admin_log', [ + 'ts' => time(), + 'ip' => ip2ulong($_SERVER['REMOTE_ADDR']), + 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '', + ]); } diff --git a/lib/ansi.php b/lib/ansi.php new file mode 100644 index 0000000..9e0a425 --- /dev/null +++ b/lib/ansi.php @@ -0,0 +1,32 @@ +<?php + +enum AnsiColor: int { + case BLACK = 0; + case RED = 1; + case GREEN = 2; + case YELLOW = 3; + case BLUE = 4; + case MAGENTA = 5; + case CYAN = 6; + case WHITE = 7; +} + +function ansi(string $text, + ?AnsiColor $fg = null, + ?AnsiColor $bg = null, + bool $bold = false, + bool $fg_bright = false, + bool $bg_bright = false): string { + $codes = []; + if (!is_null($fg)) + $codes[] = $fg->value + ($fg_bright ? 90 : 30); + if (!is_null($bg)) + $codes[] = $bg->value + ($bg_bright ? 100 : 40); + if ($bold) + $codes[] = 1; + + if (empty($codes)) + return $text; + + return "\033[".implode(';', $codes)."m".$text."\033[0m"; +}
\ No newline at end of file diff --git a/lib/cli.php b/lib/cli.php index a860871..910f4c1 100644 --- a/lib/cli.php +++ b/lib/cli.php @@ -4,7 +4,7 @@ class cli { protected ?array $commandsCache = null; - public function __construct( + function __construct( protected string $ns ) {} @@ -21,7 +21,7 @@ class cli { exit(is_null($error) ? 0 : 1); } - public function getCommands(): array { + function getCommands(): array { if (is_null($this->commandsCache)) { $funcs = array_filter(get_defined_functions()['user'], fn(string $f) => str_starts_with($f, $this->ns)); $funcs = array_map(fn(string $f) => str_replace('_', '-', substr($f, strlen($this->ns.'\\'))), $funcs); @@ -30,10 +30,10 @@ class cli { return $this->commandsCache; } - public function run(): void { + function run(): void { global $argv, $argc; - if (PHP_SAPI != 'cli') + if (!is_cli()) cli::die('SAPI != cli'); if ($argc < 2) diff --git a/lib/config.php b/lib/config.php deleted file mode 100644 index bb7e5ca..0000000 --- a/lib/config.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php - -class config { - - public static function get(string $key) { - $db = getDb(); - $q = $db->query("SELECT value FROM config WHERE name=?", $key); - if (!$db->numRows($q)) - return null; - return $db->result($q); - } - - public static function mget($keys) { - $map = []; - foreach ($keys as $key) { - $map[$key] = null; - } - - $db = getDb(); - $keys = array_map(fn($s) => $db->escape($s), $keys); - - $q = $db->query("SELECT * FROM config WHERE name IN('".implode("','", $keys)."')"); - while ($row = $db->fetch($q)) - $map[$row['name']] = $row['value']; - - return $map; - } - - public static function set($key, $value) { - $db = getDb(); - return $db->query("REPLACE INTO config (name, value) VALUES (?, ?)", $key, $value); - } - - public static function mset($map) { - $rows = []; - foreach ($map as $name => $value) { - $rows[] = [ - 'name' => $name, - 'value' => $value - ]; - } - $db = getDb(); - return $db->multipleReplace('config', $rows); - } - -} diff --git a/lib/MyParsedown.php b/lib/ext/MyParsedown.php index 85ed9c4..71dfa7f 100644 --- a/lib/MyParsedown.php +++ b/lib/ext/MyParsedown.php @@ -2,8 +2,8 @@ class MyParsedown extends ParsedownExtended { - public function __construct( - ?array $opts = null, + function __construct( + ?array $opts = null, protected bool $useImagePreviews = false ) { $parsedown_opts = [ @@ -25,7 +25,7 @@ class MyParsedown extends ParsedownExtended { protected function inlineFileAttach($excerpt) { if (preg_match('/^{fileAttach:([\w]{8})}{\/fileAttach}/', $excerpt['text'], $matches)) { $random_id = $matches[1]; - $upload = uploads::getByRandomId($random_id); + $upload = uploads::getUploadByRandomId($random_id); $result = [ 'extent' => strlen($matches[0]), 'element' => [ @@ -73,7 +73,7 @@ class MyParsedown extends ParsedownExtended { } } - $image = uploads::getByRandomId($random_id); + $image = uploads::getUploadByRandomId($random_id); $result = [ 'extent' => strlen($matches[0]), 'element' => [ @@ -141,7 +141,7 @@ class MyParsedown extends ParsedownExtended { } } - $video = uploads::getByRandomId($random_id); + $video = uploads::getUploadByRandomId($random_id); $result = [ 'extent' => strlen($matches[0]), 'element' => [ diff --git a/lib/markup.php b/lib/markup.php index f6ddd0f..9872dae 100644 --- a/lib/markup.php +++ b/lib/markup.php @@ -1,5 +1,7 @@ <?php +require_once 'lib/ext/MyParsedown.php'; + class markup { public static function markdownToHtml(string $md, bool $use_image_previews = true): string { diff --git a/lib/pages.php b/lib/pages.php index 281ee52..8524cf3 100644 --- a/lib/pages.php +++ b/lib/pages.php @@ -1,9 +1,51 @@ <?php +class Page extends model { + + const DB_TABLE = 'pages'; + const DB_KEY = 'short_name'; + + public string $title; + public string $md; + public string $html; + public int $ts; + public int $updateTs; + public bool $visible; + public string $shortName; + + function edit(array $fields) { + $fields['update_ts'] = time(); + if ($fields['md'] != $this->md) + $fields['html'] = markup::markdownToHtml($fields['md']); + parent::edit($fields); + } + + function isUpdated(): bool { + return $this->updateTs && $this->updateTs != $this->ts; + } + + function getHtml(bool $is_retina, string $user_theme): string { + $html = $this->html; + $html = markup::htmlImagesFix($html, $is_retina, $user_theme); + return $html; + } + + function getUrl(): string { + return "/{$this->shortName}/"; + } + + function updateHtml(): void { + $html = markup::markdownToHtml($this->md); + $this->html = $html; + DB()->query("UPDATE pages SET html=? WHERE short_name=?", $html, $this->shortName); + } + +} + class pages { - public static function add(array $data): ?int { - $db = getDb(); + static function add(array $data): ?int { + $db = DB(); $data['ts'] = time(); $data['html'] = markup::markdownToHtml($data['md']); if (!$db->insert('pages', $data)) @@ -11,12 +53,12 @@ class pages { return $db->insertId(); } - public static function delete(Page $page): void { - getDb()->query("DELETE FROM pages WHERE short_name=?", $page->shortName); + static function delete(Page $page): void { + DB()->query("DELETE FROM pages WHERE short_name=?", $page->shortName); } - public static function getPageByName(string $short_name): ?Page { - $db = getDb(); + static function getByName(string $short_name): ?Page { + $db = DB(); $q = $db->query("SELECT * FROM pages WHERE short_name=?", $short_name); return $db->numRows($q) ? new Page($db->fetch($q)) : null; } @@ -24,8 +66,8 @@ class pages { /** * @return Page[] */ - public static function getAll(): array { - $db = getDb(); + static function getAll(): array { + $db = DB(); return array_map('Page::create_instance', $db->fetchAll($db->query("SELECT * FROM pages"))); } diff --git a/lib/posts.php b/lib/posts.php index 1537749..8ffb92b 100644 --- a/lib/posts.php +++ b/lib/posts.php @@ -1,9 +1,202 @@ <?php +class Post extends model { + + const DB_TABLE = 'posts'; + + public int $id; + public string $title; + public string $md; + public string $html; + public string $tocHtml; + public string $text; + public int $ts; + public int $updateTs; + public bool $visible; + public bool $toc; + public string $shortName; + + function edit(array $fields) { + $cur_ts = time(); + if (!$this->visible && $fields['visible']) + $fields['ts'] = $cur_ts; + + $fields['update_ts'] = $cur_ts; + + if ($fields['md'] != $this->md) { + $fields['html'] = markup::markdownToHtml($fields['md']); + $fields['text'] = markup::htmlToText($fields['html']); + } + + if ((isset($fields['toc']) && $fields['toc']) || $this->toc) { + $fields['toc_html'] = markup::toc($fields['md']); + } + + parent::edit($fields); + $this->updateImagePreviews(); + } + + function updateHtml() { + $html = markup::markdownToHtml($this->md); + $this->html = $html; + + DB()->query("UPDATE posts SET html=? WHERE id=?", $html, $this->id); + } + + function updateText() { + $html = markup::markdownToHtml($this->md); + $text = markup::htmlToText($html); + $this->text = $text; + + DB()->query("UPDATE posts SET text=? WHERE id=?", $text, $this->id); + } + + function getDescriptionPreview(int $len): string { + if (mb_strlen($this->text) >= $len) + return mb_substr($this->text, 0, $len-3).'...'; + return $this->text; + } + + function getFirstImage(): ?Upload { + if (!preg_match('/\{image:([\w]{8})/', $this->md, $match)) + return null; + return uploads::getUploadByRandomId($match[1]); + } + + function getUrl(): string { + return $this->shortName != '' ? "/{$this->shortName}/" : "/{$this->id}/"; + } + + function getDate(): string { + return date('j M', $this->ts); + } + + function getYear(): int { + return (int)date('Y', $this->ts); + } + + function getFullDate(): string { + return date('j F Y', $this->ts); + } + + function getUpdateDate(): string { + return date('j M', $this->updateTs); + } + + function getFullUpdateDate(): string { + return date('j F Y', $this->updateTs); + } + + function getHtml(bool $is_retina, string $theme): string { + $html = $this->html; + $html = markup::htmlImagesFix($html, $is_retina, $theme); + return $html; + } + + function getToc(): ?string { + return $this->toc ? $this->tocHtml : null; + } + + function isUpdated(): bool { + return $this->updateTs && $this->updateTs != $this->ts; + } + + /** + * @return Tag[] + */ + function getTags(): array { + $db = DB(); + $q = $db->query("SELECT tags.* FROM posts_tags + LEFT JOIN tags ON tags.id=posts_tags.tag_id + WHERE posts_tags.post_id=? + ORDER BY posts_tags.tag_id", $this->id); + return array_map('Tag::create_instance', $db->fetchAll($q)); + } + + /** + * @return int[] + */ + function getTagIds(): array { + $ids = []; + $db = DB(); + $q = $db->query("SELECT tag_id FROM posts_tags WHERE post_id=? ORDER BY tag_id", $this->id); + while ($row = $db->fetch($q)) { + $ids[] = (int)$row['tag_id']; + } + return $ids; + } + + function setTagIds(array $new_tag_ids) { + $cur_tag_ids = $this->getTagIds(); + $add_tag_ids = array_diff($new_tag_ids, $cur_tag_ids); + $rm_tag_ids = array_diff($cur_tag_ids, $new_tag_ids); + + $db = DB(); + if (!empty($add_tag_ids)) { + $rows = []; + foreach ($add_tag_ids as $id) + $rows[] = ['post_id' => $this->id, 'tag_id' => $id]; + $db->multipleInsert('posts_tags', $rows); + } + + if (!empty($rm_tag_ids)) + $db->query("DELETE FROM posts_tags WHERE post_id=? AND tag_id IN(".implode(',', $rm_tag_ids).")", $this->id); + + $upd_tag_ids = array_merge($new_tag_ids, $rm_tag_ids); + $upd_tag_ids = array_unique($upd_tag_ids); + foreach ($upd_tag_ids as $id) + tags::recountTagPosts($id); + } + + /** + * @param bool $update Whether to overwrite preview if already exists + * @return int + * @throws Exception + */ + function updateImagePreviews(bool $update = false): int { + $images = []; + if (!preg_match_all('/\{image:([\w]{8}),(.*?)}/', $this->md, $matches)) + return 0; + + for ($i = 0; $i < count($matches[0]); $i++) { + $id = $matches[1][$i]; + $w = $h = null; + $opts = explode(',', $matches[2][$i]); + foreach ($opts as $opt) { + if (str_contains($opt, '=')) { + list($k, $v) = explode('=', $opt); + if ($k == 'w') + $w = (int)$v; + else if ($k == 'h') + $h = (int)$v; + } + } + $images[$id][] = [$w, $h]; + } + + if (empty($images)) + return 0; + + $images_affected = 0; + $uploads = uploads::getUploadsByRandomId(array_keys($images), true); + foreach ($uploads as $u) { + foreach ($images[$u->randomId] as $s) { + list($w, $h) = $s; + list($w, $h) = $u->getImagePreviewSize($w, $h); + if ($u->createImagePreview($w, $h, $update, $u->imageMayHaveAlphaChannel())) + $images_affected++; + } + } + + return $images_affected; + } + +} + class posts { - public static function getPostsCount(bool $include_hidden = false): int { - $db = getDb(); + static function getCount(bool $include_hidden = false): int { + $db = DB(); $sql = "SELECT COUNT(*) FROM posts"; if (!$include_hidden) { $sql .= " WHERE visible=1"; @@ -11,14 +204,14 @@ class posts { return (int)$db->result($db->query($sql)); } - public static function getPostsCountByTagId(int $tag_id, bool $include_hidden = false): int { - $db = getDb(); + static function getCountByTagId(int $tag_id, bool $include_hidden = false): int { + $db = DB(); if ($include_hidden) { $sql = "SELECT COUNT(*) FROM posts_tags WHERE tag_id=?"; } else { $sql = "SELECT COUNT(*) FROM posts_tags - LEFT JOIN posts ON posts.id=posts_tags.post_id - WHERE posts_tags.tag_id=? AND posts.visible=1"; + LEFT JOIN posts ON posts.id=posts_tags.post_id + WHERE posts_tags.tag_id=? AND posts.visible=1"; } return (int)$db->result($db->query($sql, $tag_id)); } @@ -26,8 +219,8 @@ class posts { /** * @return Post[] */ - public static function getPosts(int $offset = 0, int $count = -1, bool $include_hidden = false): array { - $db = getDb(); + static function getList(int $offset = 0, int $count = -1, bool $include_hidden = false): array { + $db = DB(); $sql = "SELECT * FROM posts"; if (!$include_hidden) $sql .= " WHERE visible=1"; @@ -41,11 +234,11 @@ class posts { /** * @return Post[] */ - public static function getPostsByTagId(int $tag_id, bool $include_hidden = false): array { - $db = getDb(); + static function getPostsByTagId(int $tag_id, bool $include_hidden = false): array { + $db = DB(); $sql = "SELECT posts.* FROM posts_tags - LEFT JOIN posts ON posts.id=posts_tags.post_id - WHERE posts_tags.tag_id=?"; + LEFT JOIN posts ON posts.id=posts_tags.post_id + WHERE posts_tags.tag_id=?"; if (!$include_hidden) $sql .= " AND posts.visible=1"; $sql .= " ORDER BY posts.ts DESC"; @@ -53,8 +246,8 @@ class posts { return array_map('Post::create_instance', $db->fetchAll($q)); } - public static function add(array $data = []): int|bool { - $db = getDb(); + static function add(array $data = []): int|bool { + $db = DB(); $html = \markup::markdownToHtml($data['md']); $text = \markup::htmlToText($html); @@ -70,64 +263,41 @@ class posts { $id = $db->insertId(); - $post = posts::get($id); + $post = self::get($id); $post->updateImagePreviews(); return $id; } - public static function delete(Post $post): void { + static function delete(Post $post): void { $tags = $post->getTags(); - $db = getDb(); + $db = DB(); $db->query("DELETE FROM posts WHERE id=?", $post->id); $db->query("DELETE FROM posts_tags WHERE post_id=?", $post->id); foreach ($tags as $tag) - self::recountPostsWithTag($tag->id); + tags::recountTagPosts($tag->id); } - public static function getTagIds(array $tags): array { - $found_tags = []; - $map = []; - - $db = getDb(); - $q = $db->query("SELECT id, tag FROM tags - WHERE tag IN ('".implode("','", array_map(function($tag) use ($db) { return $db->escape($tag); }, $tags))."')"); - while ($row = $db->fetch($q)) { - $found_tags[] = $row['tag']; - $map[$row['tag']] = (int)$row['id']; - } - - $notfound_tags = array_diff($tags, $found_tags); - if (!empty($notfound_tags)) { - foreach ($notfound_tags as $tag) { - $db->insert('tags', ['tag' => $tag]); - $map[$tag] = $db->insertId(); - } - } - - return $map; - } - - public static function get(int $id): ?Post { - $db = getDb(); + static function get(int $id): ?Post { + $db = DB(); $q = $db->query("SELECT * FROM posts WHERE id=?", $id); return $db->numRows($q) ? new Post($db->fetch($q)) : null; } - public static function getPostByName(string $short_name): ?Post { - $db = getDb(); + static function getByName(string $short_name): ?Post { + $db = DB(); $q = $db->query("SELECT * FROM posts WHERE short_name=?", $short_name); return $db->numRows($q) ? new Post($db->fetch($q)) : null; } - public static function getPostsById(array $ids, bool $flat = false): array { + static function getPostsById(array $ids, bool $flat = false): array { if (empty($ids)) { return []; } - $db = getDb(); + $db = DB(); $posts = array_fill_keys($ids, null); $q = $db->query("SELECT * FROM posts WHERE id IN(".implode(',', $ids).")"); @@ -148,44 +318,4 @@ class posts { return $posts; } - public static function getAllTags(bool $include_hidden = false): array { - $db = getDb(); - $field = $include_hidden ? 'posts_count' : 'visible_posts_count'; - $q = $db->query("SELECT * FROM tags WHERE $field > 0 ORDER BY $field DESC, tag"); - return array_map('Tag::create_instance', $db->fetchAll($q)); - } - - public static function getTag(string $tag): ?Tag { - $db = getDb(); - $q = $db->query("SELECT * FROM tags WHERE tag=?", $tag); - return $db->numRows($q) ? new Tag($db->fetch($q)) : null; - } - - /** - * @param int $tag_id - */ - public static function recountPostsWithTag($tag_id) { - $db = getDb(); - $count = $db->result($db->query("SELECT COUNT(*) FROM posts_tags WHERE tag_id=?", $tag_id)); - $vis_count = $db->result($db->query("SELECT COUNT(*) FROM posts_tags - LEFT JOIN posts ON posts.id=posts_tags.post_id - WHERE posts_tags.tag_id=? AND posts.visible=1", $tag_id)); - $db->query("UPDATE tags SET posts_count=?, visible_posts_count=? WHERE id=?", - $count, $vis_count, $tag_id); - } - - public static function splitStringToTags(string $tags): array { - $tags = trim($tags); - if ($tags == '') { - return []; - } - - $tags = preg_split('/,\s+/', $tags); - $tags = array_filter($tags, function($tag) { return trim($tag) != ''; }); - $tags = array_map('trim', $tags); - $tags = array_map('mb_strtolower', $tags); - - return $tags; - } - -} +}
\ No newline at end of file diff --git a/lib/stored_config.php b/lib/stored_config.php new file mode 100644 index 0000000..2e7dc8a --- /dev/null +++ b/lib/stored_config.php @@ -0,0 +1,14 @@ +<?php + +function scGet(string $key) { + $db = DB(); + $q = $db->query("SELECT value FROM config WHERE name=?", $key); + if (!$db->numRows($q)) + return null; + return $db->result($q); +} + +function scSet($key, $value) { + $db = DB(); + return $db->query("REPLACE INTO config (name, value) VALUES (?, ?)", $key, $value); +} diff --git a/lib/tags.php b/lib/tags.php new file mode 100644 index 0000000..ecc9e5a --- /dev/null +++ b/lib/tags.php @@ -0,0 +1,87 @@ +<?php + +class Tag extends model implements Stringable { + + const DB_TABLE = 'tags'; + + public int $id; + public string $tag; + public int $postsCount; + public int $visiblePostsCount; + + function getUrl(): string { + return '/'.$this->tag.'/'; + } + + function getPostsCount(bool $is_admin): int { + return $is_admin ? $this->postsCount : $this->visiblePostsCount; + } + + function __toString(): string { + return $this->tag; + } + +} + +class tags { + + static function getAll(bool $include_hidden = false): array { + $db = DB(); + $field = $include_hidden ? 'posts_count' : 'visible_posts_count'; + $q = $db->query("SELECT * FROM tags WHERE $field > 0 ORDER BY $field DESC, tag"); + return array_map('Tag::create_instance', $db->fetchAll($q)); + } + + static function get(string $tag): ?Tag { + $db = DB(); + $q = $db->query("SELECT * FROM tags WHERE tag=?", $tag); + return $db->numRows($q) ? new Tag($db->fetch($q)) : null; + } + + static function recountTagPosts(int $tag_id): void { + $db = DB(); + $count = $db->result($db->query("SELECT COUNT(*) FROM posts_tags WHERE tag_id=?", $tag_id)); + $vis_count = $db->result($db->query("SELECT COUNT(*) FROM posts_tags + LEFT JOIN posts ON posts.id=posts_tags.post_id + WHERE posts_tags.tag_id=? AND posts.visible=1", $tag_id)); + $db->query("UPDATE tags SET posts_count=?, visible_posts_count=? WHERE id=?", + $count, $vis_count, $tag_id); + } + + static function splitString(string $tags): array { + $tags = trim($tags); + if ($tags == '') + return []; + $tags = preg_split('/,\s+/', $tags); + $tags = array_filter($tags, static function($tag) { return trim($tag) != ''; }); + $tags = array_map('trim', $tags); + $tags = array_map('mb_strtolower', $tags); + + return $tags; + } + + static function getTags(array $tags): array { + $found_tags = []; + $map = []; + + $db = DB(); + $q = $db->query("SELECT id, tag FROM tags + WHERE tag IN ('".implode("','", array_map(fn($tag) => $db->escape($tag), $tags))."')"); + + while ($row = $db->fetch($q)) { + $found_tags[] = $row['tag']; + $map[$row['tag']] = (int)$row['id']; + } + + $notfound_tags = array_diff($tags, $found_tags); + if (!empty($notfound_tags)) { + foreach ($notfound_tags as $tag) { + $db->insert('tags', ['tag' => $tag]); + $map[$tag] = $db->insertId(); + } + } + + return $map; + } + +}
\ No newline at end of file diff --git a/lib/themes.php b/lib/themes.php new file mode 100644 index 0000000..f9b9857 --- /dev/null +++ b/lib/themes.php @@ -0,0 +1,41 @@ +<?php + +const THEMES = [ + 'dark' => [ + 'bg' => 0x222222, + // 'alpha' => 0x303132, + 'alpha' => 0x222222, + ], + 'light' => [ + 'bg' => 0xffffff, + // 'alpha' => 0xf2f2f2, + 'alpha' => 0xffffff, + ] +]; + + +function getThemes(): array { + return array_keys(THEMES); +} + +function themeExists(string $name): bool { + return array_key_exists($name, THEMES); +} + +function getThemeAlphaColorAsRGB(string $name): array { + $color = THEMES[$name]['alpha']; + $r = ($color >> 16) & 0xff; + $g = ($color >> 8) & 0xff; + $b = $color & 0xff; + return [$r, $g, $b]; +} + +function getUserTheme(): string { + if (isset($_COOKIE['theme'])) { + $val = $_COOKIE['theme']; + if (is_array($val)) + $val = implode($val); + } else + $val = 'auto'; + return $val; +} diff --git a/lib/uploads.php b/lib/uploads.php index 6c7e6bc..7540f11 100644 --- a/lib/uploads.php +++ b/lib/uploads.php @@ -1,30 +1,30 @@ <?php -class uploads { +const UPLOADS_ALLOWED_EXTENSIONS = [ + 'jpg', 'png', 'git', 'mp4', 'mp3', 'ogg', 'diff', 'txt', 'gz', 'tar', + 'icc', 'icm', 'patch', 'zip', 'brd', 'pdf', 'lua', 'xpi', 'rar', '7z', + 'tgz', 'bin', 'py', 'pac', 'yaml', 'toml', 'xml', 'json', 'yml', +]; - protected static $allowedExtensions = [ - 'jpg', 'png', 'git', 'mp4', 'mp3', 'ogg', 'diff', 'txt', 'gz', 'tar', - 'icc', 'icm', 'patch', 'zip', 'brd', 'pdf', 'lua', 'xpi', 'rar', '7z', - 'tgz', 'bin', 'py', 'pac', 'yaml', 'toml', 'xml', 'json', 'yml', - ]; +class uploads { - public static function getCount(): int { - $db = getDb(); + static function getCount(): int { + $db = DB(); return (int)$db->result($db->query("SELECT COUNT(*) FROM uploads")); } - public static function isExtensionAllowed(string $ext): bool { - return in_array($ext, self::$allowedExtensions); + static function isExtensionAllowed(string $ext): bool { + return in_array($ext, UPLOADS_ALLOWED_EXTENSIONS); } - public static function add(string $tmp_name, string $name, string $note): ?int { + static function add(string $tmp_name, string $name, string $note): ?int { global $config; $name = sanitize_filename($name); if (!$name) $name = 'file'; - $random_id = self::getNewRandomId(); + $random_id = self::_getNewUploadRandomId(); $size = filesize($tmp_name); $is_image = detect_image_type($tmp_name) !== false; $image_w = 0; @@ -33,7 +33,7 @@ class uploads { list($image_w, $image_h) = getimagesize($tmp_name); } - $db = getDb(); + $db = DB(); if (!$db->insert('uploads', [ 'random_id' => $random_id, 'ts' => time(), @@ -62,12 +62,12 @@ class uploads { return $id; } - public static function delete(int $id): bool { + static function delete(int $id): bool { $upload = self::get($id); if (!$upload) return false; - $db = getDb(); + $db = DB(); $db->query("DELETE FROM uploads WHERE id=?", $id); rrmdir($upload->getDirectory()); @@ -77,14 +77,14 @@ class uploads { /** * @return Upload[] */ - public static function getAll(): array { - $db = getDb(); + static function getAllUploads(): array { + $db = DB(); $q = $db->query("SELECT * FROM uploads ORDER BY id DESC"); return array_map('Upload::create_instance', $db->fetchAll($q)); } - public static function get(int $id): ?Upload { - $db = getDb(); + static function get(int $id): ?Upload { + $db = DB(); $q = $db->query("SELECT * FROM uploads WHERE id=?", $id); if ($db->numRows($q)) { return new Upload($db->fetch($q)); @@ -98,12 +98,12 @@ class uploads { * @param bool $flat * @return Upload[] */ - public static function getUploadsByRandomId(array $ids, bool $flat = false): array { + static function getUploadsByRandomId(array $ids, bool $flat = false): array { if (empty($ids)) { return []; } - $db = getDb(); + $db = DB(); $uploads = array_fill_keys($ids, null); $q = $db->query("SELECT * FROM uploads WHERE random_id IN('".implode('\',\'', array_map([$db, 'escape'], $ids))."')"); @@ -124,8 +124,8 @@ class uploads { return $uploads; } - public static function getByRandomId(string $random_id): ?Upload { - $db = getDb(); + static function getUploadByRandomId(string $random_id): ?Upload { + $db = DB(); $q = $db->query("SELECT * FROM uploads WHERE random_id=? LIMIT 1", $random_id); if ($db->numRows($q)) { return new Upload($db->fetch($q)); @@ -134,12 +134,175 @@ class uploads { } } - protected static function getNewRandomId(): string { - $db = getDb(); + static function _getNewUploadRandomId(): string { + $db = DB(); do { $random_id = strgen(8); } while ($db->numRows($db->query("SELECT id FROM uploads WHERE random_id=?", $random_id)) > 0); return $random_id; - } + } } + + +class Upload extends model { + + const DB_TABLE = 'uploads'; + + public static array $ImageExtensions = ['jpg', 'jpeg', 'png', 'gif']; + public static array $VideoExtensions = ['mp4', 'ogg']; + + public int $id; + public string $randomId; + public int $ts; + public string $name; + public int $size; + public int $downloads; + public int $image; // TODO: remove + public int $imageW; + public int $imageH; + public string $note; + + function getDirectory(): string { + global $config; + return $config['uploads_dir'].'/'.$this->randomId; + } + + function getDirectUrl(): string { + global $config; + return 'https://'.$config['uploads_host'].'/'.$this->randomId.'/'.$this->name; + } + + function getDirectPreviewUrl(int $w, int $h, bool $retina = false): string { + global $config; + if ($w == $this->imageW && $this->imageH == $h) + return $this->getDirectUrl(); + + if ($retina) { + $w *= 2; + $h *= 2; + } + + $prefix = $this->imageMayHaveAlphaChannel() ? 'a' : 'p'; + return 'https://'.$config['uploads_host'].'/'.$this->randomId.'/'.$prefix.$w.'x'.$h.'.jpg'; + } + + // TODO remove? + function incrementDownloads() { + $db = DB(); + $db->query("UPDATE uploads SET downloads=downloads+1 WHERE id=?", $this->id); + $this->downloads++; + } + + function getSize(): string { + return sizeString($this->size); + } + + function getMarkdown(): string { + if ($this->isImage()) { + $md = '{image:'.$this->randomId.',w='.$this->imageW.',h='.$this->imageH.'}{/image}'; + } else if ($this->isVideo()) { + $md = '{video:'.$this->randomId.'}{/video}'; + } else { + $md = '{fileAttach:'.$this->randomId.'}{/fileAttach}'; + } + $md .= ' <!-- '.$this->name.' -->'; + return $md; + } + + function setNote(string $note) { + $db = DB(); + $db->query("UPDATE uploads SET note=? WHERE id=?", $note, $this->id); + } + + function isImage(): bool { + return in_array(extension($this->name), self::$ImageExtensions); + } + + // assume all png images have alpha channel + // i know this is wrong, but anyway + function imageMayHaveAlphaChannel(): bool { + return strtolower(extension($this->name)) == 'png'; + } + + function isVideo(): bool { + return in_array(extension($this->name), self::$VideoExtensions); + } + + function getImageRatio(): float { + return $this->imageW / $this->imageH; + } + + function getImagePreviewSize(?int $w = null, ?int $h = null): array { + if (is_null($w) && is_null($h)) + throw new Exception(__METHOD__.': both width and height can\'t be null'); + + if (is_null($h)) + $h = round($w / $this->getImageRatio()); + + if (is_null($w)) + $w = round($h * $this->getImageRatio()); + + return [$w, $h]; + } + + function createImagePreview(?int $w = null, + ?int $h = null, + bool $force_update = false, + bool $may_have_alpha = false): bool { + global $config; + + $orig = $config['uploads_dir'].'/'.$this->randomId.'/'.$this->name; + $updated = false; + + foreach (themes::getThemes() as $theme) { + if (!$may_have_alpha && $theme == 'dark') + continue; + + for ($mult = 1; $mult <= 2; $mult++) { + $dw = $w * $mult; + $dh = $h * $mult; + + $prefix = $may_have_alpha ? 'a' : 'p'; + $dst = $config['uploads_dir'].'/'.$this->randomId.'/'.$prefix.$dw.'x'.$dh.($theme == 'dark' ? '_dark' : '').'.jpg'; + + if (file_exists($dst)) { + if (!$force_update) + continue; + unlink($dst); + } + + $img = imageopen($orig); + imageresize($img, $dw, $dh, themes::getThemeAlphaColorAsRGB($theme)); + imagejpeg($img, $dst, $mult == 1 ? 93 : 67); + imagedestroy($img); + + setperm($dst); + $updated = true; + } + } + + return $updated; + } + + /** + * @return int Number of deleted files + */ + function deleteAllImagePreviews(): int { + global $config; + $dir = $config['uploads_dir'].'/'.$this->randomId; + $files = scandir($dir); + $deleted = 0; + foreach ($files as $f) { + if (preg_match('/^[ap](\d+)x(\d+)(?:_dark)?\.jpg$/', $f)) { + if (is_file($dir.'/'.$f)) + unlink($dir.'/'.$f); + else + logError(__METHOD__.': '.$dir.'/'.$f.' is not a file!'); + $deleted++; + } + } + return $deleted; + } + +}
\ No newline at end of file |