レポジトリ種類: Mercurial

<?php
namespace Site\Controller;

use Site\Lib\Activitypub;
use Site\Lib\Markdown;
use Site\Lib\Template;
use Site\Controller\Mods;

class Home extends Mods {
  private array $searchKeywords = [];
  private string $domain = 'technicalsuwako.moe';

  //------------------------------------------
  // ページ
  //------------------------------------------

  /**
   * ブログ投稿ページ
   *
   * @param array $params ページ番号等
   * @return void
   */
  public function show(array $params): void {
    try {
      $page = isset($_GET['page']) ? $_GET['page'] : 1;
      $postsPerPage = 20;

      $tmpl = new Template('/');
      $pagetit = 'トップページ';

      $description = 'テクニカル諏訪子ちゃんの個人ブログ';

      $posts = $this->getPosts();
      if (!is_array($posts)) $posts = [];

      // ページネーション
      $totalPosts = count($posts);
      $totalPages = ceil($totalPosts / $postsPerPage);
      $page = min($page, $totalPages);
      $currentPosts = array_slice(
        $posts, 
        ($page - 1) * $postsPerPage, 
        $postsPerPage
      );

      $tmpl->assign('currentPage', $page);
      $tmpl->assign('totalPages', $totalPages);
      $tmpl->assign('posts', $currentPosts);
      $tmpl->assign('pagetit', $pagetit);
      $tmpl->assign('curPage', 'blog');
      $tmpl->assign('custCss', false);
      $tmpl->assign('menu', $this->getMenu());
      $tmpl->assign('description', $description);

      $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);

      $meta = $md->getMetadata();
      $pagetit = $meta->title;
      $article = $md->parse();
      $description = 'テクニカル諏訪子ちゃんの個人ブログ';

      // 検索からの遷移の場合、記事内のキーワードをハイライト
      if (isset($_GET['q']) && !empty($_GET['q'])) {
        $keywords = array_map('trim', explode(',', $_GET['q']));
      }

      $tmpl->assign('pagetit', $pagetit);
      $tmpl->assign('curPage', 'blog');
      $tmpl->assign('custCss', false);
      $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);
        }
      }

      $tmpl->render('article');
    } catch (\Exception $e) {
      throw new \Exception($e->getMessage());
    }
  }

  /**
   * 最新の5記事のAtomフィードを生成する
   * 
   * @param array $params パラメータ配列
   * @return void
   */
  public function feed(array $params): void {
    try {
      // 最新の投稿を取得
      $posts = $this->getPosts();
      // 最新の5件に制限
      $posts = array_slice($posts, 0, 5);
      
      // サイトのドメインを取得
      $domain = $_SERVER['HTTP_HOST'];
      $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ?
        'https' : 'http';
      $baseUrl = $protocol.'://'.$domain;
      
      // 現在の日時(RFC3339形式)
      $published = date('c');
      
      // XMLヘッダーとコンテンツタイプを設定
      header('Content-Type: application/atom+xml; charset=utf-8');
      
      // Atomフィードの開始部分
      echo '<?xml version="1.0" encoding="utf-8"?>'."\n";
      echo '<feed xmlns="http://www.w3.org/2005/Atom">'."\n";
      
      // フィードの基本情報
      echo '  <title>'.SITEINFO['title'].'</title>'."\n";
      echo '  <link href="'.$baseUrl.'" />'."\n";
      echo '  <link href="'.$baseUrl.'/blog.atom" rel="self" />'."\n";
      echo '  <id>' . $baseUrl.'/</id>'."\n";
      echo '  <published>'.$published.'</published>'."\n";
      echo '  <author>'."\n";
      echo '    <name>'.SITEINFO['title'].'</name>'."\n";
      echo '  </author>'."\n";
      
      // 各エントリー(記事)
      foreach ($posts as $post) {
        // 記事の本文を取得(プレーンテキスト)
        $path = ROOT.'/blog/'.$post['slug'].'.md';
        $content = '';
        $postPublished = date('c', strtotime($post['date']));
        
        if (file_exists($path)) {
          $fileContent = file_get_contents($path);
          $parts = explode('----', $fileContent, 2);
          if (count($parts) > 1) {
            // 本文をHTMLとして準備
            $md = new Markdown($post['slug']);
            $content = $md->parse();
            // HTMLタグを取り除かないようにCDATAで囲む
            $content = '<![CDATA[' . $content . ']]>';
          }
        }
        
        echo '  <entry>'."\n";
        echo '    <title>'.htmlspecialchars($post['title']).'</title>'."\n";
        echo '    <link href="'.$baseUrl.'/blog/'.$post['slug'].'" />'."\n";
        echo '    <id>'.$baseUrl.'/blog/'.$post['slug'].'</id>'."\n";
        echo '    <published>'.$postPublished.'</published>'."\n";
        
        // カテゴリ(タグ)
        if (isset($post['category']) && is_array($post['category'])) {
          foreach ($post['category'] as $category) {
            echo '    <category term="'.htmlspecialchars($category).'" />'."\n";
          }
        }
        
        // 本文(要約または全文)
        echo '    <content type="html">'.$content.'</content>'."\n";
        echo '  </entry>'."\n";
      }
      
      // フィードの終了
      echo '</feed>';
      exit;
    } catch (\Exception $e) {
      header('Content-Type: text/plain; charset=utf-8');
      echo 'フィードの作成に失敗: '.$e->getMessage();
      exit;
    }
  }

  /**
   * @param array $params パラメータ配列
   * @return void
   */
  public function apfinger(array $params): void {
    try {
      header('Content-Type: application/jrd+json');
      $ap = new Activitypub();
      echo $ap->getWebfinger();
      exit;
    } catch (\Exception $e) {
      header('Content-Type: text/plain; charset=utf-8');
      echo 'フェディバースの作成に失敗: '.$e->getMessage();
      exit;
    }
  }

  /**
   * @param array $params パラメータ配列
   * @return void
   */
  public function apactor(array $params): void {
    try {
      header('Content-Type: application/activity+json');
      $ap = new Activitypub();
      echo $ap->getActor();
      exit;
    } catch (\Exception $e) {
      header('Content-Type: text/plain; charset=utf-8');
      echo 'フェディバースの作成に失敗: '.$e->getMessage();
      exit;
    }
  }

  /**
   * @param array $params パラメータ配列
   * @return void
   */
  public function apinbox(array $params): void {
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
      header('HTTP/1.1 405 Method Not Allowed');
      header('Allow: POST');
      exit;
    }

    $input = file_get_contents('php://input');
    $activity = json_decode($input, true);
    if (!$activity || !isset($activity['type'])) {
      header('HTTP/1.1 400 Bad Request');
      header('Content-Type: application/activity+json');
      echo json_encode(['error' => '不正なアクティビティ']);
      exit;
    }

    logger(\LogType::ActivityPub, "受付に入れた:".json_encode($activity));

    try {
      header('Content-Type: application/activity+json');
      $ap = new Activitypub();
      $ap->postInbox($activity);
      exit;
    } catch (\Exception $e) {
      header('Content-Type: text/plain; charset=utf-8');
      echo 'フェディバースの作成に失敗: '.$e->getMessage();
      exit;
    }
  }

  /**
   * @param array $params パラメータ配列
   * @return void
   */
  public function apactivity(array $params): void {
    $uuid = '';
    if (isset($params['uuid'])) $uuid = $params['uuid'];

    try {
      header('Content-Type: application/activity+json');
      $posts = $this->getPosts();
      $ap = new Activitypub($posts);
      echo $ap->getActivity($uuid);
      exit;
    } catch (\Exception $e) {
      header('Content-Type: text/plain; charset=utf-8');
      echo 'フェディバースの作成に失敗: '.$e->getMessage();
      exit;
    }
  }

  /**
   * @param array $params パラメータ配列
   * @return void
   */
  public function apoutbox(array $params): void {
    try {
      header('Content-Type: application/activity+json');
      $posts = $this->getPosts();
      $ap = new Activitypub($posts);
      echo $ap->getOutbox();
      exit;
    } catch (\Exception $e) {
      header('Content-Type: text/plain; charset=utf-8');
      echo 'フェディバースの作成に失敗: '.$e->getMessage();
      exit;
    }
  }

  /**
   * @param array $params パラメータ配列
   * @return void
   */
  public function apfollowers(array $params): void {
    try {
      header('Content-Type: application/activity+json');
      $ap = new Activitypub();
      echo $ap->getFollowers();
      exit;
    } catch (\Exception $e) {
      header('Content-Type: text/plain; charset=utf-8');
      echo 'フェディバースの作成に失敗: '.$e->getMessage();
      exit;
    }
  }

  /**
   * @param array $params パラメータ配列
   * @return void
   */
  public function apfollowing(array $params): void {
    try {
      header('Content-Type: application/activity+json');
      $ap = new Activitypub();
      echo $ap->getFollowing();
      exit;
    } catch (\Exception $e) {
      header('Content-Type: text/plain; charset=utf-8');
      echo 'フェディバースの作成に失敗: '.$e->getMessage();
      exit;
    }
  }

  //------------------------------------------
  // 機能性
  //------------------------------------------

  /**
   * ブログ投稿を取得する
   * 
   * @return array 投稿の配列
   */
  private function getPosts(): array {
    $path = ROOT.'/blog/';
    $posts = [];

    if (!is_dir($path)) return $posts;
    $files = glob($path.'/*.md');

    foreach ($files as $file) {
      $content = file_get_contents($file);
      $parts = explode('----', $content, 2);
      if (count($parts) != 2) continue;

      $metadata = [];
      $meta = explode("\n", trim($parts[0]));

      foreach ($meta as $line) {
        $line = trim($line);
        if (empty($line)) continue;

        $colonPos = strpos($line, ':');
        if ($colonPos === false) continue;

        $key = trim(substr($line, 0, $colonPos));
        $value = trim(substr($line, $colonPos + 1));
        $value = trim($value, '"\'');

        if ($key == 'category') {
          $metadata[$key] = array_map('trim', explode(',', $value));
        } else {
          $metadata[$key] = $value;
        }
      }

      $articleBody = trim($parts[1]);
      $preview = mb_substr(strip_tags($articleBody), 0, 50) . '...';
      $slug = basename($file, '.md');

      $posts[] = [
        'title' => $metadata['title'] ?? '',
        'date' => $metadata['date'] ?? '',
        'thumbnail' => $metadata['thumbnail'] ?? '',
        'thumborient' => $metadata['thumborient'] ?? '',
        'category' => $metadata['category'] ?? [],
        'uuid' => $metadata['uuid'] ?? '',
        'preview' => $preview,
        'slug' => $slug,
      ];
    }

    // 日付でソート(新しい順)
    usort($posts, function($a, $b) {
      return strtotime($b['date']) - strtotime($a['date']);
    });

    return $posts;
  }
}