| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, and/or sell copies of the Software, subject to the following conditions: |
| 10 | |
| 11 | 1. **Origin Attribution**: |
| 12 | - You must not misrepresent the origin of the Software; you must not claim |
| 13 | you created the original Software. |
| 14 | - If the Software is used in a product, you must either: |
| 15 | a. Provide clear attribution in the product's documentation, user interface, |
| 16 | or other visible areas, **OR** |
| 17 | b. Pay the original developers a fee they specify in writing. |
| 18 | 2. **Usage Restriction**: |
| 19 | - The Software, or any derivative works, dependencies, or libraries |
| 20 | incorporating it, must not be used for censorship or to suppress freedom of |
| 21 | speech, expression, or creativity. Prohibited uses include, but are not |
| 22 | limited to: |
| 23 | - Censorship of so-called "hate speech", visuals, non-mainstream opinions, |
| 24 | ideas, or objective reality. |
| 25 | - Tools or systems designed to restrict access to information or |
| 26 | artistic works. |
| 27 | 3. **Notice Preservation**: |
| 28 | - This license and the above copyright notice must remain intact in all copies |
| 29 | of the source code. |
| 30 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | namespace Site\Controller; |
| 39 | |
| 40 | use Site\Controller\BlogPost; |
| 41 | use Std\Lib\Markdown; |
| 42 | |
| 43 | class Atom extends BlogPost { |
| 44 | private string $domain = 'technicalsuwako.moe'; |
| 45 | |
| 46 | /** |
| 47 | * フィードは誰でも読めるので、会員・支援者・スタッフ限定の記事を除外する |
| 48 | * (getPosts()はプレビューを隠すだけで、記事そのものは返す) |
| 49 | * |
| 50 | * @param array $posts 投稿の配列 |
| 51 | * @return array 公開記事だけの配列 |
| 52 | */ |
| 53 | private function publicOnly(array $posts): array { |
| 54 | $restricted = ['memberonly', 'subscriberonly', 'staffonly', 'nintendodevonly']; |
| 55 | return array_values(array_filter($posts, function($post) use ($restricted): bool { |
| 56 | return empty(array_intersect($restricted, $post['category'] ?? [])); |
| 57 | })); |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * 最新の5記事のAtomフィードを生成する |
| 62 | * |
| 63 | * @param array $params パラメータ配列 |
| 64 | * @return void |
| 65 | */ |
| 66 | public function feed(array $params): void { |
| 67 | try { |
| 68 | $section = (string)($params['section'] ?? 'blog'); |
| 69 | $blogs = blogs(); |
| 70 | if (!isset($blogs[$section])) { |
| 71 | header('HTTP/1.1 404 Not Found'); |
| 72 | exit; |
| 73 | } |
| 74 | |
| 75 | // 最新の投稿を取得 |
| 76 | $posts = $this->getPosts("/{$section}/", null); |
| 77 | |
| 78 | // 最新の5件に制限 |
| 79 | $posts = array_slice($this->publicOnly($posts), 0, 5); |
| 80 | |
| 81 | // サイトのドメインを取得 |
| 82 | $domain = $_SERVER['HTTP_HOST']; |
| 83 | $baseUrl = 'https://'.$domain; |
| 84 | |
| 85 | // 「blog」のフィードのIDとリンクは今まで通り(IDが変わると購読者のリーダーで全部新しい記事になる) |
| 86 | $home = $section === 'blog' ? $baseUrl : "{$baseUrl}/{$section}"; |
| 87 | |
| 88 | // 現在の日時(RFC3339形式) |
| 89 | $published = date('c'); |
| 90 | |
| 91 | // XMLヘッダーとコンテンツタイプを設定 |
| 92 | header('Content-Type: application/atom+xml; charset=utf-8'); |
| 93 | |
| 94 | // Atomフィードの開始部分 |
| 95 | echo '<?xml version="1.0" encoding="utf-8"?>'."\n"; |
| 96 | echo '<feed xmlns="http://www.w3.org/2005/Atom">'."\n"; |
| 97 | |
| 98 | // フィードの基本情報 |
| 99 | echo ' <title>'.htmlspecialchars($blogs[$section]['title'], ENT_XML1, 'UTF-8').'</title>'."\n"; |
| 100 | echo ' <link href="'.$home.'" />'."\n"; |
| 101 | echo ' <link href="'.$baseUrl.'/'.$section.'.atom" rel="self" />'."\n"; |
| 102 | echo ' <id>'.$home.'/</id>'."\n"; |
| 103 | echo ' <published>'.$published.'</published>'."\n"; |
| 104 | echo ' <updated>'.$published.'</updated>'."\n"; |
| 105 | echo ' <author>'."\n"; |
| 106 | echo ' <name>'.SITEINFO['title'].'</name>'."\n"; |
| 107 | echo ' </author>'."\n"; |
| 108 | |
| 109 | // 各エントリー(記事) |
| 110 | foreach ($posts as $post) { |
| 111 | // 記事の本文を取得(プレーンテキスト) |
| 112 | $path = ROOT."/{$section}/".$post['slug'].'.md'; |
| 113 | $content = ''; |
| 114 | $postPublished = date('c', strtotime($post['date'])); |
| 115 | |
| 116 | if (file_exists($path)) { |
| 117 | $fileContent = file_get_contents($path); |
| 118 | $parts = explode('----', $fileContent, 2); |
| 119 | if (count($parts) > 1) { |
| 120 | // 本文をHTMLとして準備 |
| 121 | $md = new Markdown($post['slug'], "/{$section}/"); |
| 122 | $content = $md->parse(); |
| 123 | // HTMLタグを取り除かないようにCDATAで囲む |
| 124 | $content = '<![CDATA['.$content.']]>'; |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | echo ' <entry>'."\n"; |
| 129 | echo ' <title>'.htmlspecialchars($post['title']).'</title>'."\n"; |
| 130 | echo ' <link href="'.$baseUrl.'/'.$section.'/'.$post['slug'].'" />'."\n"; |
| 131 | echo ' <id>'.$baseUrl.'/'.$section.'/'.$post['slug'].'</id>'."\n"; |
| 132 | echo ' <published>'.$postPublished.'</published>'."\n"; |
| 133 | |
| 134 | // カテゴリ(タグ) |
| 135 | if (isset($post['category']) && is_array($post['category'])) { |
| 136 | foreach ($post['category'] as $category) { |
| 137 | echo ' <category term="'.htmlspecialchars($category).'" />'."\n"; |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | // 本文(要約または全文) |
| 142 | echo ' <content type="html">'.$content.'</content>'."\n"; |
| 143 | echo ' </entry>'."\n"; |
| 144 | } |
| 145 | |
| 146 | // フィードの終了 |
| 147 | echo '</feed>'; |
| 148 | exit; |
| 149 | } catch (\Exception $e) { |
| 150 | header('Content-Type: text/plain; charset=utf-8'); |
| 151 | echo 'フィードの作成に失敗: '.$e->getMessage(); |
| 152 | exit; |
| 153 | } |
| 154 | } |
| 155 | } |