From c0dc531ebefd8912819f3b6c8bda1fed3c7e750c Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Wed, 31 Jan 2024 06:11:00 +0300 Subject: make it simple, but not simpler --- lib/MyParsedown.php | 218 ---------------------------------- lib/admin.php | 82 ++++++------- lib/ansi.php | 32 +++++ lib/cli.php | 8 +- lib/config.php | 46 -------- lib/ext/MyParsedown.php | 218 ++++++++++++++++++++++++++++++++++ lib/markup.php | 2 + lib/pages.php | 58 +++++++-- lib/posts.php | 306 ++++++++++++++++++++++++++++++++++-------------- lib/stored_config.php | 14 +++ lib/tags.php | 87 ++++++++++++++ lib/themes.php | 41 +++++++ lib/uploads.php | 215 ++++++++++++++++++++++++++++++---- 13 files changed, 894 insertions(+), 433 deletions(-) delete mode 100644 lib/MyParsedown.php create mode 100644 lib/ansi.php delete mode 100644 lib/config.php create mode 100644 lib/ext/MyParsedown.php create mode 100644 lib/stored_config.php create mode 100644 lib/tags.php create mode 100644 lib/themes.php (limited to 'lib') diff --git a/lib/MyParsedown.php b/lib/MyParsedown.php deleted file mode 100644 index 85ed9c4..0000000 --- a/lib/MyParsedown.php +++ /dev/null @@ -1,218 +0,0 @@ - [ - 'tablespan' => true - ] - ]; - if (!is_null($opts)) { - $parsedown_opts = array_merge($parsedown_opts, $opts); - } - parent::__construct($parsedown_opts); - - $this->InlineTypes['{'][] = 'FileAttach'; - $this->InlineTypes['{'][] = 'Image'; - $this->InlineTypes['{'][] = 'Video'; - $this->inlineMarkerList .= '{'; - } - - protected function inlineFileAttach($excerpt) { - if (preg_match('/^{fileAttach:([\w]{8})}{\/fileAttach}/', $excerpt['text'], $matches)) { - $random_id = $matches[1]; - $upload = uploads::getByRandomId($random_id); - $result = [ - 'extent' => strlen($matches[0]), - 'element' => [ - 'name' => 'span', - 'text' => '', - ], - 'type' => '' - ]; - - if (!$upload) { - return $result; - } - - unset($result['element']['text']); - - $ctx = self::getSkinContext(); - $result['element']['rawHtml'] = $ctx->fileupload($upload->name, $upload->getDirectUrl(), $upload->note, $upload->getSize()); - - return $result; - } - } - - protected function inlineImage($excerpt) { - global $config; - - if (preg_match('/^{image:([\w]{8}),(.*?)}{\/image}/', $excerpt['text'], $matches)) { - $random_id = $matches[1]; - - $opts = [ - 'w' => 'auto', - 'h' => 'auto', - 'align' => 'left', - 'nolabel' => false, - ]; - $inputopts = explode(',', $matches[2]); - - foreach ($inputopts as $opt) { - if ($opt == 'nolabel') - $opts[$opt] = true; - else { - list($k, $v) = explode('=', $opt); - if (!isset($opts[$k])) - continue; - $opts[$k] = $v; - } - } - - $image = uploads::getByRandomId($random_id); - $result = [ - 'extent' => strlen($matches[0]), - 'element' => [ - 'name' => 'span', - 'text' => '', - ], - 'type' => '' - ]; - - if (!$image) { - return $result; - } - - list($w, $h) = $image->getImagePreviewSize( - $opts['w'] == 'auto' ? null : $opts['w'], - $opts['h'] == 'auto' ? null : $opts['h'] - ); - $opts['w'] = $w; - // $opts['h'] = $h; - - if (!$this->useImagePreviews) - $image_url = $image->getDirectUrl(); - else - $image_url = $image->getDirectPreviewUrl($w, $h); - - unset($result['element']['text']); - - $ctx = self::getSkinContext(); - $result['element']['rawHtml'] = $ctx->image( - w: $opts['w'], - nolabel: $opts['nolabel'], - align: $opts['align'], - padding_top: round($h / $w * 100, 4), - may_have_alpha: $image->imageMayHaveAlphaChannel(), - - url: $image_url, - direct_url: $image->getDirectUrl(), - note: $image->note - ); - - return $result; - } - } - - protected function inlineVideo($excerpt) { - if (preg_match('/^{video:([\w]{8})(?:,(.*?))?}{\/video}/', $excerpt['text'], $matches)) { - $random_id = $matches[1]; - - $opts = [ - 'w' => 'auto', - 'h' => 'auto', - 'align' => 'left', - 'nolabel' => false, - ]; - $inputopts = !empty($matches[2]) ? explode(',', $matches[2]) : []; - - foreach ($inputopts as $opt) { - if ($opt == 'nolabel') - $opts[$opt] = true; - else { - list($k, $v) = explode('=', $opt); - if (!isset($opts[$k])) - continue; - $opts[$k] = $v; - } - } - - $video = uploads::getByRandomId($random_id); - $result = [ - 'extent' => strlen($matches[0]), - 'element' => [ - 'name' => 'span', - 'text' => '', - ], - 'type' => '' - ]; - - if (!$video) { - return $result; - } - - $video_url = $video->getDirectUrl(); - - unset($result['element']['text']); - - $ctx = self::getSkinContext(); - $result['element']['rawHtml'] = $ctx->video( - url: $video_url, - w: $opts['w'], - h: $opts['h'] - ); - - return $result; - } - } - - protected function paragraph($line) { - if (preg_match('/^{fileAttach:([\w]{8})}{\/fileAttach}$/', $line['text'])) { - return $this->inlineFileAttach($line); - } - if (preg_match('/^{image:([\w]{8}),(?:.*?)}{\/image}/', $line['text'])) { - return $this->inlineImage($line); - } - if (preg_match('/^{video:([\w]{8})(?:,(?:.*?))?}{\/video}/', $line['text'])) { - return $this->inlineVideo($line); - } - return parent::paragraph($line); - } - - protected function blockFencedCodeComplete($block) { - if (!isset($block['element']['element']['attributes'])) { - return $block; - } - - $code = $block['element']['element']['text']; - $languageClass = $block['element']['element']['attributes']['class']; - $language = explode('-', $languageClass); - - if ($language[1] == 'term') { - $lines = explode("\n", $code); - for ($i = 0; $i < count($lines); $i++) { - $line = $lines[$i]; - if (str_starts_with($line, '$ ') || str_starts_with($line, '# ')) { - $lines[$i] = ''.substr($line, 0, 2).''.htmlspecialchars(substr($line, 2), ENT_NOQUOTES, 'UTF-8'); - } else { - $lines[$i] = htmlspecialchars($line, ENT_NOQUOTES, 'UTF-8'); - } - } - $block['element']['element']['rawHtml'] = implode("\n", $lines); - unset($block['element']['element']['text']); - - return $block; - } - - return parent::blockFencedCodeComplete($block); - } - - protected static function getSkinContext(): SkinContext { - return new SkinContext('\\skin\\markdown'); - } - -} 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 @@ 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 @@ +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 @@ -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/ext/MyParsedown.php b/lib/ext/MyParsedown.php new file mode 100644 index 0000000..71dfa7f --- /dev/null +++ b/lib/ext/MyParsedown.php @@ -0,0 +1,218 @@ + [ + 'tablespan' => true + ] + ]; + if (!is_null($opts)) { + $parsedown_opts = array_merge($parsedown_opts, $opts); + } + parent::__construct($parsedown_opts); + + $this->InlineTypes['{'][] = 'FileAttach'; + $this->InlineTypes['{'][] = 'Image'; + $this->InlineTypes['{'][] = 'Video'; + $this->inlineMarkerList .= '{'; + } + + protected function inlineFileAttach($excerpt) { + if (preg_match('/^{fileAttach:([\w]{8})}{\/fileAttach}/', $excerpt['text'], $matches)) { + $random_id = $matches[1]; + $upload = uploads::getUploadByRandomId($random_id); + $result = [ + 'extent' => strlen($matches[0]), + 'element' => [ + 'name' => 'span', + 'text' => '', + ], + 'type' => '' + ]; + + if (!$upload) { + return $result; + } + + unset($result['element']['text']); + + $ctx = self::getSkinContext(); + $result['element']['rawHtml'] = $ctx->fileupload($upload->name, $upload->getDirectUrl(), $upload->note, $upload->getSize()); + + return $result; + } + } + + protected function inlineImage($excerpt) { + global $config; + + if (preg_match('/^{image:([\w]{8}),(.*?)}{\/image}/', $excerpt['text'], $matches)) { + $random_id = $matches[1]; + + $opts = [ + 'w' => 'auto', + 'h' => 'auto', + 'align' => 'left', + 'nolabel' => false, + ]; + $inputopts = explode(',', $matches[2]); + + foreach ($inputopts as $opt) { + if ($opt == 'nolabel') + $opts[$opt] = true; + else { + list($k, $v) = explode('=', $opt); + if (!isset($opts[$k])) + continue; + $opts[$k] = $v; + } + } + + $image = uploads::getUploadByRandomId($random_id); + $result = [ + 'extent' => strlen($matches[0]), + 'element' => [ + 'name' => 'span', + 'text' => '', + ], + 'type' => '' + ]; + + if (!$image) { + return $result; + } + + list($w, $h) = $image->getImagePreviewSize( + $opts['w'] == 'auto' ? null : $opts['w'], + $opts['h'] == 'auto' ? null : $opts['h'] + ); + $opts['w'] = $w; + // $opts['h'] = $h; + + if (!$this->useImagePreviews) + $image_url = $image->getDirectUrl(); + else + $image_url = $image->getDirectPreviewUrl($w, $h); + + unset($result['element']['text']); + + $ctx = self::getSkinContext(); + $result['element']['rawHtml'] = $ctx->image( + w: $opts['w'], + nolabel: $opts['nolabel'], + align: $opts['align'], + padding_top: round($h / $w * 100, 4), + may_have_alpha: $image->imageMayHaveAlphaChannel(), + + url: $image_url, + direct_url: $image->getDirectUrl(), + note: $image->note + ); + + return $result; + } + } + + protected function inlineVideo($excerpt) { + if (preg_match('/^{video:([\w]{8})(?:,(.*?))?}{\/video}/', $excerpt['text'], $matches)) { + $random_id = $matches[1]; + + $opts = [ + 'w' => 'auto', + 'h' => 'auto', + 'align' => 'left', + 'nolabel' => false, + ]; + $inputopts = !empty($matches[2]) ? explode(',', $matches[2]) : []; + + foreach ($inputopts as $opt) { + if ($opt == 'nolabel') + $opts[$opt] = true; + else { + list($k, $v) = explode('=', $opt); + if (!isset($opts[$k])) + continue; + $opts[$k] = $v; + } + } + + $video = uploads::getUploadByRandomId($random_id); + $result = [ + 'extent' => strlen($matches[0]), + 'element' => [ + 'name' => 'span', + 'text' => '', + ], + 'type' => '' + ]; + + if (!$video) { + return $result; + } + + $video_url = $video->getDirectUrl(); + + unset($result['element']['text']); + + $ctx = self::getSkinContext(); + $result['element']['rawHtml'] = $ctx->video( + url: $video_url, + w: $opts['w'], + h: $opts['h'] + ); + + return $result; + } + } + + protected function paragraph($line) { + if (preg_match('/^{fileAttach:([\w]{8})}{\/fileAttach}$/', $line['text'])) { + return $this->inlineFileAttach($line); + } + if (preg_match('/^{image:([\w]{8}),(?:.*?)}{\/image}/', $line['text'])) { + return $this->inlineImage($line); + } + if (preg_match('/^{video:([\w]{8})(?:,(?:.*?))?}{\/video}/', $line['text'])) { + return $this->inlineVideo($line); + } + return parent::paragraph($line); + } + + protected function blockFencedCodeComplete($block) { + if (!isset($block['element']['element']['attributes'])) { + return $block; + } + + $code = $block['element']['element']['text']; + $languageClass = $block['element']['element']['attributes']['class']; + $language = explode('-', $languageClass); + + if ($language[1] == 'term') { + $lines = explode("\n", $code); + for ($i = 0; $i < count($lines); $i++) { + $line = $lines[$i]; + if (str_starts_with($line, '$ ') || str_starts_with($line, '# ')) { + $lines[$i] = ''.substr($line, 0, 2).''.htmlspecialchars(substr($line, 2), ENT_NOQUOTES, 'UTF-8'); + } else { + $lines[$i] = htmlspecialchars($line, ENT_NOQUOTES, 'UTF-8'); + } + } + $block['element']['element']['rawHtml'] = implode("\n", $lines); + unset($block['element']['element']['text']); + + return $block; + } + + return parent::blockFencedCodeComplete($block); + } + + protected static function getSkinContext(): SkinContext { + return new SkinContext('\\skin\\markdown'); + } + +} 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 @@ 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 @@ 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 @@ +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 @@ +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 @@ + [ + '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 @@ 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 .= ' '; + 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 -- cgit v1.2.3