summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2022-07-09 19:40:17 +0300
committerEvgeny Zinoviev <me@ch1p.io>2022-07-09 19:40:17 +0300
commitf7bfdf58def6aadc922e1632f407d1418269a0d7 (patch)
treed7a0b2819e6a26c11d40ee0b27267ea827fbb345 /lib
initial
Diffstat (limited to 'lib')
-rw-r--r--lib/MyParsedown.php209
-rw-r--r--lib/admin.php57
-rw-r--r--lib/cli.php70
-rw-r--r--lib/config.php46
-rw-r--r--lib/markup.php30
-rw-r--r--lib/pages.php32
-rw-r--r--lib/posts.php188
-rw-r--r--lib/uploads.php145
8 files changed, 777 insertions, 0 deletions
diff --git a/lib/MyParsedown.php b/lib/MyParsedown.php
new file mode 100644
index 0000000..c2c0112
--- /dev/null
+++ b/lib/MyParsedown.php
@@ -0,0 +1,209 @@
+<?php
+
+use sixlive\ParsedownHighlight;
+
+class MyParsedown extends ParsedownHighlight {
+
+ public function __construct(
+ protected bool $useImagePreviews = false
+ ) {
+ parent::__construct();
+ $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),
+
+ 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] = '<span class="term-prompt">'.substr($line, 0, 2).'</span>'.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
new file mode 100644
index 0000000..8b36b36
--- /dev/null
+++ b/lib/admin.php
@@ -0,0 +1,57 @@
+<?php
+
+class admin {
+
+ const SESSION_TIMEOUT = 86400 * 14;
+ const 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;
+ }
+
+ public static function checkPassword(string $pwd): bool {
+ return salt_password($pwd) === config::get('admin_pwd');
+ }
+
+ protected static function getKey(): string {
+ global $config;
+ $admin_pwd_hash = config::get('admin_pwd');
+ return salt_password("$admin_pwd_hash|{$_SERVER['REMOTE_ADDR']}");
+ }
+
+ public static function setCookie(): void {
+ global $config;
+ $key = self::getKey();
+ setcookie(self::COOKIE_NAME, $key, time() + self::SESSION_TIMEOUT, '/', $config['cookie_host']);
+ }
+
+ public static function unsetCookie(): void {
+ global $config;
+ setcookie(self::COOKIE_NAME, null, -1, '/', $config['cookie_host']);
+ }
+
+ public static function logAuth(): void {
+ getDb()->insert('admin_log', [
+ 'ts' => time(),
+ 'ip' => ip2ulong($_SERVER['REMOTE_ADDR']),
+ 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
+ ]);
+ }
+
+
+}
+
diff --git a/lib/cli.php b/lib/cli.php
new file mode 100644
index 0000000..3bda20f
--- /dev/null
+++ b/lib/cli.php
@@ -0,0 +1,70 @@
+<?php
+
+class cli {
+
+ protected ?array $commandsCache = null;
+
+ public function __construct(
+ protected string $ns
+ ) {}
+
+ protected function usage($error = null): void {
+ global $argv;
+
+ if (!is_null($error))
+ echo "error: {$error}\n\n";
+
+ echo "Usage: $argv[0] COMMAND\n\nCommands:\n";
+ foreach ($this->getCommands() as $c)
+ echo " $c\n";
+
+ exit(is_null($error) ? 0 : 1);
+ }
+
+ public 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);
+ $this->commandsCache = array_values($funcs);
+ }
+ return $this->commandsCache;
+ }
+
+ public function run(): void {
+ global $argv, $argc;
+
+ if (PHP_SAPI != 'cli')
+ cli::die('SAPI != cli');
+
+ if ($argc < 2)
+ $this->usage();
+
+ $func = $argv[1];
+ if (!in_array($func, $this->getCommands()))
+ self::usage('unknown command "'.$func.'"');
+
+ $func = str_replace('-', '_', $func);
+ call_user_func($this->ns.'\\'.$func);
+ }
+
+ public static function input(string $prompt): string {
+ echo $prompt;
+ $input = substr(fgets(STDIN), 0, -1);
+ return $input;
+ }
+
+ public static function silentInput(string $prompt = ''): string {
+ echo $prompt;
+ system('stty -echo');
+ $input = substr(fgets(STDIN), 0, -1);
+ system('stty echo');
+ echo "\n";
+ return $input;
+ }
+
+ public static function die($error): void {
+ fwrite(STDERR, "error: {$error}\n");
+ exit(1);
+ }
+
+} \ No newline at end of file
diff --git a/lib/config.php b/lib/config.php
new file mode 100644
index 0000000..bb7e5ca
--- /dev/null
+++ b/lib/config.php
@@ -0,0 +1,46 @@
+<?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/markup.php b/lib/markup.php
new file mode 100644
index 0000000..52ccf24
--- /dev/null
+++ b/lib/markup.php
@@ -0,0 +1,30 @@
+<?php
+
+class markup {
+
+ public static function markdownToHtml(string $md, bool $use_image_previews = true): string {
+ $pd = new MyParsedown(useImagePreviews: $use_image_previews);
+ return $pd->text($md);
+ }
+
+ public static function htmlToText(string $html): string {
+ $text = html_entity_decode(strip_tags($html));
+ $lines = explode("\n", $text);
+ $lines = array_map('trim', $lines);
+ $text = implode("\n", $lines);
+ $text = preg_replace("/(\r?\n){2,}/", "\n\n", $text);
+ return $text;
+ }
+
+ public static function htmlRetinaFix(string $html): string {
+ global $config;
+ return preg_replace_callback(
+ '/('.preg_quote($config['uploads_host'], '/').'\/\w{8}\/p)(\d+)x(\d+)(\.jpg)/',
+ function($match) {
+ return $match[1].(intval($match[2])*2).'x'.(intval($match[3])*2).$match[4];
+ },
+ $html
+ );
+ }
+
+} \ No newline at end of file
diff --git a/lib/pages.php b/lib/pages.php
new file mode 100644
index 0000000..281ee52
--- /dev/null
+++ b/lib/pages.php
@@ -0,0 +1,32 @@
+<?php
+
+class pages {
+
+ public static function add(array $data): ?int {
+ $db = getDb();
+ $data['ts'] = time();
+ $data['html'] = markup::markdownToHtml($data['md']);
+ if (!$db->insert('pages', $data))
+ return null;
+ return $db->insertId();
+ }
+
+ public static function delete(Page $page): void {
+ getDb()->query("DELETE FROM pages WHERE short_name=?", $page->shortName);
+ }
+
+ public static function getPageByName(string $short_name): ?Page {
+ $db = getDb();
+ $q = $db->query("SELECT * FROM pages WHERE short_name=?", $short_name);
+ return $db->numRows($q) ? new Page($db->fetch($q)) : null;
+ }
+
+ /**
+ * @return Page[]
+ */
+ public static function getAll(): array {
+ $db = getDb();
+ return array_map('Page::create_instance', $db->fetchAll($db->query("SELECT * FROM pages")));
+ }
+
+} \ No newline at end of file
diff --git a/lib/posts.php b/lib/posts.php
new file mode 100644
index 0000000..bf8d149
--- /dev/null
+++ b/lib/posts.php
@@ -0,0 +1,188 @@
+<?php
+
+class posts {
+
+ public static function getPostsCount(bool $include_hidden = false): int {
+ $db = getDb();
+ $sql = "SELECT COUNT(*) FROM posts";
+ if (!$include_hidden) {
+ $sql .= " WHERE visible=1";
+ }
+ return (int)$db->result($db->query($sql));
+ }
+
+ public static function getPostsCountByTagId(int $tag_id, bool $include_hidden = false): int {
+ $db = getDb();
+ 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";
+ }
+ return (int)$db->result($db->query($sql, $tag_id));
+ }
+
+ public static function getPosts(int $offset = 0, int $count = -1, bool $include_hidden = false): array {
+ $db = getDb();
+ $sql = "SELECT * FROM posts";
+ if (!$include_hidden)
+ $sql .= " WHERE visible=1";
+ $sql .= " ORDER BY ts DESC";
+ if ($offset != 0 && $count != -1)
+ $sql .= "LIMIT $offset, $count";
+ $q = $db->query($sql);
+ return array_map('Post::create_instance', $db->fetchAll($q));
+ }
+
+ /**
+ * @return Post[]
+ */
+ public static function getPostsByTagId(int $tag_id, bool $include_hidden = false): array {
+ $db = getDb();
+ $sql = "SELECT posts.* FROM posts_tags
+ 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";
+ $q = $db->query($sql, $tag_id);
+ return array_map('Post::create_instance', $db->fetchAll($q));
+ }
+
+ public static function add(array $data = []): int|bool {
+ $db = getDb();
+
+ $html = \markup::markdownToHtml($data['md']);
+ $text = \markup::htmlToText($html);
+
+ $data += [
+ 'ts' => time(),
+ 'html' => $html,
+ 'text' => $text,
+ ];
+
+ if (!$db->insert('posts', $data))
+ return false;
+
+ $id = $db->insertId();
+
+ $post = posts::get($id);
+ $post->updateImagePreviews();
+
+ return $id;
+ }
+
+ public static function delete(Post $post): void {
+ $tags = $post->getTags();
+
+ $db = getDb();
+ $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);
+ }
+
+ 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();
+ $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();
+ $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 {
+ if (empty($ids)) {
+ return [];
+ }
+
+ $db = getDb();
+ $posts = array_fill_keys($ids, null);
+
+ $q = $db->query("SELECT * FROM posts WHERE id IN(".implode(',', $ids).")");
+
+ while ($row = $db->fetch($q)) {
+ $posts[(int)$row['id']] = new Post($row);
+ }
+
+ if ($flat) {
+ $list = [];
+ foreach ($ids as $id) {
+ $list[] = $posts[$id];
+ }
+ unset($posts);
+ return $list;
+ }
+
+ 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;
+ }
+
+}
diff --git a/lib/uploads.php b/lib/uploads.php
new file mode 100644
index 0000000..62be276
--- /dev/null
+++ b/lib/uploads.php
@@ -0,0 +1,145 @@
+<?php
+
+class uploads {
+
+ 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',
+ ];
+
+ public static function getCount(): int {
+ $db = getDb();
+ return (int)$db->result($db->query("SELECT COUNT(*) FROM uploads"));
+ }
+
+ public static function isExtensionAllowed(string $ext): bool {
+ return in_array($ext, self::$allowedExtensions);
+ }
+
+ public 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();
+ $size = filesize($tmp_name);
+ $is_image = detect_image_type($tmp_name) !== false;
+ $image_w = 0;
+ $image_h = 0;
+ if ($is_image) {
+ list($image_w, $image_h) = getimagesize($tmp_name);
+ }
+
+ $db = getDb();
+ if (!$db->insert('uploads', [
+ 'random_id' => $random_id,
+ 'ts' => time(),
+ 'name' => $name,
+ 'size' => $size,
+ 'image' => (int)$is_image,
+ 'image_w' => $image_w,
+ 'image_h' => $image_h,
+ 'note' => $note,
+ 'downloads' => 0,
+ ])) {
+ return null;
+ }
+
+ $id = $db->insertId();
+
+ $dir = $config['uploads_dir'].'/'.$random_id;
+ $path = $dir.'/'.$name;
+
+ mkdir($dir);
+ chmod($dir, 0775); // g+w
+
+ rename($tmp_name, $path);
+ chmod($path, 0664); // g+w
+
+ return $id;
+ }
+
+ public static function delete(int $id): bool {
+ $upload = self::get($id);
+ if (!$upload)
+ return false;
+
+ $db = getDb();
+ $db->query("DELETE FROM uploads WHERE id=?", $id);
+
+ rrmdir($upload->getDirectory());
+ return true;
+ }
+
+ /**
+ * @return Upload[]
+ */
+ public static function getAll(): array {
+ $db = getDb();
+ $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();
+ $q = $db->query("SELECT * FROM uploads WHERE id=?", $id);
+ if ($db->numRows($q)) {
+ return new Upload($db->fetch($q));
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @param string[] $ids
+ * @param bool $flat
+ * @return Upload[]
+ */
+ public static function getUploadsByRandomId(array $ids, bool $flat = false): array {
+ if (empty($ids)) {
+ return [];
+ }
+
+ $db = getDb();
+ $uploads = array_fill_keys($ids, null);
+
+ $q = $db->query("SELECT * FROM uploads WHERE random_id IN('".implode('\',\'', array_map([$db, 'escape'], $ids))."')");
+
+ while ($row = $db->fetch($q)) {
+ $uploads[$row['random_id']] = new Upload($row);
+ }
+
+ if ($flat) {
+ $list = [];
+ foreach ($ids as $id) {
+ $list[] = $uploads[$id];
+ }
+ unset($uploads);
+ return $list;
+ }
+
+ return $uploads;
+ }
+
+ public static function getByRandomId(string $random_id): ?Upload {
+ $db = getDb();
+ $q = $db->query("SELECT * FROM uploads WHERE random_id=? LIMIT 1", $random_id);
+ if ($db->numRows($q)) {
+ return new Upload($db->fetch($q));
+ } else {
+ return null;
+ }
+ }
+
+ protected static function getNewRandomId(): string {
+ $db = getDb();
+ do {
+ $random_id = strgen(8);
+ } while ($db->numRows($db->query("SELECT id FROM uploads WHERE random_id=?", $random_id)) > 0);
+ return $random_id;
+ }
+
+}