master
ブランチ master
LittleBeast / README-JP.md
38.8 KB、391行 表示 生 Blame 履歴
1# Little Beast
2シンプル、実践的、アンチ・ブロート
3
4![GitHub repo size](https://img.shields.io/github/repo-size/TechnicalSuwako/LittleBeast)
5[![lines count](https://img.shields.io/endpoint?url=https://ghloc.vercel.app/api/TechnicalSuwako/LittleBeast/badge?filter=.php%24%2C.maron%24%2C.css%24&branch=master$)](https://ghloc.vercel.app/TechnicalSuwako/LittleBeast?filter=.php%24%2C.maron%24%2C.css%24)
6
7サンプル:[https://lbdemo.technicalsuwako.moe/](https://lbdemo.technicalsuwako.moe/)
8
9## Little Beast とは?
10Little Beast は PHP 8.5 以上向けのフレームワークで、076.moe(ゲーム開発会社)と technicalsuwako.moe(社長のブログ)向けに作られました。\
11メイン考え方は「必要な物だけ拡張し、不要な物は削除する」です。\
12各コア機能はライブラリに分割されている為、必要な物だけを選び易い設計になっています。\
13全てのモジュールはゼロから書かれており、データベースは一切必要ありません。
14
15Little Beast はテクニカル諏訪子がゲーム開発に完全復帰する前の最後の Web プロジェクトです。
16
17## Little Beast が「ではない」物
18* 汎用フレームワーク
19* 使い憎い
20* Web 開発者向け
21* 万人向け
22* インストールが面倒
23* 教条的
24* 民主的に運営される
25
26## Little Beast が「持っていない」物
27* データベース(全てファイルベース)
28* 依存関係
29* パッケージマネージャー
30* Docker/Kubernetes/Vagrant/Nix 等のコンテナ
31* JavaScript
32* ブロートウェア
33* ORM
34* キューやバックグラウンドジョブ
35* クラウド/サーバーレス統合
36* スキャフォールディング
37* コード生成
38* 抽象化レイヤー
39* 不要なファイル
40* DEI、行動規範、その他の差別的慣行
41
42## 独自機能
43* データベース不要
44* Composer や PEAR 不要
45* サーバーオーバーヘッドゼロ
46* コンテナ不要
47* Maron テンプレートエンジン
48* カスタム Markdown
49* ActivityPub
50* 画像ライブラリ(PNG・APNG・BMP・TGA・GIF・JPEG・WebP の読み書き。GD 等の拡張は不要)
51* 二段階認証(認証アプリのワンタイムパスワード、QR コード)
52* 多言語対応
53* マルチブログ対応
54* 100% 綺麗で正しい HTML5 と CSS3
55* 100% 正しい PHP
56* SEO フレンドリー
57* モジュラー CSS
58* Atom フィード
59* 組み込みテストスイート
60
61## インストール方法
62```sh
63cd /var/www/htdocs
64git clone https://github.com/TechnicalSuwako/LittleBeast.git .
65mv config/config.sample.php config/config.php
66```
67
68HTTP サーバーを設定して `/public` をルートとして `php-fpm` で実行して下さい。\
69それだけです!
70
71### OpenBSD サーバー
72```sh
73pkg_add php-8.5.5 php-gmp-8.5.5
74rcctl enable php85_fpm httpd relayd
75rcctl start php85_fpm httpd relayd
76```
77
78#### httpd
79```
80server "technicalsuwako.moe" {
81 listen on * tls port 8443
82 gzip-static
83 tls {
84 certificate "/etc/ssl/technicalsuwako.moe.crt"
85 key "/etc/ssl/private/technicalsuwako.moe.key"
86 }
87 root "/htdocs/technicalsuwako.moe/www/public"
88 directory index "index.php"
89 location "/.well-known/acme-challenge/*" {
90 root "/acme"
91 request strip 2
92 }
93
94 location "/*.php" {
95 fastcgi socket "/run/php-fpm.sock"
96 }
97
98 location "/*.php[/?]*" {
99 fastcgi socket "/run/php-fpm.sock"
100 }
101
102 location "/" {
103 directory index "index.php"
104 }
105 location match "/blog/" {
106 request rewrite "/index.php"
107 }
108 location match "/about" {
109 request rewrite "/index.php"
110 }
111 location match "/monero" {
112 request rewrite "/index.php"
113 }
114 location match "/secret" {
115 request rewrite "/index.php"
116 }
117 location match "/ap/" {
118 request rewrite "/index.php"
119 }
120 location "/.well-known/webfinger" {
121 request rewrite "/index.php"
122 }
123 location "*.atom" {
124 request rewrite "/index.php"
125 }
126 location match "/comments" {
127 request rewrite "/index.php"
128 }
129 location "/notifications" {
130 request rewrite "/index.php"
131 }
132}
133
134server "www.technicalsuwako.moe" {
135 listen on * tls port 8443
136 gzip-static
137 tls {
138 certificate "/etc/ssl/technicalsuwako.moe.crt"
139 key "/etc/ssl/private/technicalsuwako.moe.key"
140 }
141 block return 301 "https://technicalsuwako.moe$REQUEST_URI"
142}
143```
144
145#### relayd
146```
147relayd_addr="0.0.0.0"
148router_addr="192.168.10.106"
149
150table <httpd> { $router_addr }
151
152http protocol reverse {
153 tcp { nodelay, sack, socket buffer 65536, backlog 100 }
154 tls ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256"
155 tls keypair "technicalsuwako.moe"
156
157 return error
158
159 match request header append "X-Forwarded-For" value "$REMOTE_ADDR"
160 match request header append "X-Forwarded-Port" value "$REMOTE_PORT"
161
162 #match response header set "Referrer-Policy" value "same-origin"
163 match response header set "X-Frame-Options" value "deny"
164 match response header set "X-Content-Type-Options" value "nosniff"
165 match response header set "Referrer-Policy" value "strict-origin-when-cross-origin"
166 match response header set "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload"
167 match response header set "Cross-Origin-Opener-Policy" value "same-origin"
168 match response header set "Content-Security-Policy" value "img-src 'self' https://*.076.moe http://*.076.moe https://*.technicalsuwako.moe http://*.technicalsuwako.moe; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; object-src 'none';"
169 match response header append "Permissions-Policy" value "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=()"
170
171 match request header "Accept-Encoding" value "gzip" tag "gzip"
172 match response header "Content-Type" value "text/*" tag "compress"
173 match response header "Content-Type" value "text/html" tag "charset=UTF-8"
174 match response header "Content-Type" value "application/javascript" tag "compress"
175 match response header "Content-Type" value "application/json" tag "compress"
176
177 pass request quick header "Host" value "technicalsuwako.moe" forward to <httpd>
178
179 pass
180}
181
182relay www_tls {
183 listen on $relayd_addr port 443 tls
184 protocol reverse
185
186 # Default
187 forward to <httpd> port 8443 check tcp
188}
189
190relay www_www {
191 listen on $relayd_addr port 80
192 protocol reverse
193
194 # Default
195 forward to <httpd> port 8080 check tcp
196}
197```
198
199### ブログと Atom フィード
2001つのサイトに複数のブログを置けます。`config/config.php` の `BLOGS` の各ディレクトリ(例:`blog`、`enblog`)がブログになり、それぞれ `/ディレクトリ.atom` の Atom フィードがあります(一覧と検索結果のページの上にリンクがあります)。\
201`BLOGS` が無ければ、今まで通り `blog` だけです。
202
203### 静的ファイル(ASS_DOMAIN)
204`config/config.php` の `ASS_DOMAIN` で静的ファイル(画像、CSS、アイコン、アップロードしたファイル)の場所を決めます。無ければ `/static` です。
205* パス(例:`/static`)なら、このサイトの `public/` の下のディレクトリです(`/static` は `public/static/`)。
206* URL(例:`https://ass.example.com/static`)なら、別のサーバー(サブドメイン等)です。アップロードしたファイルは今まで通り `public/` の下の同じパス(`public/static/`)に保存するので、そのディレクトリを静的ファイルのサーバーと同期して下さい(又はそのサーバーから配信して下さい)。
207
208テンプレートやコードでは、URLは `ass_url('article/a.png')`、ディスク上のファイルは `ROOT.ass_path('article/a.png')` を使います。
209
210### データベース(MySQL・MariaDB、PostgreSQL、SQLite)
211Little Beast 自体はデータベースを使いませんが、必要な場合(既存のアプリの移植等)は `Std\Lib\Db` で繋げます。ドライバは PHP だけで書かれているので、mysqli・pdo_mysql・pgsql・pdo_pgsql・sqlite3・pdo_sqlite・sockets は不要です。\
212`config/config.php` で `DB_ENABLED` を `true` にして、`DBINFO` を設定して下さい(`driver` は `mysql`(MariaDB も同じ)、PostgreSQL の `pgsql`(ポートは普通5432)、又は `sqlite`(`path` にデータベースのファイル、相対パスはサイトのルートから、例:`data/site.sqlite`)。`socket` を指定すると `host`・`port` の代わりに UNIX ソケットを使う、PostgreSQL ではソケットのファイル又は `.s.PGSQL.5432` が有るディレクトリ(例:`/tmp`)。`tls` は `true`、又は `['cafile' => '/etc/ssl/ca.pem']` 等の SSL コンテキストオプションの配列)。
213```php
214$db = new \Std\Lib\Db();
215$rows = $db->query('SELECT * FROM users WHERE id = ?', [1])['rows'];
216$id = $db->query('INSERT INTO users (name) VALUES (?)', ['諏訪子'])['insertId']; // MySQL・SQLite
217$id = $db->query('INSERT INTO users (name) VALUES (?) RETURNING id', ['諏訪子'])['rows'][0]['id']; // PostgreSQL
218$db->transaction(function(\Std\Lib\Db $db): void {
219 $db->query('UPDATE users SET points = points - ? WHERE id = ?', [100, 1]);
220 $db->query('UPDATE users SET points = points + ? WHERE id = ?', [100, 2]);
221});
222```
223* 最初のクエリで接続します。パラメータ付きのクエリはプリペアドステートメントで実行するので、値をエスケープする必要はありません。
224* 整数の列は `int`、`FLOAT`・`DOUBLE`(PostgreSQL は `real`・`double precision`)は `float`、`NULL` は `null`、その他(`DECIMAL`・`numeric`、日付、JSON、`BLOB` 等)は文字列で返します。PostgreSQL の `boolean` は `bool`、`bytea` は生のバイト、配列は PHP の配列で返します。
225* エラーは `Std\Lib\Db\DbException`(`getCode()` は MySQL のエラーコード、例:重複は1062、`sqlState` は SQLSTATE、例:PostgreSQL の重複は23505)を投げ、`log/db_log.txt` に書きます。
226* MySQL の認証:`mysql_native_password`、`caching_sha2_password`(MySQL 8 の既定)と `sha256_password`(TLS でない TCP では、パスワードをサーバーの RSA の鍵で暗号化する)、`mysql_clear_password`(TLS か UNIX ソケットのみ)。MariaDB の `client_ed25519`、圧縮、`LOAD DATA LOCAL INFILE` は未対応です。
227* SQLite:`Std\Lib\Db\Sqlite` は SQLite 3 のファイルを自分で読み書きするので(ロールバックジャーナル、UTF-8)、`sqlite3` のコマンド等でも開けます。途中で止まった書き込みは、次に開いた時にどちらでも元に戻ります。普通の SQL に対応しています:テーブル・インデックス・ビュー、`ALTER TABLE`(名前の変更、列の追加・削除・名前の変更)、`INSERT`(`OR REPLACE`・`OR IGNORE`、`ON CONFLICT`、`RETURNING`)、`UPDATE … FROM`、結合、`GROUP BY` と集約、サブクエリ、`UNION`・`INTERSECT`・`EXCEPT`、`WITH`、トランザクションとセーブポイント、普通の関数(文字列、数値、日付、JSON)、`PRAGMA`(`table_info`、`index_list`、`user_version`、`integrity_check` 等)。`=`・`IN`・範囲の検索にはインデックスを使います。WAL モード、トリガー、`WITHOUT ROWID`、仮想テーブル(FTS)、ウィンドウ関数、再帰の `WITH`、外部キーの確認、部分・式のインデックス、`ATTACH` は未対応です。ロックは `flock()` なので、サイトの動作中にファイルに書き込むのは PHP のプロセスだけにして下さい。プレースホルダは `?`・`?1`・`:name`。UTF-8 でない文字列は BLOB として保存します(`Std\Lib\Db\Sqlite\Blob` で明示も可)。
228* PostgreSQL の認証:SCRAM-SHA-256(TLS ならチャネルバインディング付きの SCRAM-SHA-256-PLUS、オプションの `channel_binding` は `prefer`・`require`・`disable`)、MD5、`password`(`allow_cleartext` を `true` にしない限り、TLS か UNIX ソケットのみ)、`trust`。GSSAPI・SSPI は未対応です。
229* PostgreSQL:`?` のプレースホルダは `$1`、`$2`… にします(文字列・識別子・コメントの外のみ。`?` そのもの(jsonb の演算子等)は `??` と書く)。`$1` もそのまま使えます。挿入した ID は無いので、`RETURNING` を使って下さい。トランザクションの中でエラーが起きた後の `COMMIT` は、PostgreSQL がロールバックするので `DbException`(SQLSTATE 25P02)を投げます。`COPY`、`LISTEN` の通知、実行中のクエリの取り消しは未対応です。PostgreSQL 16 で確認しました。
230* テストは偽物のサーバーを使います。PostgreSQL のテストを本物のサーバーでも実行するには、`LB_TEST_PGSQL=pgsql://ユーザー:パスワード@ホスト:ポート/データベース` を設定して下さい(TLS は `?cafile=/path/ca.pem` を追加)。名前が `lb_test_` で始まるテーブルを作って消します。
231
232### データベースの変換
233`Std\Lib\Db\Convert` は、MySQL・MariaDB、PostgreSQL、SQLite、SQL のダンプのファイル、`data/` の Little Beast の JSON のデータの間で、テーブルと行をどの方向にもコピーします。ドライバーと同じく PHP だけで書いてあります。
234```php
235use Std\Lib\Db;
236use Std\Lib\Db\Convert;
237use Std\Lib\Db\Convert\{DbSource, DbTarget, DumpSource, DumpTarget, JsonSource, JsonTarget};
238
239// data/(JSON)→ SQLite
240$report = Convert::run(new JsonSource(ROOT.'/data'), new DbTarget(new Db(['driver' => 'sqlite', 'path' => 'data/site.sqlite'])));
241// PostgreSQL → MySQL のダンプのファイル
242Convert::run(new DbSource(new Db(['driver' => 'pgsql', 'host' => '127.0.0.1', 'port' => 5432, 'username' => 'lb', 'password' => '…', 'dbname' => 'lb'])),
243 new DumpTarget('/tmp/lb.sql', 'mysql'));
244// mysqldump・pg_dump のファイル → PostgreSQL(既に有るテーブルは置き換える)
245Convert::run(new DumpSource('/tmp/dump.sql'), new DbTarget(new Db()), ['drop' => true]);
246```
247* 読む元:`DbSource`(接続した `Db`)、`DumpSource`(`mysqldump`、`pg_dump`(プレーンの形式、`COPY` 又は `--inserts`)、`sqlite3 .dump`、`DumpTarget` で作った `.sql` のファイル(`.sql.gz` は PHP の zlib が必要)、方言は自動で判定)、`JsonSource`(データのディレクトリ)。書く先:`DbTarget`、`DumpTarget`(方言は `mysql`・`pgsql`・`sqlite`、`mysql`・`psql`・`sqlite3` のコマンドで読み込めます)、`JsonTarget`。
248* オプション:`tables`(このテーブルだけ)、`drop`(書く先の同じ名前のテーブルを消す、無ければ既に有るテーブルはエラー)、`progress`(`fn(string $table, int $rows)`)。結果は `tables`(テーブル毎のコピーした行数)と `warnings`(変換しなかった物)。
249* 列、主キー、ユニーク・普通のインデックス、`NOT NULL`、既定値、自動の番号(`AUTO_INCREMENT`、`SERIAL`・IDENTITY、SQLite の `INTEGER PRIMARY KEY`)を変換し、番号はコピーした行の続きから始まります。型は書く先の一番近い型にします(例:`TINYINT(1)` と `boolean`、`JSON` と `jsonb`、`BLOB` と `bytea`)。`ENUM`・`SET` は文字列、PostgreSQL の配列は JSON にします。時差の有る日時は UTC で保存します。外部キー、ビュー、トリガー、関数、全文・式のインデックスは変換しません(`warnings` に出ます)。
250* JSON のデータ:ユーザー、コメント、通知、Cron のジョブ、フォロワー、ActivityPub の投稿は名前が `lb_` で始まるテーブルになり、他のファイル(`cache/`、`ratelimit/`、`tmp/`、ロック・一時ファイル、SQLite のデータベースを除く)は `lb_files` に入るので、戻すと同じファイルになります。他のテーブルは `data/db/{テーブル}.json` になります。`data/` が空・無い場合は、テーブルが無いだけです。`JsonTarget` は、2番目の引数(上書き)が `true` か `drop` が無ければ、空でないディレクトリには書きません。
251* サイトが使っているデータベースを読む・書く場合は、先に書き込みを止めて下さい(又は新しいデータベースに変換)。変換中に元をロックしません。
252
253### Git
254`Std\Lib\Git` は Git のリポジトリを PHP だけで読み書きします(PHP の zlib が必要、`git` のコマンドは不要)。SHA-1 と SHA-256 のリポジトリに対応しています。サイトでリポジトリをホストする為の基礎です。
255```php
256use Std\Lib\Git\{Repository, Tree, Commit, Person, Diff, Blame};
257
258$repo = Repository::init('/var/www/git/suwako/test.git', 'sha1', 'main', 'group'); // 又はRepository::open(...)
259$me = Person::now('諏訪子', 'suwako@example.com');
260$tree = $repo->updateTree(null, ['README.md' => [Tree::FILE, "# テスト\n"]]);
261$id = $repo->writeCommit(Commit::create($tree, [], $me, $me, "最初のコミット\n"));
262$repo->refs->update('refs/heads/main', $id, $repo->zeroId()); // 途中でブランチが変わっていれば失敗
263
264foreach ($repo->log('main', ['path' => 'README.md', 'limit' => 20]) as $c) echo $c->id, ' ', $c->subject(), "\n";
265echo $repo->blob($repo->resolve('main~1:README.md'));
266echo Diff::unified($repo, 'main~1', 'main');
267$lines = Blame::file($repo, 'main', 'README.md');
268```
269* 保存:ルーズオブジェクト、パック(`.idx` v1・v2 と `.rev`、差分も解決)、参照と `packed-refs`、`objects/info/alternates`、`core.sharedRepository`。`main~2`、`v1.0^{}`、`HEAD^2`、`main:パス/ファイル` 等のリビジョンが使えます。`Diff` は `git diff` と同じ出力(Myers)、`Blame` は同じ行が有る最初の親を辿り、`Maintenance::gc()` は到達出来るオブジェクトを1つのパックにまとめます(Cron で実行)。
270* ネットワーク:`Http::handle()` は Smart HTTP(`git clone https://…`、`git fetch`、`git push`)、`Ssh::handle()` は SSH の `git clone git@host:…` と `git push` を処理します。プロトコル v0・v1・v2、浅いクローン(`--depth`、`--deepen`)、`include-tag`、サイドバンドの進捗、atomic なプッシュ、受け取ったオブジェクトの確認に対応しています。送るパックはディスクの差分を再利用します。アクセスの確認は呼ぶ側で行い、`ReceivePack` には `check`・`update`・`after` のコールバックを渡せます(保護されたブランチ等)。
271```php
272// GET …/repo.git/info/refs、POST …/repo.git/git-upload-pack と git-receive-pack
273Http::handle(Repository::open($dir), $action, ['push' => $userMayPush]);
274
275// SSH:gitユーザーのauthorized_keys
276// command="/usr/local/bin/php-8.5 /var/www/htdocs/gitssh.php suwako",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAA…
277exit(Ssh::handle((string)getenv('SSH_ORIGINAL_COMMAND'), fn(string $path, bool $write) => /* アクセスの確認 */ Repository::open('/var/www/git/'.$path)));
278```
279* OpenBSD では php-fpm は `/var/www` に chroot されるので、リポジトリはその中に置き(例:`git` ユーザーのホームを `/var/www/git` に)、`$shared = 'group'` で作り、`www` を `git` グループに入れると、サイトと SSH の両方から書けます。SSH でプロトコル v2 を使うには、`sshd_config` に `AcceptEnv GIT_PROTOCOL` を追加して下さい。
280* 未対応:新しい差分の作成(ルーズで受け取ったオブジェクトは差分無しでパックにする)、シンパック、部分クローン(`--filter`)、`--shallow-since`・`--shallow-exclude`、`git archive --remote`、LFS、commit-graph・multi-pack-index の書き込み。
281
282### Gitler(Git のホスティング)
283Gitler(`/gitler`)は上の Git のライブラリでサイトに Git のリポジトリをホストします:リポジトリのページ(ファイル、README、履歴、差分、blame、ブランチ、タグ、ZIP・TAR.GZ のダウンロード)、非公開のリポジトリ、共同作業者(読むだけ又は読み書き)、HTTPS と SSH のクローン・プッシュ。JavaScript は不要です。
284* `config/config.php` で `GITLER_ENABLED` を `true`(`AUTH_ENABLED` も必要)にして、`GITINFO` を設定して下さい:`root` はリポジトリのディレクトリ(php-fpm の chroot の中と外で同じ設定を使える様に、サイトのディレクトリからの相対パス。`/var/www/git` なら、サイトが `/var/www/htdocs` の場合は `../git`、`/var/www/htdocs/example.com/www` の場合は `../../../git`)、`ssh` はクローンの為に表示する SSH のアドレス(例:`git@example.com`、空なら HTTPS だけ)、`sshPort`、`php` は chroot の外の PHP のコマンド、`objectFormat` と `branch`(既定は `master`)は新しいリポジトリの形式とブランチ、`maxRepos` は1人の最大の数。
285* ロール:「Gitのリポジトリ」(自分のリポジトリを作る・管理する。管理者が付けるまで誰も持っていない)、「Gitのイシュー」(既定は全員)、「Gitのプルリクエスト」(既定は無し)。イシューとプルリクエストは今後。管理者は全部のリポジトリを見て変更出来ます。
286* HTTPS:`git clone https://example.com/gitler/user/repo.git`。公開のリポジトリはログイン無しでクローン出来ます。非公開のリポジトリとプッシュは、ユーザー名と `/gitler/-/keys` で作ったアクセストークン(読むだけ又は読み書き)を聞きます。二段階認証が無いユーザーはパスワードも使えます。プッシュ出来るのはブランチとタグだけです。OpenBSD の httpd が受け付けるリクエストの本文は既定で 1MB までなので、プッシュの為に `server` のブロックで増やして下さい(例:`connection max request body 536870912`)。
287* SSH:ユーザーは `/gitler/-/keys` で公開鍵を追加します。chroot の中にホームの有る `git` ユーザーを作り、`www` をそのグループに入れ、sshd が Gitler に鍵を聞く様にします:
288```sh
289useradd -m -d /var/www/git -s /bin/sh git
290usermod -G git www # php-fpmがリポジトリに書ける
291usermod -G www git # gitler.phpがconfig/config.phpとdata/を読める
292chmod 2775 /var/www/git
293```
294```
295# /etc/ssh/sshd_config
296Match User git
297 AuthorizedKeysCommand /usr/local/bin/php-8.5 /var/www/htdocs/gitler.php keys %u %t %k
298 AuthorizedKeysCommandUser git
299 AcceptEnv GIT_PROTOCOL
300 PasswordAuthentication no
301```
302 鍵毎に `gitler.php serve <ユーザー>` を実行するので、ユーザーが出来るのは `git clone`・`git push`(`git@example.com:user/repo.git`)だけです。`AuthorizedKeysCommand` を使わない場合は、`php gitler.php authorized-keys > /var/www/git/.ssh/authorized_keys`(例えば cron から)で同じ行を書けます。
303* リポジトリを整理する為に Cron のジョブ `gitlergc` を追加するか(例:`php cron.php add "@daily" gitlergc`)、`php gitler.php gc` を実行して下さい。
304* リポジトリのページには GitHub の様に言語の割合が出ます(バイト数で、ファイル名・拡張子・`#!` で決めます)。数えるのはプログラミングとマークアップの言語だけで、データ(JSON、YAML 等)、文章(Markdown 等)、外部のコード(`vendor/`、`node_modules/` 等)、ドキュメント(`docs/`、`examples/` 等)、生成されたファイルは数えません。ルートの `.gitattributes` の `linguist-language`、`linguist-vendored`、`linguist-generated`、`linguist-documentation`、`linguist-detectable` で変えられます(例:`*.h linguist-language=C++`)。結果はリポジトリの中の `gitler-languages.json` にツリー毎に保存し、一覧には既定のブランチの一番多い言語が出ます。
305* イシュー:ラベル、担当者、マイルストーン、Markdown の本文とコメント(「#3」は他のイシューへのリンク)。リポジトリを読めて「Gitのイシュー」のロールの有るユーザーが作ってコメント出来、書き込み権限の有る人がラベル等を付けて閉じます(作った人も閉じられます)。既定のブランチにプッシュしたコミットのメッセージの「fixes #3」「closes #3」「resolves #3」でイシューを閉じ、「#3」はイシューに記録します。設定でリポジトリ毎に無効に出来ます。データはリポジトリの中の `gitler/` です。
306* Fediverse(マルチユーザーの ActivityPub が有効な場合):公開のリポジトリは `@所有者.名前@ホスト` のアカウントになり、Mastodon 等からフォローするとイシューとコメントが届き、返信するとコメント、リポジトリをメンションした投稿は新しいイシューになります。ForgeFed の `Offer{Ticket}` と `Resolve` にも対応します(イシューの Ticket は `…/issues/3/ticket`)。リポジトリの設定でオフにしたり、アカウントやドメインをブロック出来ます(サイト全体のドメインのブロックも効きます)。配信には cron の `apdeliver` を使い、SSH のプッシュ(`git` ユーザー)で閉じたイシューは `gitler/spool/` に溜めて cron の `notify` が送ります。鍵の作成に GMP が必要です。
307* 通知:所有者と共同作業者は最初からリポジトリをウォッチし(新しいイシューと全部の動き)、他の人は作った・コメントした・担当の・メンション(`@ユーザー`)されたイシューの動きを受け取ります。リポジトリのページの「通知」でウォッチ・参加しているイシューだけ・無視を、イシューのページで受け取る・止めるを選べます。通知はサイトの「通知」に届き、`MAILER_ENABLED` ならメールでも届きます(`/gitler/-/keys` で止められます)。メールと SSH のプッシュの通知は cron の `notify` が送るので追加して下さい:`php cron.php add "* * * * *" notify`。
308* アーカイブ:リポジトリの設定でアーカイブすると読むだけになります(プッシュ、イシュー・コメント・ラベル・マイルストーンの変更、Fediverse からのコメントを止めます)。何時でも解除出来ます。
309* プルリクエスト(「Gitのプルリクエスト」のロールが必要):同じリポジトリのブランチ、フォーク(リポジトリのページの「フォークする」、「Gitのリポジトリ」のロールが必要)のブランチ、又はフォークしないで `git push origin HEAD:refs/for/master/トピック`(AGit、読めるだけのリポジトリにも。同じトピックにもう一度プッシュすると更新)から作れます。新しいブランチをプッシュすると、プルリクエストを作る URL が表示されます。会話、コミット、変更されたファイル(行番号を押すとその行にコメント、下書きをまとめてレビュー(コメント・承認・変更を依頼))が有り、書き込み権限の有る人がマージコミット・スカッシュ・リベース(出来れば fast-forward)でマージします(コンフリクトは PHP の3方向のマージで調べ、有ればローカルで解決)。本文の「fixes #3」でマージした時にイシューを閉じ、マージ先にプッシュで入ったプルリクエストはマージ済みになります。番号はイシューと同じで、変更は `refs/pull/番号/head` に有ります(`git fetch origin refs/pull/3/head:pr-3`)。リポジトリ毎に設定で無効に出来ます。
310
311### TLS 1.3・1.2(php_openssl は不要)
312HTTPS のリクエスト(`Curl`、ActivityPub)、SMTP の STARTTLS(`Mailer`)、MySQL・PostgreSQL の TLS は PHP だけで書かれた TLS 1.3・1.2 のクライアント `Std\Lib\Tls`(`php_gmp` が必要)を使うので、`php_openssl` は不要です。RSA の鍵と署名(ActivityPub)は `Std\Lib\Crypto\Rsa` を使い、今までの PEM の鍵もそのまま使えます。
313* TLS 1.3 を優先し、TLS 1.2 にしか対応していないサーバーとは安全な暗号スイートだけで TLS 1.2 を使います(ECDHE と AES-GCM 又は ChaCha20-Poly1305、サーバーが対応していれば Extended Master Secret、再ネゴシエーションはしない)。CBC、RSA の鍵交換、DHE、TLS 1.1 以前は未対応です。TLS 1.3 に対応しているサーバーとの通信を攻撃者が TLS 1.2 に下げた場合は分かります。TLS 1.2 を拒否するには、オプションの `'min_version' => '1.3'` を使って下さい。
314* 証明書は `config/config.php` の `TLS_CAFILE`(信頼する CA の PEM ファイル)で確認します。空なら、普通のシステムの場所を探します(OpenBSD は `/etc/ssl/cert.pem`)。httpd・php-fpm の chroot の中では `/var/www/etc/ssl/cert.pem` になるので、システムのファイルをそこにコピーして下さい。Windows にはこのファイルが無いので、`TLS_CAFILE` を設定して下さい(例:curl の `cacert.pem`)。
315* 暗号スイート:TLS 1.3 は `TLS_CHACHA20_POLY1305_SHA256`、`TLS_AES_128_GCM_SHA256`、`TLS_AES_256_GCM_SHA384`、TLS 1.2 は `TLS_ECDHE_{ECDSA,RSA}_WITH_{CHACHA20_POLY1305_SHA256,AES_128_GCM_SHA256,AES_256_GCM_SHA384}`。鍵交換:x25519、secp256r1、secp384r1。署名:RSA-PSS、ECDSA(P-256、P-384、P-521)、RSA PKCS#1 v1.5(証明書と TLS 1.2 のみ)。セッションの再開、0-RTT、クライアント証明書、失効の確認は未対応です。
316* 暗号化は PHP で計算するので、大きいファイルのダウンロードは openssl より遅いです(数 MB/秒)。
317```php
318$s = \Std\Lib\Tls::connect('example.com', 443);
319fwrite($s, "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n");
320echo stream_get_contents($s);
321```
322
323### コメント
324`config/config.php` で `COMMENTS_ENABLED` を `true` にすると、全部のブログ記事の下にコメント欄が表示されます(書き込みには `AUTH_ENABLED` も必要)。
325* ログイン中のユーザーはコメント、返信、いいねが出来ます(Markdown:太字、斜体、取り消し線、コード、引用、リスト、リンク)。自分のコメントは削除出来、管理者とスタッフは全部のコメントを削除出来ます。JavaScript は不要です。
326* `BLOGS` で `'federate' => true` のブログは `FEDIINFO` のアカウントが投稿し(`apfederate`)、その投稿(と返信)への Fediverse からの公開・未収載の返信がコメントになります。Fediverse での編集と削除も反映します。
327* 新しいコメントは全部の管理者に、自分のコメントへの返信といいねは本人に `/notifications` で通知します。
328* コメントは `data/comments/`、通知は `data/notifications/` に保存します。
329
330### Cron
331定期的なタスク(例えば、新しいブログ投稿を ActivityPub のフォロワーに送る `apfederate`)はシステムの cron で実行します。\
332`config/config.php` で `CRON_ENABLED` を `true` にして(ActivityPub を使う場合は `FEDIINFO['domain']` も設定して)、
333php-fpm を実行するユーザーの crontab に以下の行を追加して下さい(`crontab -u www -e`):
334```
335* * * * * /usr/local/bin/php-8.5 /var/www/htdocs/cron.php
336```
337
338ジョブはコマンドライン(又は管理画面)で管理します:
339```sh
340php cron.php add "*/10 * * * *" apfederate
341php cron.php list
342php cron.php help
343```
344
345タスクは `util.php` の `register_cron_tasks()` で登録します(`cron.php` と管理画面で使います)。組み込みのタスク:
346* `apfederate`:新しいブログ投稿を ActivityPub のフォロワーに送る
347* `apupdate`:プロフィール(アクター)の更新を ActivityPub のフォロワーに送る
348* `authpurge`:7日以内にメールアドレスを確認しなかったアカウントを削除する(`AUTH_VERIFY_EMAIL` が有効の場合)
349* `apdeliver`:マイクロブログのアクティビティを他のサーバーに配信する(マルチユーザーモード、毎分実行)
350* `apcleanup`:90日より古いリモートの投稿とキャッシュを削除する(マルチユーザーモード、1日1回実行)
351* `notify`:通知のメールを送り、SSH のプッシュで Gitler のリポジトリに溜めた通知と配信を処理する(`AUTH_ENABLED` が有効の場合、毎分実行)
352* `gitlergc`:Gitler のリポジトリを整理する(`GITLER_ENABLED` が有効の場合、1日1回実行)
353
354### マイクロブログ(マルチユーザーの Fediverse)
355既定では ActivityPub はシングルユーザーで、ブログのアクター(`/ap/actor`)が新しいブログ投稿をフォロワーに送ります。\
356`FEDIINFO['isSingleUser']` を `false` にすると(`AUTH_ENABLED` と `FEDIINFO['domain']` も必要)マルチユーザーモードになります:
357* ブログのアクターは今まで通り動きます。
358* Fediverse のロールを持つユーザーはそれぞれアクター(`@ユーザー名@ドメイン`)になります。ロールは BAN されたユーザー以外の全員に付与され、管理者はユーザーのプロフィールの編集画面で外せます。
359* `/microblog` にはホーム・ローカル・連合のタイムライン、投稿(注意書き、公開範囲、画像・動画4つまで)、返信、いいね、ブースト、フォロー(承認制も可)、ブロック、通報、検索(`@user@host`、投稿の URL、`#タグ`、文字)があります。JavaScript は不要です。
360* 管理者は `/microblog/admin` でサーバーごとブロックしたり、通報を確認したり出来ます。
361
362配信は待ち行列に入るので、cron のジョブを追加して下さい:
363```sh
364php cron.php add "* * * * *" apdeliver
365php cron.php add "@daily" apcleanup
366```
367アップロード出来るのは画像(PNG・JPEG・GIF・WebP、20 MiB まで)と動画(MP4・MKV・WebM、400 MiB まで)で、メタデータ(Exif、XMP、テキスト、MP4 の撮影日時・位置等)を取り除きます。動画は変換しないので、ブラウザで再生出来る形式(H.264 等)でアップロードして下さい。\
368大きいファイルを受け付ける様に、PHP の `upload_max_filesize` と `post_max_size`(添付4つ分)、httpd の `connection max request body`(既定は1 MiB、超えると「413 Payload Too Large」)も設定して下さい。動画を再生出来る様に、httpd の `types` に `include "/usr/share/misc/mime.types"` もお勧めします。
369他のサーバーのアイコンや画像は `/microblog/media` 経由で表示するので、CSP の `img-src` は `'self'` のままで構いません(閲覧者の IP アドレスも相手のサーバーに渡りません)。画像は `data/ap/remote/media/` に1週間キャッシュします。\
370ユーザーの鍵(RSA、2048ビット)は初めて使う時に作ります。
371
372### 二段階認証
373`config/config.php` で `AUTH_2FA_ENABLED` を `true` にすると、各ユーザーがプロフィールの編集の「二段階認証」から有効に出来ます。`false` なら誰も使えず、有効にしていたユーザーもパスワードだけでログインします。\
374設定画面には QR コードと秘密鍵(PC のパスワードマネージャー用)が表示されるので、認証アプリ(Aegis、Google Authenticator、KeePassXC 等)に登録して、表示された6桁のコードを入力すると有効になります。以後はパスワードの後にコードを入力します(TOTP、SHA-1、6桁、30秒。同じコードは2度使えません)。\
375端末を失くした場合は、管理者がそのユーザーの設定画面(`/profile/ユーザー名/2fa`)で自分のパスワードを入力すると無効に出来ます。
376
377### 管理画面
378`config/config.php` で `ADMIN_ENABLED` を `true` にすると(`AUTH_ENABLED` も必要)、管理者のロールを持つユーザーのメニューに「管理」が出て、`/admin` を開けます:
379* ユーザー:アカウントの一覧と検索、作成(登録が無効でも、メールアドレスの確認なし)、ロールの変更(全部外すと BAN)、メールアドレスを確認済みにする、全部のログインの取り消し、メールアドレス・パスワードの変更と二段階認証の無効化(この3つは自分のパスワードが必要)。表示名・アイコン等は今まで通りプロフィールの編集で変更します。
380* ログ:`log/` のファイル。最後の行、検索、ダウンロード、空にする、削除。
381* Cron:ジョブの追加、変更、有効・無効、削除、今すぐ実行と、システムの cron の前回の実行。
382* キャッシュ:`data/cache/` を種類毎に表示・削除(1件、種類毎、全部)と、Fediverse のキャッシュ(リモートのアクター、WebFinger、画像)の削除。
383* データベースの変換:上の変換をフォームで。読む元・書く先は JSON のデータ(`data/`)、SQLite のファイル、MySQL・MariaDB 又は PostgreSQL のサーバー、SQL のダンプ(アップロード又はサーバーのファイル、書く先はダウンロード又はサーバーに保存)、サイトのデータベース(`DBINFO`)。パスはサイトのディレクトリからの相対パスで、その外は使えません。アップロードしたファイルと変換したファイルは `data/tmp/convert/` に1時間残ります。大きなファイルをアップロードするには、`php.ini` の `upload_max_filesize` と `post_max_size` を大きくして下さい。
384
385新しいサイトには管理者が居なく、`data/` が空の場合も有ります。`ADMIN_SETUP_CODE` に長いランダムな文字列(例:`openssl rand -hex 16` の出力)を設定して `/admin` を開くと `/admin/setup` に移動するので、そのコードで最初の管理者を作ってログインします。その後 `ADMIN_SETUP_CODE` を `''` に戻して下さい。管理者が居ればこのページは使えません。
386
387## 必要な PHP モジュール
388* php_fileinfo(OpenBSDでデフォルトで有効している)
389* php_gmp
390* php_mbstring(OpenBSDでデフォルトで有効している)
391* php_zlib(Git、普通はPHPに組み込まれている)