diff --git a/public/manage/news/categories.php b/public/manage/news/categories.php index cb3a0b8..32f10a7 100644 --- a/public/manage/news/categories.php +++ b/public/manage/news/categories.php @@ -1,7 +1,6 @@ return; } -$categoriesPagination = new Pagination(NewsCategory::countAll(true), 15); +$news = $msz->getNews(); +$pagination = new Pagination($news->countAllCategories(true), 15); -if(!$categoriesPagination->hasValidOffset()) { +if(!$pagination->hasValidOffset()) { echo render_error(404); return; } -$categories = NewsCategory::all($categoriesPagination, true); +$categories = $news->getAllCategories(true, $pagination); Template::render('manage.news.categories', [ 'news_categories' => $categories, - 'categories_pagination' => $categoriesPagination, + 'categories_pagination' => $pagination, ]); diff --git a/public/manage/news/category.php b/public/manage/news/category.php index 12f34f3..668831e 100644 --- a/public/manage/news/category.php +++ b/public/manage/news/category.php @@ -1,9 +1,8 @@ return; } -$categoryId = (int)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT); +$news = $msz->getNews(); +$categoryId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT); +$loadCategoryInfo = fn() => $news->getCategoryById($categoryId); -if($categoryId > 0) +if(empty($categoryId)) + $isNew = true; +else try { - $categoryInfo = NewsCategory::byId($categoryId); - Template::set('category_info', $categoryInfo); - } catch(NewsCategoryNotFoundException $ex) { + $isNew = false; + $categoryInfo = $loadCategoryInfo(); + } catch(RuntimeException $ex) { echo render_error(404); return; } -if(!empty($_POST['category']) && CSRF::validateRequest()) { - if(!isset($categoryInfo)) { - $categoryInfo = new NewsCategory; - $isNew = true; +if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { + if(CSRF::validateRequest()) { + $news->deleteCategory($categoryInfo); + AuditLog::create(AuditLog::NEWS_CATEGORY_DELETE, [$categoryInfo->getId()]); + url_redirect('manage-news-categories'); + } else render_error(403); + return; +} + +while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { + $name = trim((string)filter_input(INPUT_POST, 'nc_name')); + $description = trim((string)filter_input(INPUT_POST, 'nc_desc')); + $hidden = !empty($_POST['nc_hidden']); + + if($isNew) { + $categoryInfo = $news->createCategory($name, $description, $hidden); + } else { + if($name === $categoryInfo->getName()) + $name = null; + if($description === $categoryInfo->getDescription()) + $description = null; + if($hidden === $categoryInfo->isHidden()) + $hidden = null; + + if($name !== null || $description !== null || $hidden !== null) + $news->updateCategory($categoryInfo, $name, $description, $hidden); } - $categoryInfo->setName($_POST['category']['name']) - ->setDescription($_POST['category']['description']) - ->setHidden(!empty($_POST['category']['hidden'])) - ->save(); - AuditLog::create( - empty($isNew) - ? AuditLog::NEWS_CATEGORY_EDIT - : AuditLog::NEWS_CATEGORY_CREATE, + $isNew ? AuditLog::NEWS_CATEGORY_CREATE : AuditLog::NEWS_CATEGORY_EDIT, [$categoryInfo->getId()] ); - if(!empty($isNew)) { - header('Location: ' . url('manage-news-category', ['category' => $categoryInfo->getId()])); + if($isNew) { + url_redirect('manage-news-category', ['category' => $categoryInfo->getId()]); return; - } + } else $categoryInfo = $loadCategoryInfo(); + break; } -Template::render('manage.news.category'); +Template::render('manage.news.category', [ + 'category_new' => $isNew, + 'category_info' => $categoryInfo ?? null, +]); diff --git a/public/manage/news/post.php b/public/manage/news/post.php index 19f5cf0..8969a47 100644 --- a/public/manage/news/post.php +++ b/public/manage/news/post.php @@ -1,10 +1,8 @@ return; } -$postId = (int)filter_input(INPUT_GET, 'p', FILTER_SANITIZE_NUMBER_INT); -if($postId > 0) +$news = $msz->getNews(); +$postId = (string)filter_input(INPUT_GET, 'p', FILTER_SANITIZE_NUMBER_INT); +$loadPostInfo = fn() => $news->getPostById($postId); + +if(empty($postId)) + $isNew = true; +else try { - $postInfo = NewsPost::byId($postId); - Template::set('post_info', $postInfo); - } catch(NewsPostNotFoundException $ex) { + $isNew = false; + $postInfo = $loadPostInfo(); + } catch(RuntimeException $ex) { echo render_error(404); return; } -$categories = NewsCategory::all(null, true); +if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { + if(CSRF::validateRequest()) { + $news->deletePost($postInfo); + AuditLog::create(AuditLog::NEWS_POST_DELETE, [$postInfo->getId()]); + url_redirect('manage-news-posts'); + } else render_error(403); + return; +} -if(!empty($_POST['post']) && CSRF::validateRequest()) { - if(!isset($postInfo)) { - $postInfo = new NewsPost; - $isNew = true; +while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { + $title = trim((string)filter_input(INPUT_POST, 'np_title')); + $category = (string)filter_input(INPUT_POST, 'np_category', FILTER_SANITIZE_NUMBER_INT); + $featured = !empty($_POST['np_featured']); + $body = trim((string)filter_input(INPUT_POST, 'np_body')); + + if($isNew) { + $postInfo = $news->createPost($category, $title, $body, $featured, User::getCurrent()); + } else { + if($category === $postInfo->getCategoryId()) + $category = null; + if($title === $postInfo->getTitle()) + $title = null; + if($body === $postInfo->getBody()) + $body = null; + if($featured === $postInfo->isFeatured()) + $featured = null; + + if($category !== null || $title !== null || $body !== null || $featured !== null) + $news->updatePost($postInfo, $category, $title, $body, $featured); } - $currentUserId = User::getCurrent()->getId(); - $postInfo->setTitle( $_POST['post']['title']) - ->setText($_POST['post']['text']) - ->setCategoryId($_POST['post']['category']) - ->setFeatured(!empty($_POST['post']['featured'])); - - if(!empty($isNew)) - $postInfo->setUserId($currentUserId); - - $postInfo->save(); - AuditLog::create( - empty($isNew) - ? AuditLog::NEWS_POST_EDIT - : AuditLog::NEWS_POST_CREATE, + $isNew ? AuditLog::NEWS_POST_CREATE : AuditLog::NEWS_POST_EDIT, [$postInfo->getId()] ); - if(!empty($isNew)) { + if($isNew) { if($postInfo->isFeatured()) { // Twitter integration used to be here, replace with Railgun Pulse integration } - header('Location: ' . url('manage-news-post', ['post' => $postInfo->getId()])); + url_redirect('manage-news-post', ['post' => $postInfo->getId()]); return; - } + } else $postInfo = $loadPostInfo(); + break; } +$categories = []; +foreach($news->getAllCategories(true) as $categoryInfo) + $categories[$categoryInfo->getId()] = $categoryInfo->getName(); + Template::render('manage.news.post', [ 'categories' => $categories, + 'post_new' => $isNew, + 'post_info' => $postInfo ?? null, ]); diff --git a/public/manage/news/posts.php b/public/manage/news/posts.php index faeb4dd..683bd71 100644 --- a/public/manage/news/posts.php +++ b/public/manage/news/posts.php @@ -1,7 +1,6 @@ return; } -$postsPagination = new Pagination(NewsPost::countAll(false, true, true), 15); +$news = $msz->getNews(); +$pagination = new Pagination($news->countAllPosts( + includeScheduled: true, + includeDeleted: true +), 15); -if(!$postsPagination->hasValidOffset()) { +if(!$pagination->hasValidOffset()) { echo render_error(404); return; } -$posts = NewsPost::all($postsPagination, false, true, true); +$posts = $news->getAllPosts( + includeScheduled: true, + includeDeleted: true, + pagination: $pagination +); Template::render('manage.news.posts', [ 'news_posts' => $posts, - 'posts_pagination' => $postsPagination, + 'posts_pagination' => $pagination, ]); diff --git a/public/search.php b/public/search.php index 88369ef..022f16e 100644 --- a/public/search.php +++ b/public/search.php @@ -1,7 +1,8 @@ getId() : 0); $forumPosts = forum_post_search($searchQuery); - $newsPosts = NewsPost::bySearchQuery($searchQuery); + + // this sure is an expansion + $news = $msz->getNews(); + $newsPosts = []; + $newsPostInfos = $news->getPostsBySearchQuery($searchQuery); + $newsUserInfos = []; + $newsCategoryInfos = []; + + foreach($newsPostInfos as $postInfo) { + $userId = $postInfo->getUserId(); + $categoryId = $postInfo->getCategoryId(); + + if(array_key_exists($userId, $newsUserInfos)) { + $userInfo = $newsUserInfos[$userId]; + } else { + try { + $userInfo = User::byId($userId); + } catch(UserNotFoundException $ex) { + $userInfo = null; + } + + $newsUserInfos[$userId] = $userInfo; + } + + if(array_key_exists($categoryId, $newsCategoryInfos)) + $categoryInfo = $newsCategoryInfos[$categoryId]; + else + $newsCategoryInfos[$categoryId] = $categoryInfo = $news->getCategoryByPost($postInfo); + + $commentsCount = 0; + if($postInfo->hasCommentsCategoryId()) + try { + $commentsCount = CommentsCategory::byId($postInfo->getCommentsCategoryId())->getPostCount(); + } catch(CommentsCategoryNotFoundException $ex) {} + + $newsPosts[] = [ + 'post' => $postInfo, + 'category' => $categoryInfo, + 'user' => $userInfo, + 'comments_count' => $commentsCount, + ]; + } $findUsers = DB::prepare(' SELECT u.`user_id`, u.`username`, u.`user_country`, diff --git a/src/AuditLog.php b/src/AuditLog.php index 912b05e..34bd244 100644 --- a/src/AuditLog.php +++ b/src/AuditLog.php @@ -31,8 +31,10 @@ class AuditLog { public const NEWS_POST_CREATE = 'NEWS_POST_CREATE'; public const NEWS_POST_EDIT = 'NEWS_POST_EDIT'; + public const NEWS_POST_DELETE = 'NEWS_POST_DELETE'; public const NEWS_CATEGORY_CREATE = 'NEWS_CATEGORY_CREATE'; public const NEWS_CATEGORY_EDIT = 'NEWS_CATEGORY_EDIT'; + public const NEWS_CATEGORY_DELETE = 'NEWS_CATEGORY_DELETE'; public const FORUM_TOPIC_DELETE = 'FORUM_TOPIC_DELETE'; public const FORUM_TOPIC_RESTORE = 'FORUM_TOPIC_RESTORE'; @@ -83,8 +85,10 @@ class AuditLog { self::NEWS_POST_CREATE => 'Created news post #%d.', self::NEWS_POST_EDIT => 'Edited news post #%d.', + self::NEWS_POST_DELETE => 'Deleted news post #%d.', self::NEWS_CATEGORY_CREATE => 'Created news category #%d.', self::NEWS_CATEGORY_EDIT => 'Edited news category #%d.', + self::NEWS_CATEGORY_DELETE => 'Deleted news category #%d.', self::FORUM_POST_EDIT => 'Edited forum post #%d.', self::FORUM_POST_DELETE => 'Deleted forum post #%d.', diff --git a/src/Http/Handlers/HomeHandler.php b/src/Http/Handlers/HomeHandler.php index fb1d4d4..229039d 100644 --- a/src/Http/Handlers/HomeHandler.php +++ b/src/Http/Handlers/HomeHandler.php @@ -6,9 +6,11 @@ use Misuzu\Config\IConfig; use Misuzu\DB; use Misuzu\Pagination; use Misuzu\Template; -use Misuzu\News\NewsPost; +use Misuzu\Comments\CommentsCategory; +use Misuzu\Comments\CommentsCategoryNotFoundException; use Misuzu\Users\User; use Misuzu\Users\UserSession; +use Misuzu\Users\UserNotFoundException; final class HomeHandler extends Handler { public function index($response, $request): void { @@ -27,8 +29,10 @@ final class HomeHandler extends Handler { 'same_as' => Config::get('social.linked', IConfig::T_ARR), ] : null; - - $featuredNews = NewsPost::all(new Pagination(3), true); + $featuredNews = $this->context->getNews()->getAllPosts( + onlyFeatured: true, + pagination: new Pagination(3) + ); $stats = DB::query( 'SELECT' @@ -102,7 +106,49 @@ final class HomeHandler extends Handler { } public function home($response, $request): void { - $featuredNews = NewsPost::all(new Pagination(5), true); + $news = $this->context->getNews(); + $featuredNews = []; + $userInfos = []; + $categoryInfos = []; + $featuredNewsInfos = $news->getAllPosts( + onlyFeatured: true, + pagination: new Pagination(5) + ); + + foreach($featuredNewsInfos as $postInfo) { + $userId = $postInfo->getUserId(); + $categoryId = $postInfo->getCategoryId(); + + if(array_key_exists($userId, $userInfos)) { + $userInfo = $userInfos[$userId]; + } else { + try { + $userInfo = User::byId($userId); + } catch(UserNotFoundException $ex) { + $userInfo = null; + } + + $userInfos[$userId] = $userInfo; + } + + if(array_key_exists($categoryId, $categoryInfos)) + $categoryInfo = $categoryInfos[$categoryId]; + else + $categoryInfos[$categoryId] = $categoryInfo = $news->getCategoryByPost($postInfo); + + $commentsCount = 0; + if($postInfo->hasCommentsCategoryId()) + try { + $commentsCount = CommentsCategory::byId($postInfo->getCommentsCategoryId())->getPostCount(); + } catch(CommentsCategoryNotFoundException $ex) {} + + $featuredNews[] = [ + 'post' => $postInfo, + 'category' => $categoryInfo, + 'user' => $userInfo, + 'comments_count' => $commentsCount, + ]; + } $stats = DB::query( 'SELECT' diff --git a/src/Http/Handlers/NewsHandler.php b/src/Http/Handlers/NewsHandler.php index 41ca4fe..56686e2 100644 --- a/src/Http/Handlers/NewsHandler.php +++ b/src/Http/Handlers/NewsHandler.php @@ -1,44 +1,95 @@ context->getNews(); + $posts = []; + $userInfos = []; - if(!$newsPagination->hasValidOffset()) + foreach($postInfos as $postInfo) { + $userId = $postInfo->getUserId(); + $categoryId = $postInfo->getCategoryId(); + + if(array_key_exists($userId, $userInfos)) { + $userInfo = $userInfos[$userId]; + } else { + try { + $userInfo = User::byId($userId); + } catch(UserNotFoundException $ex) { + $userInfo = null; + } + + $userInfos[$userId] = $userInfo; + } + + if(array_key_exists($categoryId, $categoryInfos)) + $categoryInfo = $categoryInfos[$categoryId]; + else + $categoryInfos[$categoryId] = $categoryInfo = $news->getCategoryByPost($postInfo); + + $commentsCount = 0; + if($postInfo->hasCommentsCategoryId()) + try { + $commentsCount = CommentsCategory::byId($postInfo->getCommentsCategoryId())->getPostCount(); + } catch(CommentsCategoryNotFoundException $ex) {} + + $posts[] = [ + 'post' => $postInfo, + 'category' => $categoryInfo, + 'user' => $userInfo, + 'comments_count' => $commentsCount, + ]; + } + + return $posts; + } + + public function index($response, $request) { + $news = $this->context->getNews(); + + $categories = $news->getAllCategories(); + $pagination = new Pagination($news->countAllPosts(onlyFeatured: true), 5); + + if(!$pagination->hasValidOffset()) return 404; + $postInfos = $news->getAllPosts(onlyFeatured: true, pagination: $pagination); + $posts = $this->fetchPostInfo($postInfos); + $response->setContent(Template::renderRaw('news.index', [ - 'categories' => $categories, - 'posts' => NewsPost::all($newsPagination, true), - 'news_pagination' => $newsPagination, + 'news_categories' => $categories, + 'news_posts' => $posts, + 'news_pagination' => $pagination, ])); } public function viewCategory($response, $request, string $fileName) { - $categoryId = (int)pathinfo($fileName, PATHINFO_FILENAME); + $news = $this->context->getNews(); + + $categoryId = pathinfo($fileName, PATHINFO_FILENAME); $type = pathinfo($fileName, PATHINFO_EXTENSION); try { - $categoryInfo = NewsCategory::byId($categoryId); - } catch(NewsCategoryNotFoundException $ex) { + $categoryInfo = $news->getCategoryById($categoryId); + } catch(RuntimeException $ex) { return 404; } @@ -49,41 +100,64 @@ final class NewsHandler extends Handler { elseif($type !== '') return 404; - $categoryPagination = new Pagination(NewsPost::countByCategory($categoryInfo), 5); - if(!$categoryPagination->hasValidOffset()) + $pagination = new Pagination($news->countPostsByCategory($categoryInfo), 5); + if(!$pagination->hasValidOffset()) return 404; + $postInfos = $news->getPostsByCategory($categoryInfo, pagination: $pagination); + $posts = $this->fetchPostInfo($postInfos, [$categoryInfo->getId() => $categoryInfo]); + $response->setContent(Template::renderRaw('news.category', [ - 'category_info' => $categoryInfo, - 'posts' => $categoryInfo->posts($categoryPagination), - 'news_pagination' => $categoryPagination, + 'news_category' => $categoryInfo, + 'news_posts' => $posts, + 'news_pagination' => $pagination, ])); } - public function viewPost($response, $request, int $postId) { + public function viewPost($response, $request, string $postId) { + $news = $this->context->getNews(); + try { - $postInfo = NewsPost::byId($postId); - } catch(NewsPostNotFoundException $ex) { + $postInfo = $news->getPostById($postId); + } catch(RuntimeException $ex) { return 404; } if(!$postInfo->isPublished() || $postInfo->isDeleted()) return 404; - $postInfo->ensureCommentsCategory(); - $commentsInfo = $postInfo->getCommentsCategory(); + $categoryInfo = $news->getCategoryByPost($postInfo); + + if($postInfo->hasCommentsCategoryId()) { + $commentsCategory = CommentsCategory::byId($postInfo->getCommentsCategoryId()); + } else { + $commentsCategoryName = $postInfo->getCommentsCategoryName(); + try { + $commentsCategory = CommentsCategory::byName($commentsCategoryName); + } catch(CommentsCategoryNotFoundException $ex) { + $commentsCategory = new CommentsCategory($commentsCategoryName); + $commentsCategory->save(); + $news->updatePostCommentCategory($postInfo, $commentsCategory); + } + } + + $userInfo = null; + if($postInfo->hasUserId()) + try { + $userInfo = User::byId($postInfo->getUserId()); + } catch(UserNotFoundException $ex) {} $response->setContent(Template::renderRaw('news.post', [ 'post_info' => $postInfo, - 'comments_info' => $commentsInfo, - 'comments_user' => User::getCurrent(), + 'post_category_info' => $categoryInfo, + 'post_user_info' => $userInfo, + 'comments_info' => $commentsCategory, + 'comments_user' => User::getCurrent(), ])); } - private function createFeed(string $feedMode, ?NewsCategory $categoryInfo, array $posts): Feed { - $hasCategory = !empty($categoryInfo); - $pagination = new Pagination(10); - $posts = $hasCategory ? $categoryInfo->posts($pagination) : NewsPost::all($pagination, true); + private function createFeed(string $feedMode, ?NewsCategoryInfo $categoryInfo, array $posts): Feed { + $hasCategory = $categoryInfo !== null; $feed = (new Feed) ->setTitle(Config::get('site.name', IConfig::T_STR, 'Misuzu') . ' » ' . ($hasCategory ? $categoryInfo->getName() : 'Featured News')) @@ -92,19 +166,29 @@ final class NewsHandler extends Handler { ->setFeedUrl(url_prefix(false) . ($hasCategory ? url("news-category-feed-{$feedMode}", ['category' => $categoryInfo->getId()]) : url("news-feed-{$feedMode}"))); foreach($posts as $post) { - $postUrl = url_prefix(false) . url('news-post', ['post' => $post->getId()]); - $commentsUrl = url_prefix(false) . url('news-post-comments', ['post' => $post->getId()]); - $authorUrl = url_prefix(false) . url('user-profile', ['user' => $post->getUser()->getId()]); + $postInfo = $post['post']; + $userInfo = $post['user']; + + $userId = 0; + $userName = 'Author'; + if($userInfo !== null) { + $userId = $userInfo->getId(); + $userName = $userInfo->getUsername(); + } + + $postUrl = url_prefix(false) . url('news-post', ['post' => $postInfo->getId()]); + $commentsUrl = url_prefix(false) . url('news-post-comments', ['post' => $postInfo->getId()]); + $authorUrl = url_prefix(false) . url('user-profile', ['user' => $userId]); $feedItem = (new FeedItem) - ->setTitle($post->getTitle()) - ->setSummary($post->getFirstParagraph()) - ->setContent(Parser::instance(Parser::MARKDOWN)->parseText($post->getText())) - ->setCreationDate($post->getCreatedTime()) + ->setTitle($postInfo->getTitle()) + ->setSummary($postInfo->getFirstParagraph()) + ->setContent(Parser::instance(Parser::MARKDOWN)->parseText($postInfo->getBody())) + ->setCreationDate($postInfo->getCreatedTime()) ->setUniqueId($postUrl) ->setContentUrl($postUrl) ->setCommentsUrl($commentsUrl) - ->setAuthorName($post->getUser()->getUsername()) + ->setAuthorName($userName) ->setAuthorUrl($authorUrl); if(!$feed->hasLastUpdate() || $feed->getLastUpdate() < $feedItem->getCreationDate()) @@ -116,31 +200,75 @@ final class NewsHandler extends Handler { return $feed; } + private function fetchPostInfoForFeed(array $postInfos): array { + $news = $this->context->getNews(); + $posts = []; + $userInfos = []; + + foreach($postInfos as $postInfo) { + $userId = $postInfo->getUserId(); + + if(array_key_exists($userId, $userInfos)) { + $userInfo = $userInfos[$userId]; + } else { + try { + $userInfo = User::byId($userId); + } catch(UserNotFoundException $ex) { + $userInfo = null; + } + + $userInfos[$userId] = $userInfo; + } + + $posts[] = [ + 'post' => $postInfo, + 'user' => $userInfo, + ]; + } + + return $posts; + } + + private function getFeaturedPostsForFeed(): array { + return $this->fetchPostInfoForFeed( + $this->context->getNews()->getAllPosts( + onlyFeatured: true, + pagination: new Pagination(10) + ) + ); + } + public function feedIndexAtom($response, $request) { $response->setContentType('application/atom+xml; charset=utf-8'); return (new AtomFeedSerializer)->serializeFeed( - self::createFeed('atom', null, NewsPost::all(new Pagination(10), true)) + self::createFeed('atom', null, $this->getFeaturedPostsForFeed()) ); } public function feedIndexRss($response, $request) { $response->setContentType('application/rss+xml; charset=utf-8'); return (new RssFeedSerializer)->serializeFeed( - self::createFeed('rss', null, NewsPost::all(new Pagination(10), true)) + self::createFeed('rss', null, $this->getFeaturedPostsForFeed()) ); } - public function feedCategoryAtom($response, $request, NewsCategory $categoryInfo) { + private function getCategoryPostsForFeed(NewsCategoryInfo $categoryInfo): array { + return $this->fetchPostInfoForFeed( + $this->context->getNews()->getPostsByCategory($categoryInfo, pagination: new Pagination(10)) + ); + } + + public function feedCategoryAtom($response, $request, NewsCategoryInfo $categoryInfo) { $response->setContentType('application/atom+xml; charset=utf-8'); return (new AtomFeedSerializer)->serializeFeed( - self::createFeed('atom', $categoryInfo, $categoryInfo->posts(new Pagination(10))) + self::createFeed('atom', $categoryInfo, $this->getCategoryPostsForFeed($categoryInfo)) ); } - public function feedCategoryRss($response, $request, NewsCategory $categoryInfo) { + public function feedCategoryRss($response, $request, NewsCategoryInfo $categoryInfo) { $response->setContentType('application/rss+xml; charset=utf-8'); return (new RssFeedSerializer)->serializeFeed( - self::createFeed('rss', $categoryInfo, $categoryInfo->posts(new Pagination(10))) + self::createFeed('rss', $categoryInfo, $this->getCategoryPostsForFeed($categoryInfo)) ); } } diff --git a/src/MisuzuContext.php b/src/MisuzuContext.php index 3ca607f..eadbf0d 100644 --- a/src/MisuzuContext.php +++ b/src/MisuzuContext.php @@ -5,6 +5,7 @@ use Misuzu\Template; use Misuzu\Changelog\Changelog; use Misuzu\Config\IConfig; use Misuzu\Emoticons\Emotes; +use Misuzu\News\News; use Misuzu\SharpChat\SharpChatRoutes; use Misuzu\Users\Users; use Index\Data\IDbConnection; @@ -25,6 +26,7 @@ class MisuzuContext { private HttpFx $router; private Emotes $emotes; private Changelog $changelog; + private News $news; public function __construct(IDbConnection $dbConn, IConfig $config) { $this->dbConn = $dbConn; @@ -32,6 +34,7 @@ class MisuzuContext { $this->users = new Users($this->dbConn); $this->emotes = new Emotes($this->dbConn); $this->changelog = new Changelog($this->dbConn); + $this->news = new News($this->dbConn); } public function getDbConn(): IDbConnection { @@ -71,6 +74,10 @@ class MisuzuContext { return $this->changelog; } + public function getNews(): News { + return $this->news; + } + public function setUpHttp(bool $legacy = false): void { $this->router = new HttpFx; $this->router->use('/', function($response) { diff --git a/src/News/News.php b/src/News/News.php new file mode 100644 index 0000000..f3a04da --- /dev/null +++ b/src/News/News.php @@ -0,0 +1,481 @@ +dbConn = $dbConn; + $this->cache = new DbStatementCache($dbConn); + } + + private function readCategories(IDbResult $result): array { + $categories = []; + + while($result->next()) + $categories[] = new NewsCategoryInfo($result); + + return $categories; + } + + private function readPosts(IDbResult $result): array { + $posts = []; + + while($result->next()) + $posts[] = new NewsPostInfo($result); + + return $posts; + } + + public function countAllCategories(bool $includeHidden = false): int { + $query = 'SELECT COUNT(*) FROM msz_news_categories'; + if($includeHidden) + $query .= ' WHERE category_is_hidden = 0'; + + $result = $this->dbConn->query($query); + $count = 0; + + if($result->next()) + $count = $result->getInteger(0); + + return $count; + } + + public function getAllCategories( + bool $includeHidden = false, + ?Pagination $pagination = null + ): array { + $hasPagination = $pagination !== null; + + $query = 'SELECT category_id, category_name, category_description, category_is_hidden, UNIX_TIMESTAMP(category_created), (SELECT COUNT(*) FROM msz_news_posts AS np WHERE np.category_id = nc.category_id) AS category_posts_count FROM msz_news_categories AS nc'; + if(!$includeHidden) + $query .= ' WHERE category_is_hidden = 0'; + $query .= ' ORDER BY category_created ASC'; + if($hasPagination) + $query .= ' LIMIT ? OFFSET ?'; + $stmt = $this->cache->get($query); + + $args = 0; + if($hasPagination) { + $stmt->addParameter(++$args, $pagination->getRange()); + $stmt->addParameter(++$args, $pagination->getOffset()); + } + + $stmt->execute(); + + return self::readCategories($stmt->getResult()); + } + + public function getCategoryByPost(NewsPostInfo|string $postInfo): NewsCategoryInfo { + $query = 'SELECT category_id, category_name, category_description, category_is_hidden, UNIX_TIMESTAMP(category_created) FROM msz_news_categories WHERE category_id = '; + + if($postInfo instanceof NewsPostInfo) { + $query .= '?'; + $param = $postInfo->getCategoryId(); + } else { + $query .= '(SELECT category_id FROM msz_news_posts WHERE post_id = ?)'; + $param = $postInfo; + } + + $stmt = $this->cache->get($query); + $stmt->addParameter(1, $param); + $stmt->execute(); + + $result = $stmt->getResult(); + if(!$result->next()) + throw new RuntimeException('No news category associated with that ID exists.'); + + return new NewsCategoryInfo($result); + } + + public function getCategoryById(string $categoryId): NewsCategoryInfo { + $stmt = $this->cache->get('SELECT category_id, category_name, category_description, category_is_hidden, UNIX_TIMESTAMP(category_created) FROM msz_news_categories WHERE category_id = ?'); + $stmt->addParameter(1, $categoryId); + $stmt->execute(); + + $result = $stmt->getResult(); + if(!$result->next()) + throw new RuntimeException('No news category with that ID exists.'); + + return new NewsCategoryInfo($result); + } + + public function createCategory( + string $name, + string $description, + bool $hidden + ): NewsCategoryInfo { + $name = trim($name); + if(empty($name)) + throw new InvalidArgumentException('$name may not be empty'); + + $description = trim($description); + if(empty($description)) + throw new InvalidArgumentException('$description may not be empty'); + + $stmt = $this->cache->get('INSERT INTO msz_news_categories (category_name, category_description, category_is_hidden) VALUES (?, ?, ?)'); + $stmt->addParameter(1, $name); + $stmt->addParameter(2, $description); + $stmt->addParameter(3, $hidden ? 1 : 0); + $stmt->execute(); + + return $this->getCategoryById((string)$this->dbConn->getLastInsertId()); + } + + public function deleteCategory(NewsCategoryInfo|string $infoOrId): void { + if($infoOrId instanceof NewsCategoryInfo) + $infoOrId = $infoOrId->getId(); + + $stmt = $this->cache->get('DELETE FROM msz_news_categories WHERE category_id = ?'); + $stmt->addParameter(1, $infoOrId); + $stmt->execute(); + } + + public function updateCategory( + NewsCategoryInfo|string $infoOrId, + ?string $name = null, + ?string $description = null, + ?bool $hidden = null + ): void { + if($infoOrId instanceof NewsCategoryInfo) + $infoOrId = $infoOrId->getId(); + + if($name !== null) { + $name = trim($name); + if(empty($name)) + throw new InvalidArgumentException('$name may not be empty'); + } + + if($description !== null) { + $description = trim($description); + if(empty($description)) + throw new InvalidArgumentException('$description may not be empty'); + } + + $hasHidden = $hidden !== null; + + $stmt = $this->cache->get('UPDATE msz_news_categories SET category_name = COALESCE(?, category_name), category_description = COALESCE(?, category_description), category_is_hidden = IF(?, ?, category_is_hidden) WHERE category_id = ?'); + $stmt->addParameter(1, $name); + $stmt->addParameter(2, $description); + $stmt->addParameter(3, $hasHidden ? 1 : 0); + $stmt->addParameter(4, $hidden ? 1 : 0); + $stmt->addParameter(5, $infoOrId); + $stmt->execute(); + } + + public function countAllPosts( + bool $onlyFeatured = false, + bool $includeScheduled = false, + bool $includeDeleted = false + ): int { + $args = 0; + $query = 'SELECT COUNT(*) FROM msz_news_posts'; + if($onlyFeatured) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= ' post_is_featured = 1'; + } + if(!$includeScheduled) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= ' post_scheduled <= NOW()'; + } + if(!$includeDeleted) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= ' post_deleted IS NULL'; + } + + $result = $this->dbConn->query($query); + $count = 0; + + if($result->next()) + $count = $result->getInteger(0); + + return $count; + } + + public function countPostsByCategory( + NewsCategoryInfo|string $categoryInfo, + bool $onlyFeatured = false, + bool $includeScheduled = false, + bool $includeDeleted = false + ): int { + if($categoryInfo instanceof NewsCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + + $query = 'SELECT COUNT(*) FROM msz_news_posts WHERE category_id = ?'; + if($onlyFeatured) + $query .= ' AND post_is_featured = 1'; + if(!$includeScheduled) + $query .= ' AND post_scheduled <= NOW()'; + if(!$includeDeleted) + $query .= ' AND post_deleted IS NULL'; + + $stmt = $this->cache->get($query); + $stmt->addParameter(1, $categoryInfo); + $stmt->execute(); + + $result = $stmt->getResult(); + $count = 0; + + if($result->next()) + $count = $result->getInteger(0); + + return $count; + } + + private const POSTS_SELECT_QUERY = 'SELECT post_id, category_id, user_id, comment_section_id, post_is_featured, post_title, post_text, UNIX_TIMESTAMP(post_scheduled), UNIX_TIMESTAMP(post_created), UNIX_TIMESTAMP(post_updated), UNIX_TIMESTAMP(post_deleted) FROM msz_news_posts'; + private const POSTS_SELECT_ORDER = ' ORDER BY post_scheduled DESC'; + + public function getAllPosts( + bool $onlyFeatured = false, + bool $includeScheduled = false, + bool $includeDeleted = false, + ?Pagination $pagination = null + ): array { + $args = 0; + $hasPagination = $pagination !== null; + + $query = self::POSTS_SELECT_QUERY; + if($onlyFeatured) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= ' post_is_featured = 1'; + } + if(!$includeScheduled) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= ' post_scheduled <= NOW()'; + } + if(!$includeDeleted) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= ' post_deleted IS NULL'; + } + $query .= self::POSTS_SELECT_ORDER; + if($hasPagination) + $query .= ' LIMIT ? OFFSET ?'; + $stmt = $this->cache->get($query); + + $args = 0; + if($hasPagination) { + $stmt->addParameter(++$args, $pagination->getRange()); + $stmt->addParameter(++$args, $pagination->getOffset()); + } + + $stmt->execute(); + + return self::readPosts($stmt->getResult()); + } + + public function getPostsByCategory( + NewsCategoryInfo|string $categoryInfo, + bool $onlyFeatured = false, + bool $includeScheduled = false, + bool $includeDeleted = false, + ?Pagination $pagination = null + ): array { + if($categoryInfo instanceof NewsCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + + $hasPagination = $pagination !== null; + + $query = self::POSTS_SELECT_QUERY; + $query .= ' WHERE category_id = ?'; + if($onlyFeatured) + $query .= ' AND post_is_featured = 1'; + if(!$includeScheduled) + $query .= ' AND post_scheduled <= NOW()'; + if(!$includeDeleted) + $query .= ' AND post_deleted IS NULL'; + $query .= self::POSTS_SELECT_ORDER; + if($hasPagination) + $query .= ' LIMIT ? OFFSET ?'; + $stmt = $this->cache->get($query); + + $args = 0; + $stmt->addParameter(++$args, $categoryInfo); + if($hasPagination) { + $stmt->addParameter(++$args, $pagination->getRange()); + $stmt->addParameter(++$args, $pagination->getOffset()); + } + + $stmt->execute(); + + return self::readPosts($stmt->getResult()); + } + + public function getPostsBySearchQuery( + string $searchQuery, + bool $includeScheduled = false, + bool $includeDeleted = false, + ?Pagination $pagination = null + ): array { + $hasPagination = $pagination !== null; + + $query = self::POSTS_SELECT_QUERY; + $query .= ' WHERE MATCH(post_title, post_text) AGAINST (? IN NATURAL LANGUAGE MODE)'; + if(!$includeScheduled) + $query .= ' AND post_scheduled <= NOW()'; + if(!$includeDeleted) + $query .= ' AND post_deleted IS NULL'; + $query .= self::POSTS_SELECT_ORDER; + if($hasPagination) + $query .= ' LIMIT ? OFFSET ?'; + $stmt = $this->cache->get($query); + + $args = 0; + $stmt->addParameter(++$args, $searchQuery); + if($hasPagination) { + $stmt->addParameter(++$args, $pagination->getRange()); + $stmt->addParameter(++$args, $pagination->getOffset()); + } + + $stmt->execute(); + + return self::readPosts($stmt->getResult()); + } + + public function getPostById(string $postId): NewsPostInfo { + $stmt = $this->cache->get('SELECT post_id, category_id, user_id, comment_section_id, post_is_featured, post_title, post_text, UNIX_TIMESTAMP(post_scheduled), UNIX_TIMESTAMP(post_created), UNIX_TIMESTAMP(post_updated), UNIX_TIMESTAMP(post_deleted) FROM msz_news_posts WHERE post_id = ?'); + $stmt->addParameter(1, $postId); + $stmt->execute(); + + $result = $stmt->getResult(); + if(!$result->next()) + throw new RuntimeException('No news post with that ID exists.'); + + return new NewsPostInfo($result); + } + + public function createPost( + NewsCategoryInfo|string $categoryInfo, + string $title, + string $body, + bool $featured = false, + User|string|null $userInfo = null, + DateTime|int|null $schedule = null + ): NewsPostInfo { + if($categoryInfo instanceof NewsCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + if($userInfo instanceof User) + $userInfo = (string)$userInfo->getId(); + if($schedule instanceof DateTime) + $schedule = $schedule->getUnixTimeSeconds(); + + $title = trim($title); + if(empty($title)) + throw new InvalidArgumentException('$title may not be empty'); + + $body = trim($body); + if(empty($body)) + throw new InvalidArgumentException('$body may not be empty'); + + $stmt = $this->cache->get('INSERT INTO msz_news_posts (category_id, user_id, post_is_featured, post_title, post_text, post_scheduled) VALUES (?, ?, ?, ?, ?, ?)'); + $stmt->addParameter(1, $categoryInfo); + $stmt->addParameter(2, $userInfo); + $stmt->addParameter(3, $featured ? 1 : 0); + $stmt->addParameter(4, $title); + $stmt->addParameter(5, $body); + $stmt->addParameter(6, $schedule); + $stmt->execute(); + + return $this->getPostById((string)$this->dbConn->getLastInsertId()); + } + + public function deletePost(NewsPostInfo|string $postInfo): void { + if($postInfo instanceof NewsPostInfo) + $postInfo = $postInfo->getId(); + + $stmt = $this->cache->get('UPDATE msz_news_posts SET post_deleted = COALESCE(post_deleted, NOW()) WHERE post_id = ?'); + $stmt->addParameter(1, $postInfo); + $stmt->execute(); + } + + public function restorePost(NewsPostInfo|string $postInfo): void { + if($postInfo instanceof NewsPostInfo) + $postInfo = $postInfo->getId(); + + $stmt = $this->cache->get('UPDATE msz_news_posts SET post_deleted = NULL WHERE post_id = ?'); + $stmt->addParameter(1, $postInfo); + $stmt->execute(); + } + + public function nukePost(NewsPostInfo|string $postInfo): void { + if($postInfo instanceof NewsPostInfo) + $postInfo = $postInfo->getId(); + + // should this enforce a soft delete first? (AND post_deleted IS NOT NULL) + $stmt = $this->cache->get('DELETE FROM msz_news_posts WHERE post_id = ?'); + $stmt->addParameter(1, $postInfo); + $stmt->execute(); + } + + public function updatePost( + NewsPostInfo|string $postInfo, + NewsCategoryInfo|string|null $categoryInfo = null, + ?string $title = null, + ?string $body = null, + ?bool $featured = null, + bool $updateUserInfo = false, + User|string|null $userInfo = null, + DateTime|int|null $schedule = null + ): void { + if($postInfo instanceof NewsPostInfo) + $postInfo = $postInfo->getId(); + if($categoryInfo instanceof NewsCategoryInfo) + $categoryInfo = $categoryInfo->getId(); + if($userInfo instanceof User) + $userInfo = (string)$userInfo->getId(); + if($schedule instanceof DateTime) + $schedule = $schedule->getUnixTimeSeconds(); + + if($title !== null) { + $title = trim($title); + if(empty($title)) + throw new InvalidArgumentException('$title may not be empty'); + } + + if($body !== null) { + $body = trim($body); + if(empty($body)) + throw new InvalidArgumentException('$body may not be empty'); + } + + $hasFeatured = $featured !== null; + + $stmt = $this->cache->get('UPDATE msz_news_posts SET category_id = COALESCE(?, category_id), user_id = IF(?, ?, user_id), post_is_featured = IF(?, ?, post_is_featured), post_title = COALESCE(?, post_title), post_text = COALESCE(?, post_text), post_scheduled = COALESCE(?, post_scheduled) WHERE post_id = ?'); + $stmt->addParameter(1, $categoryInfo); + $stmt->addParameter(2, $updateUserInfo ? 1 : 0); + $stmt->addParameter(3, $userInfo); + $stmt->addParameter(4, $hasFeatured ? 1 : 0); + $stmt->addParameter(5, $featured ? 1 : 0); + $stmt->addParameter(6, $title); + $stmt->addParameter(7, $body); + $stmt->addParameter(8, $schedule); + $stmt->addParameter(9, $postInfo); + $stmt->execute(); + } + + public function updatePostCommentCategory( + NewsPostInfo|string $postInfo, + CommentsCategory|string $commentsCategory + ): void { + if($postInfo instanceof NewsPostInfo) + $postInfo = $postInfo->getId(); + if($commentsCategory instanceof CommentsCategory) + $commentsCategory = (string)$commentsCategory->getId(); + + // "post_updated = post_updated" is an Attempt at making this not bump post_updated ON UPDATE + $stmt = $this->cache->get('UPDATE msz_news_posts SET comment_section_id = ?, post_updated = post_updated WHERE post_id = ?'); + $stmt->addParameter(1, $postInfo); + $stmt->addParameter(2, $commentsCategory); + $stmt->execute(); + } +} diff --git a/src/News/NewsCategory.php b/src/News/NewsCategory.php deleted file mode 100644 index 6bd4087..0000000 --- a/src/News/NewsCategory.php +++ /dev/null @@ -1,148 +0,0 @@ -category_id < 1 ? -1 : $this->category_id; - } - - public function getName(): string { - return $this->category_name ?? ''; - } - public function setName(string $name): self { - $this->category_name = $name; - return $this; - } - - public function getDescription(): string { - return $this->category_description ?? ''; - } - public function setDescription(string $description): self { - $this->category_description = $description; - return $this; - } - - public function isHidden(): bool { - return $this->category_is_hidden !== 0; - } - public function setHidden(bool $hide): self { - $this->category_is_hidden = $hide ? 1 : 0; - return $this; - } - - public function getCreatedTime(): int { - return $this->category_created === null ? -1 : $this->category_created; - } - - // Purely cosmetic, use ::countAll for pagination - public function getPostCount(): int { - if($this->postCount < 0) - $this->postCount = (int)DB::prepare(' - SELECT COUNT(`post_id`) - FROM `msz_news_posts` - WHERE `category_id` = :cat_id - AND `post_scheduled` <= NOW() - AND `post_deleted` IS NULL - ')->bind('cat_id', $this->getId())->fetchColumn(); - - return $this->postCount; - } - - public function save(): void { - $isInsert = $this->getId() < 1; - if($isInsert) { - $query = 'INSERT INTO `%1$s%2$s` (`category_name`, `category_description`, `category_is_hidden`) VALUES' - . ' (:name, :description, :hidden)'; - } else { - $query = 'UPDATE `%1$s%2$s` SET `category_name` = :name, `category_description` = :description, `category_is_hidden` = :hidden' - . ' WHERE `category_id` = :category'; - } - - $savePost = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE)) - ->bind('name', $this->category_name) - ->bind('description', $this->category_description) - ->bind('hidden', $this->category_is_hidden); - - if($isInsert) { - $this->category_id = $savePost->executeGetId(); - $this->category_created = time(); - } else { - $savePost->bind('category', $this->getId()) - ->execute(); - } - } - - public function posts(?Pagination $pagination = null, bool $includeScheduled = false, bool $includeDeleted = false): array { - return NewsPost::byCategory($this, $pagination, $includeScheduled, $includeDeleted); - } - - private static function countQueryBase(): string { - return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`category_id`)', self::TABLE)); - } - public static function countAll(bool $showHidden = false): int { - return (int)DB::prepare(self::countQueryBase() - . ($showHidden ? '' : ' WHERE `category_is_hidden` = 0')) - ->fetchColumn(); - } - - private static function byQueryBase(): string { - return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE)); - } - public static function byId(int $categoryId): self { - $getCat = DB::prepare(self::byQueryBase() . ' WHERE `category_id` = :cat_id'); - $getCat->bind('cat_id', $categoryId); - $cat = $getCat->fetchObject(self::class); - if(!$cat) - throw new NewsCategoryNotFoundException; - return $cat; - } - public static function all(?Pagination $pagination = null, bool $showHidden = false): array { - $catsQuery = self::byQueryBase() - . ($showHidden ? '' : ' WHERE `category_is_hidden` = 0') - . ' ORDER BY `category_id` ASC'; - - if($pagination !== null) - $catsQuery .= ' LIMIT :range OFFSET :offset'; - - $getCats = DB::prepare($catsQuery); - - if($pagination !== null) - $getCats->bind('range', $pagination->getRange()) - ->bind('offset', $pagination->getOffset()); - - return $getCats->fetchObjects(self::class); - } - - // Twig shim for the news category list in manage, don't use this class as an array normally. - public function offsetExists($offset): bool { - return $offset === 'name' || $offset === 'id'; - } - public function offsetGet($offset): mixed { - return $this->{'get' . ucfirst($offset)}(); - } - public function offsetSet($offset, $value): void {} - public function offsetUnset($offset): void {} -} diff --git a/src/News/NewsCategoryInfo.php b/src/News/NewsCategoryInfo.php new file mode 100644 index 0000000..2edc52b --- /dev/null +++ b/src/News/NewsCategoryInfo.php @@ -0,0 +1,51 @@ +id = (string)$result->getInteger(0); + $this->name = $result->getString(1); + $this->description = $result->getString(2); + $this->hidden = $result->getInteger(3) !== 0; + $this->created = $result->getInteger(4); + $this->posts = $result->getInteger(5); + } + + public function getId(): string { + return $this->id; + } + + public function getName(): string { + return $this->name; + } + + public function getDescription(): string { + return $this->description; + } + + public function isHidden(): bool { + return $this->hidden; + } + + public function getCreatedTime(): int { + return $this->created; + } + + public function getCreatedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->created); + } + + public function getPostsCount(): int { + return $this->posts; + } +} diff --git a/src/News/NewsException.php b/src/News/NewsException.php deleted file mode 100644 index faccdd3..0000000 --- a/src/News/NewsException.php +++ /dev/null @@ -1,6 +0,0 @@ -post_id < 1 ? -1 : $this->post_id; - } - - public function getCategoryId(): int { - return $this->category_id < 1 ? -1 : $this->category_id; - } - public function setCategoryId(int $categoryId): self { - $this->category_id = max(1, $categoryId); - $this->category = null; - return $this; - } - public function getCategory(): NewsCategory { - if($this->category === null) - $this->category = NewsCategory::byId($this->getCategoryId()); - return $this->category; - } - public function setCategory(NewsCategory $category): self { - $this->category_id = $category->getId(); - $this->category = $category; - return $this; - } - - public function getUserId(): int { - return $this->user_id < 1 ? -1 : $this->user_id; - } - public function setUserId(int $userId): self { - $this->user_id = $userId < 1 ? null : $userId; - $this->userLookedUp = false; - $this->user = null; - return $this; - } - public function getUser(): ?User { - if(!$this->userLookedUp && ($userId = $this->getUserId()) > 0) { - $this->userLookedUp = true; - try { - $this->user = User::byId($userId); - } catch(UserNotFoundException $ex) {} - } - return $this->user; - } - public function setUser(?User $user): self { - $this->user_id = $user === null ? null : $user->getId(); - $this->userLookedUp = true; - $this->user = $user; - return $this; - } - - public function getCommentsCategoryId(): int { - return $this->comment_section_id < 1 ? -1 : $this->comment_section_id; - } - public function hasCommentsCategory(): bool { - return $this->getCommentsCategoryId() > 0; - } - public function getCommentsCategory(): CommentsCategory { - if($this->comments === null) - $this->comments = CommentsCategory::byId($this->getCommentsCategoryId()); - return $this->comments; - } - - public function isFeatured(): bool { - return $this->post_is_featured !== 0; - } - public function setFeatured(bool $featured): self { - $this->post_is_featured = $featured ? 1 : 0; - return $this; - } - - public function getTitle(): string { - return $this->post_title; - } - public function setTitle(string $title): self { - $this->post_title = $title; - return $this; - } - - public function getText(): string { - return $this->post_text; - } - public function setText(string $text): self { - $this->post_text = $text; - return $this; - } - public function getParsedText(): string { - return Parser::instance(Parser::MARKDOWN)->parseText($this->getText()); - } - public function getFirstParagraph(): string { - $text = $this->getText(); - $index = mb_strpos($text, "\n"); - return $index === false ? $text : mb_substr($text, 0, $index); - } - public function getParsedFirstParagraph(): string { - return Parser::instance(Parser::MARKDOWN)->parseText($this->getFirstParagraph()); - } - - public function getScheduledTime(): int { - return $this->post_scheduled === null ? -1 : $this->post_scheduled; - } - public function setScheduledTime(int $scheduled): self { - $time = ($time = $this->getCreatedTime()) < 0 ? time() : $time; - $this->post_scheduled = $scheduled < $time ? $time : $scheduled; - return $this; - } - public function isPublished(): bool { - return $this->getScheduledTime() < time(); - } - - public function getCreatedTime(): int { - return $this->post_created === null ? -1 : $this->post_created; - } - - public function getUpdatedTime(): int { - return $this->post_updated === null ? -1 : $this->post_updated; - } - public function isEdited(): bool { - return $this->getUpdatedTime() >= 0; - } - - public function getDeletedTime(): int { - return $this->post_deleted === null ? -1 : $this->post_deleted; - } - public function isDeleted(): bool { - return $this->getDeletedTime() >= 0; - } - public function setDeleted(bool $isDeleted): self { - if($this->isDeleted() !== $isDeleted) - $this->post_deleted = $isDeleted ? time() : null; - return $this; - } - - public function ensureCommentsCategory(): void { - if($this->hasCommentsCategory()) - return; - - $this->comments = new CommentsCategory("news-{$this->getId()}"); - $this->comments->save(); - - $this->comment_section_id = $this->comments->getId(); - DB::prepare('UPDATE `msz_news_posts` SET `comment_section_id` = :comment_section_id WHERE `post_id` = :post_id') - ->execute([ - 'comment_section_id' => $this->getCommentsCategoryId(), - 'post_id' => $this->getId(), - ]); - } - - public function save(): void { - $isInsert = $this->getId() < 1; - if($isInsert) { - $query = 'INSERT INTO `%1$s%2$s` (`category_id`, `user_id`, `post_is_featured`, `post_title`' - . ', `post_text`, `post_scheduled`, `post_deleted`) VALUES' - . ' (:category, :user, :featured, :title, :text, FROM_UNIXTIME(:scheduled), FROM_UNIXTIME(:deleted))'; - } else { - $query = 'UPDATE `%1$s%2$s` SET `category_id` = :category, `user_id` = :user, `post_is_featured` = :featured' - . ', `post_title` = :title, `post_text` = :text, `post_scheduled` = FROM_UNIXTIME(:scheduled)' - . ', `post_deleted` = FROM_UNIXTIME(:deleted)' - . ' WHERE `post_id` = :post'; - } - - $savePost = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE)) - ->bind('category', $this->category_id) - ->bind('user', $this->user_id) - ->bind('featured', $this->post_is_featured) - ->bind('title', $this->post_title) - ->bind('text', $this->post_text) - ->bind('scheduled', $this->post_scheduled) - ->bind('deleted', $this->post_deleted); - - if($isInsert) { - $this->post_id = $savePost->executeGetId(); - $this->post_created = time(); - } else { - $this->post_updated = time(); - $savePost->bind('post', $this->getId()) - ->execute(); - } - } - - private static function countQueryBase(): string { - return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`post_id`)', self::TABLE)); - } - public static function countAll(bool $onlyFeatured = false, bool $includeScheduled = false, bool $includeDeleted = false): int { - return (int)DB::prepare(self::countQueryBase() - . ' WHERE IF(:only_featured, `post_is_featured` <> 0, 1)' - . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()') - . ($includeDeleted ? '' : ' AND `post_deleted` IS NULL')) - ->bind('only_featured', $onlyFeatured ? 1 : 0) - ->fetchColumn(); - } - public static function countByCategory(NewsCategory $category, bool $includeScheduled = false, bool $includeDeleted = false): int { - return (int)DB::prepare(self::countQueryBase() - . ' WHERE `category_id` = :cat_id' - . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()') - . ($includeDeleted ? '' : ' AND `post_deleted` IS NULL')) - ->bind('cat_id', $category->getId()) - ->fetchColumn(); - } - - private static function byQueryBase(): string { - return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE)); - } - public static function byId(int $postId): self { - $post = DB::prepare(self::byQueryBase() . ' WHERE `post_id` = :post_id') - ->bind('post_id', $postId) - ->fetchObject(self::class); - if(!$post) - throw new NewsPostNotFoundException; - return $post; - } - public static function bySearchQuery(string $query, bool $includeScheduled = false, bool $includeDeleted = false): array { - return DB::prepare( - self::byQueryBase() - . ' WHERE MATCH(`post_title`, `post_text`) AGAINST (:query IN NATURAL LANGUAGE MODE)' - . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()') - . ($includeDeleted ? '' : ' AND `post_deleted` IS NULL') - . ' ORDER BY `post_id` DESC' - ) ->bind('query', $query) - ->fetchObjects(self::class); - } - public static function byCategory(NewsCategory $category, ?Pagination $pagination = null, bool $includeScheduled = false, bool $includeDeleted = false): array { - $postsQuery = self::byQueryBase() - . ' WHERE `category_id` = :cat_id' - . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()') - . ($includeDeleted ? '' : ' AND `post_deleted` IS NULL') - . ' ORDER BY `post_id` DESC'; - - if($pagination !== null) - $postsQuery .= ' LIMIT :range OFFSET :offset'; - - $getPosts = DB::prepare($postsQuery) - ->bind('cat_id', $category->getId()); - - if($pagination !== null) - $getPosts->bind('range', $pagination->getRange()) - ->bind('offset', $pagination->getOffset()); - - return $getPosts->fetchObjects(self::class); - } - public static function all(?Pagination $pagination = null, bool $onlyFeatured = false, bool $includeScheduled = false, bool $includeDeleted = false): array { - $postsQuery = self::byQueryBase() - . ' WHERE IF(:only_featured, `post_is_featured` <> 0, 1)' - . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()') - . ($includeDeleted ? '' : ' AND `post_deleted` IS NULL') - . ' ORDER BY `post_id` DESC'; - - if($pagination !== null) - $postsQuery .= ' LIMIT :range OFFSET :offset'; - - $getPosts = DB::prepare($postsQuery) - ->bind('only_featured', $onlyFeatured ? 1 : 0); - - if($pagination !== null) - $getPosts->bind('range', $pagination->getRange()) - ->bind('offset', $pagination->getOffset()); - - return $getPosts->fetchObjects(self::class); - } -} diff --git a/src/News/NewsPostInfo.php b/src/News/NewsPostInfo.php new file mode 100644 index 0000000..5f9db32 --- /dev/null +++ b/src/News/NewsPostInfo.php @@ -0,0 +1,122 @@ +id = (string)$result->getInteger(0); + $this->categoryId = (string)$result->getInteger(1); + $this->userId = $result->isNull(2) ? null : (string)$result->getInteger(2); + $this->commentsSectionId = $result->isNull(3) ? null : (string)$result->getInteger(3); + $this->featured = $result->getInteger(4) !== 0; + $this->title = $result->getString(5); + $this->body = $result->getString(6); + $this->scheduled = $result->getInteger(7); + $this->created = $result->getInteger(8); + $this->updated = $result->getInteger(9); + $this->deleted = $result->isNull(10) ? null : $result->getInteger(10); + } + + public function getId(): string { + return $this->id; + } + + public function getCategoryId(): string { + return $this->categoryId; + } + + public function hasUserId(): bool { + return $this->userId !== null; + } + + public function getUserId(): ?string { + return $this->userId; + } + + public function hasCommentsCategoryId(): bool { + return $this->commentsSectionId !== null; + } + + public function getCommentsCategoryId(): string { + return $this->commentsSectionId; + } + + public function getCommentsCategoryName(): string { + return sprintf('news-%s', $this->id); + } + + public function isFeatured(): bool { + return $this->featured; + } + + public function getTitle(): string { + return $this->title; + } + + public function getBody(): string { + return $this->body; + } + + public function getFirstParagraph(): string { + $index = mb_strpos($this->body, "\n"); + return $index === false ? $this->body : mb_substr($this->body, 0, $index); + } + + public function getScheduledTime(): int { + return $this->scheduled; + } + + public function getScheduledAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->scheduled); + } + + public function isPublished(): bool { + return $this->scheduled <= time(); + } + + public function getCreatedTime(): int { + return $this->created; + } + + public function getCreatedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->created); + } + + public function getUpdatedTime(): int { + return $this->updated; + } + + public function getUpdatedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->updated); + } + + public function isEdited(): bool { + return $this->updated > $this->created; + } + + public function isDeleted(): bool { + return $this->deleted !== null; + } + + public function getDeletedTime(): ?int { + return $this->deleted; + } + + public function getDeletedAt(): ?DateTime { + return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted); + } +} diff --git a/src/url.php b/src/url.php index c56a8fd..b2593f3 100644 --- a/src/url.php +++ b/src/url.php @@ -117,8 +117,10 @@ define('MSZ_URLS', [ 'manage-news-categories' => ['/manage/news/categories.php'], 'manage-news-category' => ['/manage/news/category.php', ['c' => '']], + 'manage-news-category-delete' => ['/manage/news/category.php', ['c' => '', 'delete' => '1', 'csrf' => '{token}']], 'manage-news-posts' => ['/manage/news/posts.php'], 'manage-news-post' => ['/manage/news/post.php', ['p' => '']], + 'manage-news-post-delete' => ['/manage/news/post.php', ['p' => '', 'delete' => '1', 'csrf' => '{token}']], 'manage-users' => ['/manage/users'], 'manage-user' => ['/manage/users/user.php', ['u' => '']], diff --git a/templates/home/landing.twig b/templates/home/landing.twig index fa4b1b4..cdf7c76 100644 --- a/templates/home/landing.twig +++ b/templates/home/landing.twig @@ -171,11 +171,9 @@ {% for post in featured_news %}

{{ post.title }}

-

{{ post.parsedFirstParagraph|raw }}

+

{{ post.firstParagraph|parse_text(2)|raw }}

Continue reading - | - {{ not post.hasCommentsCategory or post.commentsCategory.postCount < 1 ? 'No' : post.commentsCategory.postCount|number_format }} comment{{ not post.hasCommentsCategory or post.commentsCategory.postCount != 1 ? 's' : '' }} | {{ post.createdTime|time_diff }}
diff --git a/templates/manage/news/categories.twig b/templates/manage/news/categories.twig index 995c502..352075f 100644 --- a/templates/manage/news/categories.twig +++ b/templates/manage/news/categories.twig @@ -9,10 +9,10 @@ {% for cat in news_categories %}

- {{ cat.id }} - {{ cat.name }}, - {{ cat.isHidden }}, - {{ cat.createdTime|date('r') }} + #{{ cat.id }} + {{ cat.name }} | + {{ cat.isHidden ? 'Unlisted' : 'Public' }} | + {{ cat.createdAt }}

{% endfor %} diff --git a/templates/manage/news/category.twig b/templates/manage/news/category.twig index 2d078c4..6ae94b6 100644 --- a/templates/manage/news/category.twig +++ b/templates/manage/news/category.twig @@ -2,32 +2,33 @@ {% from 'macros.twig' import container_title %} {% from '_layout/input.twig' import input_hidden, input_csrf, input_text, input_checkbox %} -{% set is_new = category is not defined %} - {% block manage_content %}
- {{ container_title(is_new ? 'New Category' : 'Editing ' ~ category_info.name) }} - + {{ container_title(category_new ? 'New Category' : 'Editing ' ~ category_info.name) }} {{ input_csrf() }} - {{ input_hidden('category[id]', category_info.id|default(0)) }} - + - + - +
Name{{ input_text('category[name]', '', category_info.name|default(), 'text', '', true) }}{{ input_text('nc_name', '', category_info.name|default(), 'text', '', true) }}
Description
Is Hidden{{ input_checkbox('category[hidden]', '', category_info.isHidden|default(false)) }}{{ input_checkbox('nc_hidden', '', category_info.isHidden|default(false)) }}
- +
+ + {% if not category_new %} + Delete + {% endif %} +
{% endblock %} diff --git a/templates/manage/news/post.twig b/templates/manage/news/post.twig index 2cb3d59..1c3f46b 100644 --- a/templates/manage/news/post.twig +++ b/templates/manage/news/post.twig @@ -2,36 +2,37 @@ {% from 'macros.twig' import container_title %} {% from '_layout/input.twig' import input_hidden, input_csrf, input_text, input_checkbox, input_select %} -{% set is_new = post_info is not defined %} - {% block manage_content %}
- {{ container_title(is_new ? 'New Post' : 'Editing ' ~ post_info.title) }} - + {{ container_title(post_new ? 'New Post' : 'Editing ' ~ post_info.title) }} {{ input_csrf() }} - {{ input_hidden('post[id]', post_info.id|default(0)) }} - + - + - + - +
Name{{ input_text('post[title]', '', post_info.title|default(), 'text', '', true) }}{{ input_text('np_title', '', post_info.title|default(), 'text', '', true) }}
Category{{ input_select('post[category]', categories, post_info.categoryId|default(0), 'name', 'id') }}{{ input_select('np_category', categories, post_info.categoryId|default(0)) }}
Is Featured{{ input_checkbox('post[featured]', '', post_info.isFeatured|default(false)) }}{{ input_checkbox('np_featured', '', post_info.isFeatured|default(false)) }}
- +
+ + {% if not post_new %} + Delete + {% endif %} +
{% endblock %} diff --git a/templates/manage/news/posts.twig b/templates/manage/news/posts.twig index d7abc80..a3f72d1 100644 --- a/templates/manage/news/posts.twig +++ b/templates/manage/news/posts.twig @@ -9,16 +9,16 @@ {% for post in news_posts %}

- {{ post.id }} - Cat: {{ post.categoryId }} - {{ post.isFeatured }}, - {{ post.user.id }}, - {{ post.title }}, - {{ post.scheduledTime|date('r') }}, - {{ post.createdTime|date('r') }}, - {{ post.updatedTime|date('r') }}, - {{ post.deletedTime|date('r') }}, - {{ post.commentsCategoryId }} + #{{ post.id }} + Category #{{ post.categoryId }} + {{ post.title }} | + {{ post.isFeatured ? 'Featured' : 'Normal' }} | + User #{{ post.userId }} | + {% if post.hasCommentsCategoryId %}Comments category #{{ post.commentsCategoryId }}{% else %}No comments category{% endif %} | + Created {{ post.createdAt }} | + {{ post.isPublished ? 'published' : 'Published ' ~ post.scheduledAt }} | + {{ post.isEdited ? 'Edited ' ~ post.updatedAt : 'not edited' }} | + {{ post.isDeleted ? 'Deleted ' ~ post.deletedAt : 'not deleted' }}

{% endfor %} diff --git a/templates/news/category.twig b/templates/news/category.twig index afe804b..19581f9 100644 --- a/templates/news/category.twig +++ b/templates/news/category.twig @@ -2,10 +2,10 @@ {% from 'macros.twig' import pagination, container_title %} {% from 'news/macros.twig' import news_preview %} -{% set title = category_info.name ~ ' :: News' %} -{% set manage_link = url('manage-news-category', {'category': category_info.id}) %} +{% set title = news_category.name ~ ' :: News' %} +{% set manage_link = url('manage-news-category', {'category': news_category.id}) %} {% set canonical_url = url('news-category', { - 'category': category_info.id, + 'category': news_category.id, 'page': news_pagination.page > 2 ? news_pagination.page : 0, }) %} @@ -13,33 +13,33 @@ { 'type': 'rss', 'title': '', - 'url': url('news-category-feed-rss', {'category': category_info.id}), + 'url': url('news-category-feed-rss', {'category': news_category.id}), }, { 'type': 'atom', 'title': '', - 'url': url('news-category-feed-atom', {'category': category_info.id}), + 'url': url('news-category-feed-atom', {'category': news_category.id}), }, ] %} {% block content %}
- {% for post in posts %} + {% for post in news_posts %} {{ news_preview(post) }} {% endfor %}
- {{ pagination(news_pagination, url('news-category', {'category':category_info.id})) }} + {{ pagination(news_pagination, url('news-category', {'category': news_category.id})) }}
- {{ container_title('News » ' ~ category_info.name) }} + {{ container_title('News » ' ~ news_category.name) }}
- {{ category_info.description }} + {{ news_category.description }}
@@ -47,7 +47,7 @@ {{ container_title('Feeds') }} - +
diff --git a/templates/news/index.twig b/templates/news/index.twig index ec7747c..9158018 100644 --- a/templates/news/index.twig +++ b/templates/news/index.twig @@ -24,7 +24,7 @@ {% block content %}
{% endmacro %} -{% macro news_post(post) %} +{% macro news_post(post, category, user) %} {% from 'macros.twig' import avatar %} -
+