$title, 'lang' => $lang->value, 'post_id' => $this->id, 'html' => $html, 'text' => $text, 'md' => $md, 'toc' => $toc, 'keywords' => $keywords, ]; $db = DB(); if (!$db->insert('posts_texts', $data)) return null; $id = $db->insertId(); $post_text = posts::getText($id); $post_text->updateImagePreviews(); return $post_text; } public function registerText(PostText $postText): void { if (array_key_exists($postText->lang->value, $this->texts)) throw new Exception("text for language {$postText->lang->value} has already been registered"); $this->texts[$postText->lang->value] = $postText; } public function loadTexts() { if (!empty($this->texts)) return; $db = DB(); $q = $db->query("SELECT * FROM posts_texts WHERE post_id=?", $this->id); while ($row = $db->fetch($q)) { $text = new PostText($row); $this->registerText($text); } } /** * @return PostText[] */ public function getTexts(): array { $this->loadTexts(); return $this->texts; } public function getText(PostLanguage $lang): ?PostText { $this->loadTexts(); return $this->texts[$lang->value] ?? null; } public function hasLang(PostLanguage $lang) { $this->loadTexts(); foreach ($this->texts as $text) { if ($text->lang == $lang) return true; } return false; } public function getUrl(?PostLanguage $lang = null): string { $buf = $this->shortName != '' ? "/articles/{$this->shortName}/" : "/articles/{$this->id}/"; if ($lang && $lang != PostLanguage::English) $buf .= '?lang='.$lang->value; return $buf; } public function getTimestamp(): int { return (new DateTime($this->date))->getTimestamp(); } public function getDate(): string { return date('j M', $this->getTimestamp()); } public function getYear(): int { return (int)date('Y', $this->getTimestamp()); } public function getFullDate(): string { return date('j F Y', $this->getTimestamp()); } public function getDateForInputField(): string { return date('Y-m-d', $this->getTimestamp()); } } class PostText extends model { const DB_TABLE = 'posts_texts'; public int $id; public int $postId; public PostLanguage $lang; public string $title; public string $md; public string $html; public string $text; public bool $toc; public string $tocHtml; public string $keywords; public function edit(array $fields) { if ($fields['md'] != $this->md) { $fields['html'] = markup::markdownToHtml($fields['md'], lang: $this->lang); $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(); } public function updateHtml(): void { $html = markup::markdownToHtml($this->md, lang: $this->lang); $this->html = $html; DB()->query("UPDATE posts_texts SET html=? WHERE id=?", $html, $this->id); } public function updateText(): void { $html = markup::markdownToHtml($this->md, lang: $this->lang); $text = markup::htmlToText($html); $this->text = $text; DB()->query("UPDATE posts_texts SET text=? WHERE id=?", $text, $this->id); } public function getDescriptionPreview(int $len): string { if (mb_strlen($this->text) >= $len) return mb_substr($this->text, 0, $len-3).'...'; return $this->text; } public function getFirstImage(): ?Upload { if (!preg_match('/\{image:([\w]{8})/', $this->md, $match)) return null; return uploads::getUploadByRandomId($match[1]); } public function getHtml(bool $is_retina, string $theme): string { $html = $this->html; return markup::htmlImagesFix($html, $is_retina, $theme); } public function getTableOfContentsHtml(): ?string { return $this->toc ? $this->tocHtml : null; } public function hasTableOfContents(): bool { return $this->toc; } /** * @param bool $update Whether to overwrite preview if already exists * @return int * @throws Exception */ public 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 $upload_key => $u) { if ($u === null) { logError(__METHOD__.': upload '.$upload_key.' is null'); continue; } 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 { static function getCount(bool $include_hidden = false): int { $db = DB(); $sql = "SELECT COUNT(*) FROM posts"; if (!$include_hidden) { $sql .= " WHERE visible=1"; } return (int)$db->result($db->query($sql)); } /** * @return Post[] */ static function getList(int $offset = 0, int $count = -1, bool $include_hidden = false, ?PostLanguage $filter_by_lang = null ): array { $db = DB(); $sql = "SELECT * FROM posts"; if (!$include_hidden) $sql .= " WHERE visible=1"; $sql .= " ORDER BY `date` DESC"; if ($offset != 0 || $count != -1) $sql .= " LIMIT $offset, $count"; $q = $db->query($sql); $posts = []; while ($row = $db->fetch($q)) { $posts[$row['id']] = $row; } if (!empty($posts)) { foreach ($posts as &$post) $post = new Post($post); $q = $db->query("SELECT * FROM posts_texts WHERE post_id IN (".implode(',', array_keys($posts)).")"); while ($row = $db->fetch($q)) { $posts[$row['post_id']]->registerText(new PostText($row)); } } if ($filter_by_lang !== null) $posts = array_filter($posts, fn(Post $post) => $post->hasLang($filter_by_lang)); return array_values($posts); } static function add(array $data = []): ?Post { $db = DB(); if (!$db->insert('posts', $data)) return null; return self::get($db->insertId()); } static function delete(Post $post): void { $db = DB(); $db->query("DELETE FROM posts WHERE id=?", $post->id); $db->query("DELETE FROM posts_texts WHERE post_id=?", $post->id); } 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; } static function getText(int $text_id): ?PostText { $db = DB(); $q = $db->query("SELECT * FROM posts_texts WHERE id=?", $text_id); return $db->numRows($q) ? new PostText($db->fetch($q)) : null; } 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; } static function getPostsById(array $ids, bool $flat = false): array { if (empty($ids)) { return []; } $db = DB(); $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; } static function getPostTextsById(array $ids, bool $flat = false): array { if (empty($ids)) { return []; } $db = DB(); $posts = array_fill_keys($ids, null); $q = $db->query("SELECT * FROM posts_texts WHERE id IN(".implode(',', $ids).")"); while ($row = $db->fetch($q)) { $posts[(int)$row['id']] = new PostText($row); } if ($flat) { $list = []; foreach ($ids as $id) { $list[] = $posts[$id]; } unset($posts); return $list; } return $posts; } /** * @param Upload $upload * @return PostText[] Array of PostTexts that includes specified upload */ static function getTextsWithUpload(Upload $upload): array { $db = DB(); $q = $db->query("SELECT id FROM posts_texts WHERE md LIKE '%{image:{$upload->randomId}%'"); $ids = []; while ($row = $db->fetch($q)) $ids[] = (int)$row['id']; return self::getPostTextsById($ids, true); } }