getLoggedInUser(); $posts = $this->getPosts('/blog/', $user); if (!is_array($posts)) $posts = []; // 検索機能が使用されている場合 if (isset($_GET['q']) && !empty($_GET['q'])) { $this->searchKeywords = array_map('trim', explode(',', $_GET['q'])); $posts = $this->searchPosts($this->searchKeywords, $posts, '/blog/'); $pagetit = '検索結果: '.htmlspecialchars($_GET['q']); // 検索結果にキーワードをハイライト $posts = $this->highlightKeywords($posts); } // ページネーション $totalPosts = count($posts); $totalPages = ceil($totalPosts / $postsPerPage); $page = min($page, $totalPages); $currentPosts = array_slice( $posts, ($page - 1) * $postsPerPage, $postsPerPage ); $tmpl->assign('user', $user); $tmpl->assign('currentPage', $page); $tmpl->assign('totalPages', $totalPages); $tmpl->assign('posts', $currentPosts); $tmpl->assign('pagetit', $pagetit); $tmpl->assign('curblog', 'gd'); $tmpl->assign('section', 'blog'); $tmpl->assign('curPage', 'blog'); $tmpl->assign('custCss', false); $tmpl->assign('menu', $this->getMenu()); $tmpl->assign('description', $description); $tmpl->assign('searchActive', !empty($this->searchKeywords)); $tmpl->addCss('news'); $tmpl->addCss('search'); $tmpl->addCss('blogtype'); $tmpl->addCss('pagination'); $tmpl->render('home'); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } } /** * ブログ投稿ページ * * @param array $params マークダウンファイル等 * @return void */ public function article(array $params): void { $page = ''; if (isset($params['page'])) $page = $params['page']; try { $tmpl = new Template('/'); $md = new Markdown($page, '/blog/'); $meta = $md->getMetadata(); $pagetit = $meta->title; $article = $md->parse(); $description = 'テクニカル諏訪子ちゃんの個人ブログ'; // 検索からの遷移の場合、記事内のキーワードをハイライト if (isset($_GET['q']) && !empty($_GET['q'])) { $keywords = array_map('trim', explode(',', $_GET['q'])); $article = $this->highlightTextContent($article, $keywords); $meta->title = $this->highlightTextContent($meta->title, $keywords); } // ユーザー $auth = new Auth(); $user = $auth->getLoggedInUser(); $isMember = $user !== NULL && $user->role !== Roles::BANNED; $isPaywall = $user !== NULL && ($user->role & Roles::SUBSCRIBER); $isStaff = $user !== NULL && ($user->role & (Roles::ADMIN | Roles::STAFF)); $tmpl->assign('user', $user); $tmpl->assign('pagetit', $pagetit); $tmpl->assign('curPage', 'blog'); $tmpl->assign('custCss', false); $tmpl->assign('curblog', 'gd'); $tmpl->assign('section', 'blog'); $tmpl->assign('menu', $this->getMenu()); $tmpl->assign('article', $article); $tmpl->assign('meta', $meta); $tmpl->assign('description', $description); if (isset($meta->css) && !empty($meta->css)) { foreach ($meta->css as $v) { $tmpl->addCss($v); } } if (!$isMember && in_array('memberonly', $meta->category)) goto denyaccess; if ((!$isPaywall && !$isStaff) && in_array('subscriberonly', $meta->category)) goto denyaccess; if (!$isStaff && in_array('staffonly', $meta->category)) goto denyaccess; if (isset($meta->draft) && !$isStaff) goto denyaccess; showpage: $this->assignComments($tmpl, 'blog', $page, $meta, $user); $tmpl->addCss('news-article'); $tmpl->addCss('search'); $tmpl->addCss('blogtype'); $tmpl->addCss('code'); $tmpl->render('article'); exit(); denyaccess: unset($md, $meta, $pagetit, $article, $description); $tmpl->assign('pagetit', 'Not found'); $tmpl->assign('curPage', '404'); $tmpl->assign('menu', $this->getMenu()); $tmpl->assign('description', ''); $tmpl->render('404'); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } } public function robots(array $params): void { $txt = "User-Agent: *\nDisallow: /login/\nDisallow: /register/"; $tmpFile = tempnam(sys_get_temp_dir(), 'tmpl_'); file_put_contents($tmpFile, $txt); include $tmpFile; unlink($tmpFile); } //------------------------------------------ // 機能性 //------------------------------------------ /** * キーワードに基づいて投稿を検索する * * @param array $keywords 検索キーワードの配列 * @param array $posts 検索対象の投稿記事の配列 * @return array 検索条件に一致する投稿記事の配列 */ private function searchPosts(array $keywords, array $posts, string $section): array { if (empty($keywords) || empty($posts)) { return $posts; } $foundPosts = []; $path = ROOT.$section; foreach ($posts as $post) { $matched = false; // タイトルで検索 foreach ($keywords as $keyword) { $keyword = trim($keyword); if (empty($keyword)) continue; // タイトル内でキーワードが見つかった場合 if (mb_stripos($post['title'], $keyword) !== false) { $foundPosts[] = $post; $matched = true; break; } } // すでにマッチしていれば次の記事へ if ($matched) continue; // 記事の本文をチェック $slug = $post['slug']; $filePath = $path.$slug.'.md'; if (file_exists($filePath)) { $content = file_get_contents($filePath); $parts = explode('----', $content, 2); if (count($parts) > 1) { // コメントは検索しない(非公開のメモが検索で当たらない様に) $articleBody = trim(Markdown::stripComments($parts[1])); foreach ($keywords as $keyword) { $keyword = trim($keyword); if (empty($keyword)) continue; // 本文内でキーワードが見つかった場合 if (mb_stripos($articleBody, $keyword) !== false) { $foundPosts[] = $post; break; } } } } } return $foundPosts; } /** * 検索結果の投稿内のキーワードをハイライトする * * @param array $posts 検索結果の投稿配列 * @return array ハイライト処理後の投稿配列 */ private function highlightKeywords(array $posts): array { if (empty($this->searchKeywords) || empty($posts)) { return $posts; } foreach ($posts as &$post) { // タイトルのハイライト if (!empty($post['title'])) { $post['title'] = $this->highlightTextContent($post['title'], $this->searchKeywords); } // プレビューのハイライト if (!empty($post['preview'])) { $post['preview'] = $this->highlightTextContent($post['preview'], $this->searchKeywords); } } return $posts; } /** * テキスト内のキーワードをハイライトする * * @param string $text ハイライト対象のテキスト * @param array $keywords ハイライトするキーワード配列 * @return string ハイライト処理後のテキスト */ private function highlightTextContent(string $text, array $keywords): string { if (empty($keywords) || empty($text)) { return $text; } $alts = []; foreach ($keywords as $keyword) { $keyword = trim($keyword); $escaped = htmlspecialchars($keyword, ENT_QUOTES, 'UTF-8'); if ($escaped === '') continue; $alts[] = preg_quote($escaped, '/'); } if (empty($alts)) return $text; $pattern = '/('.implode('|', $alts).')/iu'; $parts = preg_split('/(<[^>]*>|&[#\w]+;)/u', $text, -1, PREG_SPLIT_DELIM_CAPTURE); if ($parts === false) return $text; foreach ($parts as $i => $part) { if ($part === '' || $part[0] === '<' || $part[0] === '&') continue; $res = preg_replace($pattern, '$1', $part); if ($res !== null) $parts[$i] = $res; } return implode('', $parts); } }