# Little Beast シンプル、実践的、アンチ・ブロート ![GitHub repo size](https://img.shields.io/github/repo-size/TechnicalSuwako/LittleBeast) [![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) サンプル:[https://lbdemo.technicalsuwako.moe/](https://lbdemo.technicalsuwako.moe/) ## Little Beast とは? Little Beast は PHP 8.5 以上向けのフレームワークで、076.moe(ゲーム開発会社)と technicalsuwako.moe(社長のブログ)向けに作られました。\ メイン考え方は「必要な物だけ拡張し、不要な物は削除する」です。\ 各コア機能はライブラリに分割されている為、必要な物だけを選び易い設計になっています。\ 全てのモジュールはゼロから書かれており、データベースは一切必要ありません。 Little Beast はテクニカル諏訪子がゲーム開発に完全復帰する前の最後の Web プロジェクトです。 ## Little Beast が「ではない」物 * 汎用フレームワーク * 使い憎い * Web 開発者向け * 万人向け * インストールが面倒 * 教条的 * 民主的に運営される ## Little Beast が「持っていない」物 * データベース(全てファイルベース) * 依存関係 * パッケージマネージャー * Docker/Kubernetes/Vagrant/Nix 等のコンテナ * JavaScript * ブロートウェア * ORM * キューやバックグラウンドジョブ * クラウド/サーバーレス統合 * スキャフォールディング * コード生成 * 抽象化レイヤー * 不要なファイル * DEI、行動規範、その他の差別的慣行 ## 独自機能 * データベース不要 * Composer や PEAR 不要 * サーバーオーバーヘッドゼロ * コンテナ不要 * Maron テンプレートエンジン * カスタム Markdown * ActivityPub * 画像ライブラリ(PNG・APNG・BMP・TGA・GIF・JPEG・WebP の読み書き。GD 等の拡張は不要) * 二段階認証(認証アプリのワンタイムパスワード、QR コード) * 多言語対応 * マルチブログ対応 * 100% 綺麗で正しい HTML5 と CSS3 * 100% 正しい PHP * SEO フレンドリー * モジュラー CSS * Atom フィード * 組み込みテストスイート ## インストール方法 ```sh cd /var/www/htdocs git clone https://github.com/TechnicalSuwako/LittleBeast.git . mv config/config.sample.php config/config.php ``` HTTP サーバーを設定して `/public` をルートとして `php-fpm` で実行して下さい。\ それだけです! ### OpenBSD サーバー ```sh pkg_add php-8.5.5 php-gmp-8.5.5 rcctl enable php85_fpm httpd relayd rcctl start php85_fpm httpd relayd ``` #### httpd ``` server "technicalsuwako.moe" { listen on * tls port 8443 gzip-static tls { certificate "/etc/ssl/technicalsuwako.moe.crt" key "/etc/ssl/private/technicalsuwako.moe.key" } root "/htdocs/technicalsuwako.moe/www/public" directory index "index.php" location "/.well-known/acme-challenge/*" { root "/acme" request strip 2 } location "/*.php" { fastcgi socket "/run/php-fpm.sock" } location "/*.php[/?]*" { fastcgi socket "/run/php-fpm.sock" } location "/" { directory index "index.php" } location match "/blog/" { request rewrite "/index.php" } location match "/about" { request rewrite "/index.php" } location match "/monero" { request rewrite "/index.php" } location match "/secret" { request rewrite "/index.php" } location match "/ap/" { request rewrite "/index.php" } location "/.well-known/webfinger" { request rewrite "/index.php" } location "*.atom" { request rewrite "/index.php" } location match "/comments" { request rewrite "/index.php" } location "/notifications" { request rewrite "/index.php" } } server "www.technicalsuwako.moe" { listen on * tls port 8443 gzip-static tls { certificate "/etc/ssl/technicalsuwako.moe.crt" key "/etc/ssl/private/technicalsuwako.moe.key" } block return 301 "https://technicalsuwako.moe$REQUEST_URI" } ``` #### relayd ``` relayd_addr="0.0.0.0" router_addr="192.168.10.106" table { $router_addr } http protocol reverse { tcp { nodelay, sack, socket buffer 65536, backlog 100 } tls ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256" tls keypair "technicalsuwako.moe" return error match request header append "X-Forwarded-For" value "$REMOTE_ADDR" match request header append "X-Forwarded-Port" value "$REMOTE_PORT" #match response header set "Referrer-Policy" value "same-origin" match response header set "X-Frame-Options" value "deny" match response header set "X-Content-Type-Options" value "nosniff" match response header set "Referrer-Policy" value "strict-origin-when-cross-origin" match response header set "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload" match response header set "Cross-Origin-Opener-Policy" value "same-origin" 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';" match response header append "Permissions-Policy" value "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=()" match request header "Accept-Encoding" value "gzip" tag "gzip" match response header "Content-Type" value "text/*" tag "compress" match response header "Content-Type" value "text/html" tag "charset=UTF-8" match response header "Content-Type" value "application/javascript" tag "compress" match response header "Content-Type" value "application/json" tag "compress" pass request quick header "Host" value "technicalsuwako.moe" forward to pass } relay www_tls { listen on $relayd_addr port 443 tls protocol reverse # Default forward to port 8443 check tcp } relay www_www { listen on $relayd_addr port 80 protocol reverse # Default forward to port 8080 check tcp } ``` ### ブログと Atom フィード 1つのサイトに複数のブログを置けます。`config/config.php` の `BLOGS` の各ディレクトリ(例:`blog`、`enblog`)がブログになり、それぞれ `/ディレクトリ.atom` の Atom フィードがあります(一覧と検索結果のページの上にリンクがあります)。\ `BLOGS` が無ければ、今まで通り `blog` だけです。 ### 静的ファイル(ASS_DOMAIN) `config/config.php` の `ASS_DOMAIN` で静的ファイル(画像、CSS、アイコン、アップロードしたファイル)の場所を決めます。無ければ `/static` です。 * パス(例:`/static`)なら、このサイトの `public/` の下のディレクトリです(`/static` は `public/static/`)。 * URL(例:`https://ass.example.com/static`)なら、別のサーバー(サブドメイン等)です。アップロードしたファイルは今まで通り `public/` の下の同じパス(`public/static/`)に保存するので、そのディレクトリを静的ファイルのサーバーと同期して下さい(又はそのサーバーから配信して下さい)。 テンプレートやコードでは、URLは `ass_url('article/a.png')`、ディスク上のファイルは `ROOT.ass_path('article/a.png')` を使います。 ### データベース(MySQL・MariaDB、PostgreSQL、SQLite) Little Beast 自体はデータベースを使いませんが、必要な場合(既存のアプリの移植等)は `Std\Lib\Db` で繋げます。ドライバは PHP だけで書かれているので、mysqli・pdo_mysql・pgsql・pdo_pgsql・sqlite3・pdo_sqlite・sockets は不要です。\ `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 コンテキストオプションの配列)。 ```php $db = new \Std\Lib\Db(); $rows = $db->query('SELECT * FROM users WHERE id = ?', [1])['rows']; $id = $db->query('INSERT INTO users (name) VALUES (?)', ['諏訪子'])['insertId']; // MySQL・SQLite $id = $db->query('INSERT INTO users (name) VALUES (?) RETURNING id', ['諏訪子'])['rows'][0]['id']; // PostgreSQL $db->transaction(function(\Std\Lib\Db $db): void { $db->query('UPDATE users SET points = points - ? WHERE id = ?', [100, 1]); $db->query('UPDATE users SET points = points + ? WHERE id = ?', [100, 2]); }); ``` * 最初のクエリで接続します。パラメータ付きのクエリはプリペアドステートメントで実行するので、値をエスケープする必要はありません。 * 整数の列は `int`、`FLOAT`・`DOUBLE`(PostgreSQL は `real`・`double precision`)は `float`、`NULL` は `null`、その他(`DECIMAL`・`numeric`、日付、JSON、`BLOB` 等)は文字列で返します。PostgreSQL の `boolean` は `bool`、`bytea` は生のバイト、配列は PHP の配列で返します。 * エラーは `Std\Lib\Db\DbException`(`getCode()` は MySQL のエラーコード、例:重複は1062、`sqlState` は SQLSTATE、例:PostgreSQL の重複は23505)を投げ、`log/db_log.txt` に書きます。 * 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` は未対応です。 * 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` で明示も可)。 * PostgreSQL の認証:SCRAM-SHA-256(TLS ならチャネルバインディング付きの SCRAM-SHA-256-PLUS、オプションの `channel_binding` は `prefer`・`require`・`disable`)、MD5、`password`(`allow_cleartext` を `true` にしない限り、TLS か UNIX ソケットのみ)、`trust`。GSSAPI・SSPI は未対応です。 * PostgreSQL:`?` のプレースホルダは `$1`、`$2`… にします(文字列・識別子・コメントの外のみ。`?` そのもの(jsonb の演算子等)は `??` と書く)。`$1` もそのまま使えます。挿入した ID は無いので、`RETURNING` を使って下さい。トランザクションの中でエラーが起きた後の `COMMIT` は、PostgreSQL がロールバックするので `DbException`(SQLSTATE 25P02)を投げます。`COPY`、`LISTEN` の通知、実行中のクエリの取り消しは未対応です。PostgreSQL 16 で確認しました。 * テストは偽物のサーバーを使います。PostgreSQL のテストを本物のサーバーでも実行するには、`LB_TEST_PGSQL=pgsql://ユーザー:パスワード@ホスト:ポート/データベース` を設定して下さい(TLS は `?cafile=/path/ca.pem` を追加)。名前が `lb_test_` で始まるテーブルを作って消します。 ### データベースの変換 `Std\Lib\Db\Convert` は、MySQL・MariaDB、PostgreSQL、SQLite、SQL のダンプのファイル、`data/` の Little Beast の JSON のデータの間で、テーブルと行をどの方向にもコピーします。ドライバーと同じく PHP だけで書いてあります。 ```php use Std\Lib\Db; use Std\Lib\Db\Convert; use Std\Lib\Db\Convert\{DbSource, DbTarget, DumpSource, DumpTarget, JsonSource, JsonTarget}; // data/(JSON)→ SQLite $report = Convert::run(new JsonSource(ROOT.'/data'), new DbTarget(new Db(['driver' => 'sqlite', 'path' => 'data/site.sqlite']))); // PostgreSQL → MySQL のダンプのファイル Convert::run(new DbSource(new Db(['driver' => 'pgsql', 'host' => '127.0.0.1', 'port' => 5432, 'username' => 'lb', 'password' => '…', 'dbname' => 'lb'])), new DumpTarget('/tmp/lb.sql', 'mysql')); // mysqldump・pg_dump のファイル → PostgreSQL(既に有るテーブルは置き換える) Convert::run(new DumpSource('/tmp/dump.sql'), new DbTarget(new Db()), ['drop' => true]); ``` * 読む元:`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`。 * オプション:`tables`(このテーブルだけ)、`drop`(書く先の同じ名前のテーブルを消す、無ければ既に有るテーブルはエラー)、`progress`(`fn(string $table, int $rows)`)。結果は `tables`(テーブル毎のコピーした行数)と `warnings`(変換しなかった物)。 * 列、主キー、ユニーク・普通のインデックス、`NOT NULL`、既定値、自動の番号(`AUTO_INCREMENT`、`SERIAL`・IDENTITY、SQLite の `INTEGER PRIMARY KEY`)を変換し、番号はコピーした行の続きから始まります。型は書く先の一番近い型にします(例:`TINYINT(1)` と `boolean`、`JSON` と `jsonb`、`BLOB` と `bytea`)。`ENUM`・`SET` は文字列、PostgreSQL の配列は JSON にします。時差の有る日時は UTC で保存します。外部キー、ビュー、トリガー、関数、全文・式のインデックスは変換しません(`warnings` に出ます)。 * JSON のデータ:ユーザー、コメント、通知、Cron のジョブ、フォロワー、ActivityPub の投稿は名前が `lb_` で始まるテーブルになり、他のファイル(`cache/`、`ratelimit/`、`tmp/`、ロック・一時ファイル、SQLite のデータベースを除く)は `lb_files` に入るので、戻すと同じファイルになります。他のテーブルは `data/db/{テーブル}.json` になります。`data/` が空・無い場合は、テーブルが無いだけです。`JsonTarget` は、2番目の引数(上書き)が `true` か `drop` が無ければ、空でないディレクトリには書きません。 * サイトが使っているデータベースを読む・書く場合は、先に書き込みを止めて下さい(又は新しいデータベースに変換)。変換中に元をロックしません。 ### Git `Std\Lib\Git` は Git のリポジトリを PHP だけで読み書きします(PHP の zlib が必要、`git` のコマンドは不要)。SHA-1 と SHA-256 のリポジトリに対応しています。サイトでリポジトリをホストする為の基礎です。 ```php use Std\Lib\Git\{Repository, Tree, Commit, Person, Diff, Blame}; $repo = Repository::init('/var/www/git/suwako/test.git', 'sha1', 'main', 'group'); // 又はRepository::open(...) $me = Person::now('諏訪子', 'suwako@example.com'); $tree = $repo->updateTree(null, ['README.md' => [Tree::FILE, "# テスト\n"]]); $id = $repo->writeCommit(Commit::create($tree, [], $me, $me, "最初のコミット\n")); $repo->refs->update('refs/heads/main', $id, $repo->zeroId()); // 途中でブランチが変わっていれば失敗 foreach ($repo->log('main', ['path' => 'README.md', 'limit' => 20]) as $c) echo $c->id, ' ', $c->subject(), "\n"; echo $repo->blob($repo->resolve('main~1:README.md')); echo Diff::unified($repo, 'main~1', 'main'); $lines = Blame::file($repo, 'main', 'README.md'); ``` * 保存:ルーズオブジェクト、パック(`.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 で実行)。 * ネットワーク:`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` のコールバックを渡せます(保護されたブランチ等)。 ```php // GET …/repo.git/info/refs、POST …/repo.git/git-upload-pack と git-receive-pack Http::handle(Repository::open($dir), $action, ['push' => $userMayPush]); // SSH:gitユーザーのauthorized_keys // 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… exit(Ssh::handle((string)getenv('SSH_ORIGINAL_COMMAND'), fn(string $path, bool $write) => /* アクセスの確認 */ Repository::open('/var/www/git/'.$path))); ``` * OpenBSD では php-fpm は `/var/www` に chroot されるので、リポジトリはその中に置き(例:`git` ユーザーのホームを `/var/www/git` に)、`$shared = 'group'` で作り、`www` を `git` グループに入れると、サイトと SSH の両方から書けます。SSH でプロトコル v2 を使うには、`sshd_config` に `AcceptEnv GIT_PROTOCOL` を追加して下さい。 * 未対応:新しい差分の作成(ルーズで受け取ったオブジェクトは差分無しでパックにする)、シンパック、部分クローン(`--filter`)、`--shallow-since`・`--shallow-exclude`、`git archive --remote`、LFS、commit-graph・multi-pack-index の書き込み。 ### Gitler(Git のホスティング) Gitler(`/gitler`)は上の Git のライブラリでサイトに Git のリポジトリをホストします:リポジトリのページ(ファイル、README、履歴、差分、blame、ブランチ、タグ、ZIP・TAR.GZ のダウンロード)、非公開のリポジトリ、共同作業者(読むだけ又は読み書き)、HTTPS と SSH のクローン・プッシュ。JavaScript は不要です。 * `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人の最大の数。 * ロール:「Gitのリポジトリ」(自分のリポジトリを作る・管理する。管理者が付けるまで誰も持っていない)、「Gitのイシュー」(既定は全員)、「Gitのプルリクエスト」(既定は無し)。イシューとプルリクエストは今後。管理者は全部のリポジトリを見て変更出来ます。 * HTTPS:`git clone https://example.com/gitler/user/repo.git`。公開のリポジトリはログイン無しでクローン出来ます。非公開のリポジトリとプッシュは、ユーザー名と `/gitler/-/keys` で作ったアクセストークン(読むだけ又は読み書き)を聞きます。二段階認証が無いユーザーはパスワードも使えます。プッシュ出来るのはブランチとタグだけです。OpenBSD の httpd が受け付けるリクエストの本文は既定で 1MB までなので、プッシュの為に `server` のブロックで増やして下さい(例:`connection max request body 536870912`)。 * SSH:ユーザーは `/gitler/-/keys` で公開鍵を追加します。chroot の中にホームの有る `git` ユーザーを作り、`www` をそのグループに入れ、sshd が Gitler に鍵を聞く様にします: ```sh useradd -m -d /var/www/git -s /bin/sh git usermod -G git www # php-fpmがリポジトリに書ける usermod -G www git # gitler.phpがconfig/config.phpとdata/を読める chmod 2775 /var/www/git ``` ``` # /etc/ssh/sshd_config Match User git AuthorizedKeysCommand /usr/local/bin/php-8.5 /var/www/htdocs/gitler.php keys %u %t %k AuthorizedKeysCommandUser git AcceptEnv GIT_PROTOCOL PasswordAuthentication no ``` 鍵毎に `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 から)で同じ行を書けます。 * リポジトリを整理する為に Cron のジョブ `gitlergc` を追加するか(例:`php cron.php add "@daily" gitlergc`)、`php gitler.php gc` を実行して下さい。 * リポジトリのページには 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` にツリー毎に保存し、一覧には既定のブランチの一番多い言語が出ます。 * イシュー:ラベル、担当者、マイルストーン、Markdown の本文とコメント(「#3」は他のイシューへのリンク)。リポジトリを読めて「Gitのイシュー」のロールの有るユーザーが作ってコメント出来、書き込み権限の有る人がラベル等を付けて閉じます(作った人も閉じられます)。既定のブランチにプッシュしたコミットのメッセージの「fixes #3」「closes #3」「resolves #3」でイシューを閉じ、「#3」はイシューに記録します。設定でリポジトリ毎に無効に出来ます。データはリポジトリの中の `gitler/` です。 * Fediverse(マルチユーザーの ActivityPub が有効な場合):公開のリポジトリは `@所有者.名前@ホスト` のアカウントになり、Mastodon 等からフォローするとイシューとコメントが届き、返信するとコメント、リポジトリをメンションした投稿は新しいイシューになります。ForgeFed の `Offer{Ticket}` と `Resolve` にも対応します(イシューの Ticket は `…/issues/3/ticket`)。リポジトリの設定でオフにしたり、アカウントやドメインをブロック出来ます(サイト全体のドメインのブロックも効きます)。配信には cron の `apdeliver` を使い、SSH のプッシュ(`git` ユーザー)で閉じたイシューは `gitler/spool/` に溜めて cron の `notify` が送ります。鍵の作成に GMP が必要です。 * 通知:所有者と共同作業者は最初からリポジトリをウォッチし(新しいイシューと全部の動き)、他の人は作った・コメントした・担当の・メンション(`@ユーザー`)されたイシューの動きを受け取ります。リポジトリのページの「通知」でウォッチ・参加しているイシューだけ・無視を、イシューのページで受け取る・止めるを選べます。通知はサイトの「通知」に届き、`MAILER_ENABLED` ならメールでも届きます(`/gitler/-/keys` で止められます)。メールと SSH のプッシュの通知は cron の `notify` が送るので追加して下さい:`php cron.php add "* * * * *" notify`。 * アーカイブ:リポジトリの設定でアーカイブすると読むだけになります(プッシュ、イシュー・コメント・ラベル・マイルストーンの変更、Fediverse からのコメントを止めます)。何時でも解除出来ます。 * プルリクエスト(「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`)。リポジトリ毎に設定で無効に出来ます。 ### TLS 1.3・1.2(php_openssl は不要) HTTPS のリクエスト(`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 の鍵もそのまま使えます。 * 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'` を使って下さい。 * 証明書は `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`)。 * 暗号スイート: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、クライアント証明書、失効の確認は未対応です。 * 暗号化は PHP で計算するので、大きいファイルのダウンロードは openssl より遅いです(数 MB/秒)。 ```php $s = \Std\Lib\Tls::connect('example.com', 443); fwrite($s, "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"); echo stream_get_contents($s); ``` ### コメント `config/config.php` で `COMMENTS_ENABLED` を `true` にすると、全部のブログ記事の下にコメント欄が表示されます(書き込みには `AUTH_ENABLED` も必要)。 * ログイン中のユーザーはコメント、返信、いいねが出来ます(Markdown:太字、斜体、取り消し線、コード、引用、リスト、リンク)。自分のコメントは削除出来、管理者とスタッフは全部のコメントを削除出来ます。JavaScript は不要です。 * `BLOGS` で `'federate' => true` のブログは `FEDIINFO` のアカウントが投稿し(`apfederate`)、その投稿(と返信)への Fediverse からの公開・未収載の返信がコメントになります。Fediverse での編集と削除も反映します。 * 新しいコメントは全部の管理者に、自分のコメントへの返信といいねは本人に `/notifications` で通知します。 * コメントは `data/comments/`、通知は `data/notifications/` に保存します。 ### Cron 定期的なタスク(例えば、新しいブログ投稿を ActivityPub のフォロワーに送る `apfederate`)はシステムの cron で実行します。\ `config/config.php` で `CRON_ENABLED` を `true` にして(ActivityPub を使う場合は `FEDIINFO['domain']` も設定して)、 php-fpm を実行するユーザーの crontab に以下の行を追加して下さい(`crontab -u www -e`): ``` * * * * * /usr/local/bin/php-8.5 /var/www/htdocs/cron.php ``` ジョブはコマンドライン(又は管理画面)で管理します: ```sh php cron.php add "*/10 * * * *" apfederate php cron.php list php cron.php help ``` タスクは `util.php` の `register_cron_tasks()` で登録します(`cron.php` と管理画面で使います)。組み込みのタスク: * `apfederate`:新しいブログ投稿を ActivityPub のフォロワーに送る * `apupdate`:プロフィール(アクター)の更新を ActivityPub のフォロワーに送る * `authpurge`:7日以内にメールアドレスを確認しなかったアカウントを削除する(`AUTH_VERIFY_EMAIL` が有効の場合) * `apdeliver`:マイクロブログのアクティビティを他のサーバーに配信する(マルチユーザーモード、毎分実行) * `apcleanup`:90日より古いリモートの投稿とキャッシュを削除する(マルチユーザーモード、1日1回実行) * `notify`:通知のメールを送り、SSH のプッシュで Gitler のリポジトリに溜めた通知と配信を処理する(`AUTH_ENABLED` が有効の場合、毎分実行) * `gitlergc`:Gitler のリポジトリを整理する(`GITLER_ENABLED` が有効の場合、1日1回実行) ### マイクロブログ(マルチユーザーの Fediverse) 既定では ActivityPub はシングルユーザーで、ブログのアクター(`/ap/actor`)が新しいブログ投稿をフォロワーに送ります。\ `FEDIINFO['isSingleUser']` を `false` にすると(`AUTH_ENABLED` と `FEDIINFO['domain']` も必要)マルチユーザーモードになります: * ブログのアクターは今まで通り動きます。 * Fediverse のロールを持つユーザーはそれぞれアクター(`@ユーザー名@ドメイン`)になります。ロールは BAN されたユーザー以外の全員に付与され、管理者はユーザーのプロフィールの編集画面で外せます。 * `/microblog` にはホーム・ローカル・連合のタイムライン、投稿(注意書き、公開範囲、画像・動画4つまで)、返信、いいね、ブースト、フォロー(承認制も可)、ブロック、通報、検索(`@user@host`、投稿の URL、`#タグ`、文字)があります。JavaScript は不要です。 * 管理者は `/microblog/admin` でサーバーごとブロックしたり、通報を確認したり出来ます。 配信は待ち行列に入るので、cron のジョブを追加して下さい: ```sh php cron.php add "* * * * *" apdeliver php cron.php add "@daily" apcleanup ``` アップロード出来るのは画像(PNG・JPEG・GIF・WebP、20 MiB まで)と動画(MP4・MKV・WebM、400 MiB まで)で、メタデータ(Exif、XMP、テキスト、MP4 の撮影日時・位置等)を取り除きます。動画は変換しないので、ブラウザで再生出来る形式(H.264 等)でアップロードして下さい。\ 大きいファイルを受け付ける様に、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"` もお勧めします。 他のサーバーのアイコンや画像は `/microblog/media` 経由で表示するので、CSP の `img-src` は `'self'` のままで構いません(閲覧者の IP アドレスも相手のサーバーに渡りません)。画像は `data/ap/remote/media/` に1週間キャッシュします。\ ユーザーの鍵(RSA、2048ビット)は初めて使う時に作ります。 ### 二段階認証 `config/config.php` で `AUTH_2FA_ENABLED` を `true` にすると、各ユーザーがプロフィールの編集の「二段階認証」から有効に出来ます。`false` なら誰も使えず、有効にしていたユーザーもパスワードだけでログインします。\ 設定画面には QR コードと秘密鍵(PC のパスワードマネージャー用)が表示されるので、認証アプリ(Aegis、Google Authenticator、KeePassXC 等)に登録して、表示された6桁のコードを入力すると有効になります。以後はパスワードの後にコードを入力します(TOTP、SHA-1、6桁、30秒。同じコードは2度使えません)。\ 端末を失くした場合は、管理者がそのユーザーの設定画面(`/profile/ユーザー名/2fa`)で自分のパスワードを入力すると無効に出来ます。 ### 管理画面 `config/config.php` で `ADMIN_ENABLED` を `true` にすると(`AUTH_ENABLED` も必要)、管理者のロールを持つユーザーのメニューに「管理」が出て、`/admin` を開けます: * ユーザー:アカウントの一覧と検索、作成(登録が無効でも、メールアドレスの確認なし)、ロールの変更(全部外すと BAN)、メールアドレスを確認済みにする、全部のログインの取り消し、メールアドレス・パスワードの変更と二段階認証の無効化(この3つは自分のパスワードが必要)。表示名・アイコン等は今まで通りプロフィールの編集で変更します。 * ログ:`log/` のファイル。最後の行、検索、ダウンロード、空にする、削除。 * Cron:ジョブの追加、変更、有効・無効、削除、今すぐ実行と、システムの cron の前回の実行。 * キャッシュ:`data/cache/` を種類毎に表示・削除(1件、種類毎、全部)と、Fediverse のキャッシュ(リモートのアクター、WebFinger、画像)の削除。 * データベースの変換:上の変換をフォームで。読む元・書く先は JSON のデータ(`data/`)、SQLite のファイル、MySQL・MariaDB 又は PostgreSQL のサーバー、SQL のダンプ(アップロード又はサーバーのファイル、書く先はダウンロード又はサーバーに保存)、サイトのデータベース(`DBINFO`)。パスはサイトのディレクトリからの相対パスで、その外は使えません。アップロードしたファイルと変換したファイルは `data/tmp/convert/` に1時間残ります。大きなファイルをアップロードするには、`php.ini` の `upload_max_filesize` と `post_max_size` を大きくして下さい。 新しいサイトには管理者が居なく、`data/` が空の場合も有ります。`ADMIN_SETUP_CODE` に長いランダムな文字列(例:`openssl rand -hex 16` の出力)を設定して `/admin` を開くと `/admin/setup` に移動するので、そのコードで最初の管理者を作ってログインします。その後 `ADMIN_SETUP_CODE` を `''` に戻して下さい。管理者が居ればこのページは使えません。 ## 必要な PHP モジュール * php_fileinfo(OpenBSDでデフォルトで有効している) * php_gmp * php_mbstring(OpenBSDでデフォルトで有効している) * php_zlib(Git、普通はPHPに組み込まれている)