aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2024-01-31 06:11:00 +0300
committerEvgeny Zinoviev <me@ch1p.io>2024-01-31 20:45:40 +0300
commitc0dc531ebefd8912819f3b6c8bda1fed3c7e750c (patch)
tree2c75aa9df182260aef09faf4befd81a4c2b9c5e2 /lib
parent48d688cdf7f9eae1bf11b8a6f0e5b98687c604cb (diff)
make it simple, but not simpler
Diffstat (limited to 'lib')
-rw-r--r--lib/admin.php82
-rw-r--r--lib/ansi.php32
-rw-r--r--lib/cli.php8
-rw-r--r--lib/config.php46
-rw-r--r--lib/ext/MyParsedown.php (renamed from lib/MyParsedown.php)10
-rw-r--r--lib/markup.php2
-rw-r--r--lib/pages.php58
-rw-r--r--lib/posts.php306
-rw-r--r--lib/stored_config.php14
-rw-r--r--lib/tags.php87
-rw-r--r--lib/themes.php41
-rw-r--r--lib/uploads.php215
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