user = (new Auth())->getLoggedInUser(); } /** * コメントする(POST /comments) * thread:スレッド、parent:返信先のコメント、body:本文(Markdown) * * @param array $params パラメータ配列 * @return void */ public function post(array $params): void { $user = $this->requireUser(); $thread = (string)($_POST['thread'] ?? ''); $parent = (string)($_POST['parent'] ?? ''); $body = (string)($_POST['body'] ?? ''); $title = $this->commentable($thread); if ($title === null) $this->notFound(); if (!$this->csrfOk()) { $error = 'セッションの有効期限が切れました。もう一度送信して下さい。'; } else { $res = Comments::addLocal($thread, $title, $user, $body, $parent === '' ? null : $parent, $this->postNote($thread)); if ($res->isSuccess) $this->redirect('/'.$thread.'#c-'.$res->data['id']); $error = $res->message; } // 失敗:入力した内容を残して表示する $this->render('comment', 'コメント', [ 'error' => $error, 'thread' => $thread, 'title' => $title, 'parent' => $parent, 'body' => $body, 'maxLength' => Comments::MAX_LENGTH, ]); } /** * いいね・いいねの取り消し(POST /comments/like) * * @param array $params パラメータ配列 * @return void */ public function like(array $params): void { [$user, $thread, $id] = $this->target(); $res = Comments::like($thread, $id, $user); if (!$res->isSuccess) $this->message($res->message, '/'.$thread.'#comments'); $this->redirect('/'.$thread.'#c-'.$id); } /** * コメントを削除する(POST /comments/delete、本人又は管理者) * * @param array $params パラメータ配列 * @return void */ public function delete(array $params): void { [$user, $thread, $id] = $this->target(); $res = Comments::delete($thread, $id, $user); if (!$res->isSuccess) $this->message($res->message, '/'.$thread.'#comments'); $this->redirect('/'.$thread.'#comments'); } /** * 通知(GET /notifications)。管理者には新しいコメント、全員には自分のコメントへの返信といいね * * @param array $params パラメータ配列 * @return void */ public function notifications(array $params): void { $user = $this->requireUser(); $list = array_map(fn($n) => Comments::present($n), \Std\Lib\Notifications::list($user->username)); \Std\Lib\Notifications::markRead($user->username); $this->render('notifications', '通知', ['list' => $list]); } ////// /** * コメント出来る記事か(記事が存在し、このユーザーが読める) * * @param string $thread スレッド(「blog/記事」等) * @return string|null 記事のタイトル、コメント出来なければnull */ private function commentable(string $thread): ?string { if (!Comments::validThread($thread)) return null; $parts = explode('/', $thread); if (count($parts) !== 2 || !isset(blogs()[$parts[0]])) return null; foreach ($this->getPosts("/{$parts[0]}/", $this->user) as $post) { // 会員限定等で読めない記事にはコメント出来ない if ($post['slug'] === $parts[1]) return $post['preview'] === '未許可' ? null : (string)$post['title']; } return null; } /** * 連合するブログの記事のNote(Fediverseのロールを持つユーザーのコメントを、記事への返信として送る為) * * @param string $thread スレッド * @return array|null 連合しない記事(勉強会、会員限定等)ならnull */ private function postNote(string $thread): ?array { [$section, $slug] = array_pad(explode('/', $thread, 2), 2, ''); if (!ACTIVITYPUB_ENABLED || !(blogs()[$section]['federate'] ?? false)) return null; foreach ($this->getPosts("/{$section}/", null) as $post) { if ($post['slug'] !== $slug) continue; $json = (new Activitypub([$post + ['section' => $section]]))->getObject((string)$post['uuid'], $section); return $json !== '' ? json_decode($json, true) : null; } return null; } /** いいね・削除の対象(ログイン、CSRF、記事を確認する) */ private function target(): array { $user = $this->requireUser(); $thread = (string)($_POST['thread'] ?? ''); $id = (string)($_POST['id'] ?? ''); if ($this->commentable($thread) === null) $this->notFound(); if (!$this->csrfOk()) $this->message('セッションの有効期限が切れました。前のページに戻って、もう一度お試し下さい。', '/'.$thread.'#comments'); return [$user, $thread, $id]; } private function requireUser(): \stdClass { if ($this->user === null) $this->redirect('/login'); return $this->user; } private function csrfOk(): bool { return is_string($_POST['csrf_token'] ?? null) && verify_csrf_token($_POST['csrf_token']); } private function redirect(string $to): never { header('Location: '.$to); exit; } private function notFound(): never { header('Location: /404'); exit; } private function message(string $msg, string $back): never { $this->render('comment', 'コメント', ['error' => $msg, 'back' => $back]); exit; } private function render(string $view, string $title, array $vars): void { $tmpl = new Template('/'); $tmpl->assign('pagetit', $title); $tmpl->assign('curPage', 'comments'); $tmpl->assign('custCss', false); $tmpl->assign('menu', $this->getMenu()); $tmpl->assign('description', SITEINFO['description']); $tmpl->assign('user', $this->user); // クッキーはテンプレートの出力の前に設定する $tmpl->assign('csrf', make_csrf_token()); foreach ($vars as $k => $v) $tmpl->assign($k, $v); $tmpl->addCss('comments'); $tmpl->render($view); } }