master
ブランチ master
LittleBeast / README.md
30.1 KB、379行 表示 生 Blame 履歴
1# Little Beast
2Simple, Pragmatic, Anti-bloat
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
7Demo installation: [https://lbdemo.technicalsuwako.moe/](https://lbdemo.technicalsuwako.moe/)
8
9## What is Little Beast?
10Little Beast is a PHP 8.5 or above framework made for 076.moe (game developer company) and technicalsuwako.moe (CEO's blog).\
11The core mentality is to extend what you need, and remove what you don't need.\
12Each core feature is split into libraries, so it is easy to choose what you need.\
13All modules are written from scratch, nothing is to be require a database.
14
15Little Beast is TechnicalSuwako's final web project before moving back to game development completely.
16
17## What Little Beast is not?
18* General purpose framework
19* Hard to use
20* Meant for web developers
21* For everyone/wider audience
22* Tedious to install
23* Dogmatic
24* Democratically run
25
26## What Little Beast has not?
27* Database (everything is a file)
28* Dependencies
29* Package manager
30* Docker/Kubernetes/Vagrant/Nix/etc containers
31* Javascript
32* Bloatware
33* ORM
34* Queues or background job
35* Cloud/Serverless integration
36* Scaffolding
37* Code generation
38* Abstraction layers
39* Unnecessary files
40* DEI, Code of Conduct, or other discriminatory practises
41
42## Unique features
43* No database
44* No Composer or PEAR
45* No server overhead
46* No container
47* Maron template engine
48* Custom Markdown
49* ActivityPub
50* Multi-language support
51* Multi-blog support
52* 100% correct and beautiful HTML5 and CSS3
53* 100% correct PHP
54* SEO friendly
55* Modular CSS
56* Atom feeds
57* Built-in test suite
58
59## How to install
60```sh
61cd /var/www/htdocs
62git clone https://gitler.moe/suwako/LittleBeast.git .
63mv config/config.sample.php config/config.php
64```
65
66Configure HTTP server to run `/public` as root via php-fpm.\
67That's all!
68
69### OpenBSD server
70```sh
71pkg_add php-8.5.5 php-gmp-8.5.5
72rcctl enable php85_fpm httpd relayd
73rcctl start php85_fpm httpd relayd
74```
75
76#### httpd
77```
78server "technicalsuwako.moe" {
79 listen on * tls port 8443
80 gzip-static
81 tls {
82 certificate "/etc/ssl/technicalsuwako.moe.crt"
83 key "/etc/ssl/private/technicalsuwako.moe.key"
84 }
85 root "/htdocs/technicalsuwako.moe/www/public"
86 directory index "index.php"
87 location "/.well-known/acme-challenge/*" {
88 root "/acme"
89 request strip 2
90 }
91
92 location "/*.php" {
93 fastcgi socket "/run/php-fpm.sock"
94 }
95
96 location "/*.php[/?]*" {
97 fastcgi socket "/run/php-fpm.sock"
98 }
99
100 location "/" {
101 directory index "index.php"
102 }
103 location match "/blog/" {
104 request rewrite "/index.php"
105 }
106 location match "/about" {
107 request rewrite "/index.php"
108 }
109 location match "/monero" {
110 request rewrite "/index.php"
111 }
112 location match "/secret" {
113 request rewrite "/index.php"
114 }
115 location match "/ap/" {
116 request rewrite "/index.php"
117 }
118 location "/.well-known/webfinger" {
119 request rewrite "/index.php"
120 }
121 location "*.atom" {
122 request rewrite "/index.php"
123 }
124 location match "/comments" {
125 request rewrite "/index.php"
126 }
127 location "/notifications" {
128 request rewrite "/index.php"
129 }
130}
131
132server "www.technicalsuwako.moe" {
133 listen on * tls port 8443
134 gzip-static
135 tls {
136 certificate "/etc/ssl/technicalsuwako.moe.crt"
137 key "/etc/ssl/private/technicalsuwako.moe.key"
138 }
139 block return 301 "https://technicalsuwako.moe$REQUEST_URI"
140}
141```
142
143#### relayd
144```
145relayd_addr="0.0.0.0"
146router_addr="192.168.10.106"
147
148table <httpd> { $router_addr }
149
150http protocol reverse {
151 tcp { nodelay, sack, socket buffer 65536, backlog 100 }
152 tls ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256"
153 tls keypair "technicalsuwako.moe"
154
155 return error
156
157 match request header append "X-Forwarded-For" value "$REMOTE_ADDR"
158 match request header append "X-Forwarded-Port" value "$REMOTE_PORT"
159
160 #match response header set "Referrer-Policy" value "same-origin"
161 match response header set "X-Frame-Options" value "deny"
162 match response header set "X-Content-Type-Options" value "nosniff"
163 match response header set "Referrer-Policy" value "strict-origin-when-cross-origin"
164 match response header set "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload"
165 match response header set "Cross-Origin-Opener-Policy" value "same-origin"
166 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';"
167 match response header append "Permissions-Policy" value "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=()"
168
169 match request header "Accept-Encoding" value "gzip" tag "gzip"
170 match response header "Content-Type" value "text/*" tag "compress"
171 match response header "Content-Type" value "text/html" tag "charset=UTF-8"
172 match response header "Content-Type" value "application/javascript" tag "compress"
173 match response header "Content-Type" value "application/json" tag "compress"
174
175 pass request quick header "Host" value "technicalsuwako.moe" forward to <httpd>
176
177 pass
178}
179
180relay www_tls {
181 listen on $relayd_addr port 443 tls
182 protocol reverse
183
184 # Default
185 forward to <httpd> port 8443 check tcp
186}
187
188relay www_www {
189 listen on $relayd_addr port 80
190 protocol reverse
191
192 # Default
193 forward to <httpd> port 8080 check tcp
194}
195```
196
197### Blogs and Atom feeds
198A site can have more than one blog: every directory in `BLOGS` in `config/config.php` (for example `blog`, `enblog`) is a blog with its own Atom feed at `/directory.atom`, linked from the top of its index and search result pages.\
199Without `BLOGS`, only `blog` exists, as before.
200
201### Static files (ASS_DOMAIN)
202`ASS_DOMAIN` in `config/config.php` sets where static files (images, CSS, avatars, uploads) are served from. Without it, it is `/static`.
203* A path (for example `/static`) is a directory under `public/` of this site: `/static` is `public/static/`.
204* A URL (for example `https://ass.example.com/static`) points to another server, such as a subdomain. Uploaded files are still saved under `public/` with the same path (`public/static/`), so sync that directory to the asset server (or serve it from there).
205
206In templates and code, use `ass_url('article/a.png')` for the URL and `ROOT.ass_path('article/a.png')` for the file on disk.
207
208### Database (MySQL/MariaDB, PostgreSQL, SQLite)
209Little Beast itself needs no database, but `Std\Lib\Db` can talk to one when a site has to (for example, when porting an existing application). The drivers are written in pure PHP, so mysqli, pdo_mysql, pgsql, pdo_pgsql, sqlite3, pdo_sqlite and sockets are not needed.\
210Set `DB_ENABLED` to `true` and fill in `DBINFO` in `config/config.php` (`driver` is `mysql`, which also works with MariaDB, `pgsql` for PostgreSQL, whose port is usually 5432, or `sqlite` with `path` set to the database file, relative to the site root, for example `data/site.sqlite`; `socket` uses a UNIX socket instead of `host` and `port`, for PostgreSQL either the socket file or the directory that has `.s.PGSQL.5432`, such as `/tmp`; `tls` is `true` or an array of SSL context options such as `['cafile' => '/etc/ssl/ca.pem']`).
211```php
212$db = new \Std\Lib\Db();
213$rows = $db->query('SELECT * FROM users WHERE id = ?', [1])['rows'];
214$id = $db->query('INSERT INTO users (name) VALUES (?)', ['諏訪子'])['insertId']; // MySQL, SQLite
215$id = $db->query('INSERT INTO users (name) VALUES (?) RETURNING id', ['諏訪子'])['rows'][0]['id']; // PostgreSQL
216$db->transaction(function(\Std\Lib\Db $db): void {
217 $db->query('UPDATE users SET points = points - ? WHERE id = ?', [100, 1]);
218 $db->query('UPDATE users SET points = points + ? WHERE id = ?', [100, 2]);
219});
220```
221* The connection is opened on the first query. Queries with parameters use prepared statements, so values never need escaping.
222* Integer columns come back as `int`, `FLOAT`/`DOUBLE` (PostgreSQL: `real`/`double precision`) as `float`, `NULL` as `null`, and everything else (including `DECIMAL`/`numeric`, dates, JSON and `BLOB`) as strings. PostgreSQL's `boolean` comes back as `bool`, `bytea` as the raw bytes and arrays as PHP arrays.
223* Errors throw `Std\Lib\Db\DbException` (`getCode()` is MySQL's error code, for example 1062 for a duplicate key, and `sqlState` the SQLSTATE, for example 23505 for a duplicate key in PostgreSQL) and are written to `log/db_log.txt`.
224* MySQL authentication: `mysql_native_password`, `caching_sha2_password` (the MySQL 8 default) and `sha256_password` (over plain TCP the password is encrypted with the server's RSA key), and `mysql_clear_password` (TLS or UNIX socket only). MariaDB's `client_ed25519`, compression and `LOAD DATA LOCAL INFILE` are not supported.
225* SQLite: `Std\Lib\Db\Sqlite` reads and writes SQLite 3 files itself (rollback journal, UTF-8), so they can also be opened with the `sqlite3` tool, and a write that was interrupted is rolled back the next time the file is opened, by either. It supports the common SQL: tables, indexes and views, `ALTER TABLE` (rename, add, drop and rename columns), `INSERT` with `OR REPLACE`/`OR IGNORE`, `ON CONFLICT` and `RETURNING`, `UPDATE … FROM`, joins, `GROUP BY` and aggregates, subqueries, `UNION`/`INTERSECT`/`EXCEPT`, `WITH`, transactions and savepoints, the usual functions (text, math, date and JSON), and `PRAGMA` such as `table_info`, `index_list`, `user_version` and `integrity_check`. Indexes are used for `=`, `IN` and ranges. WAL mode, triggers, `WITHOUT ROWID`, virtual tables (FTS), window functions, recursive `WITH`, foreign key enforcement, partial and expression indexes and `ATTACH` are not supported. Locking uses `flock()`, so only let PHP processes write to the file while the site is running. `?`, `?1` and `:name` placeholders work; strings that are not valid UTF-8 are stored as BLOBs (or pass a `Std\Lib\Db\Sqlite\Blob`).
226* PostgreSQL authentication: SCRAM-SHA-256 (over TLS with channel binding, SCRAM-SHA-256-PLUS; the option `channel_binding` is `prefer`, `require` or `disable`), MD5, `password` (TLS or UNIX socket only, unless `allow_cleartext` is `true`) and `trust`. GSSAPI and SSPI are not supported.
227* PostgreSQL: `?` placeholders become `$1`, `$2`… (outside strings, identifiers and comments; write `??` for a literal `?`, such as the jsonb operator), and `$1` can be used directly too. There is no insert ID, so use `RETURNING`. A `COMMIT` after an error in the transaction throws `DbException` (SQLSTATE 25P02), because PostgreSQL rolls the transaction back. `COPY`, `LISTEN` notifications and cancelling a running query are not supported. Tested with PostgreSQL 16.
228* The tests use a mock server. To run the PostgreSQL tests against a real server as well, set `LB_TEST_PGSQL=pgsql://user:password@host:port/database` (add `?cafile=/path/ca.pem` for TLS); they create and drop tables whose names start with `lb_test_`.
229
230### Converting databases
231`Std\Lib\Db\Convert` copies tables and rows between MySQL/MariaDB, PostgreSQL, SQLite, SQL dump files and Little Beast's own JSON data in `data/`, in any direction. It is pure PHP, like the drivers.
232```php
233use Std\Lib\Db;
234use Std\Lib\Db\Convert;
235use Std\Lib\Db\Convert\{DbSource, DbTarget, DumpSource, DumpTarget, JsonSource, JsonTarget};
236
237// data/ (JSON) → SQLite
238$report = Convert::run(new JsonSource(ROOT.'/data'), new DbTarget(new Db(['driver' => 'sqlite', 'path' => 'data/site.sqlite'])));
239// PostgreSQL → MySQL dump file
240Convert::run(new DbSource(new Db(['driver' => 'pgsql', 'host' => '127.0.0.1', 'port' => 5432, 'username' => 'lb', 'password' => '…', 'dbname' => 'lb'])),
241 new DumpTarget('/tmp/lb.sql', 'mysql'));
242// mysqldump or pg_dump file → PostgreSQL, replacing tables that already exist
243Convert::run(new DumpSource('/tmp/dump.sql'), new DbTarget(new Db()), ['drop' => true]);
244```
245* Sources: `DbSource` (a connected `Db`), `DumpSource` (a `.sql` file, or `.sql.gz` with PHP's zlib, from `mysqldump`, `pg_dump` (plain format, `COPY` or `--inserts`), `sqlite3 .dump` or `DumpTarget`; the dialect is detected) and `JsonSource` (a data directory). Targets: `DbTarget`, `DumpTarget` (dialect `mysql`, `pgsql` or `sqlite`; the file can be loaded with `mysql`, `psql` or `sqlite3`) and `JsonTarget`.
246* Options: `tables` (only these tables), `drop` (drop tables with the same name in the target; without it, a table that already exists is an error) and `progress` (`fn(string $table, int $rows)`). The result has `tables` (rows copied per table) and `warnings` (things that were not converted).
247* Columns, primary keys, unique and plain indexes, `NOT NULL`, defaults and auto increment (`AUTO_INCREMENT`, `SERIAL`/identity, SQLite's `INTEGER PRIMARY KEY`) are converted, and the next number continues after the copied rows. Types are mapped to the nearest type of the target (for example `TINYINT(1)` and `boolean`, `JSON` and `jsonb`, `BLOB` and `bytea`); `ENUM`/`SET` become text and PostgreSQL arrays become JSON. Times with a time zone are stored in UTC. Foreign keys, views, triggers, functions, full-text and expression indexes are not converted (they are listed in `warnings`).
248* JSON data: users, comments, notifications, Cron jobs, followers and ActivityPub notes become tables whose names start with `lb_`, and every other file (except `cache/`, `ratelimit/`, `tmp/`, lock and temporary files and SQLite databases) goes into `lb_files`, so converting back gives the same files. Other tables become `data/db/{table}.json`. An empty or missing `data/` directory simply has no tables. `JsonTarget` refuses to write into a directory that is not empty unless its second argument (overwrite) is `true` or `drop` is set.
249* When the source or target is the database the site is using, stop writes to it first (or convert into a new database), as the conversion does not lock the source.
250
251### Git
252`Std\Lib\Git` reads and writes Git repositories in pure PHP (it needs PHP's zlib, but no `git` command), for both SHA-1 and SHA-256 repositories. It is the base for hosting repositories on the site.
253```php
254use Std\Lib\Git\{Repository, Tree, Commit, Person, Diff, Blame};
255
256$repo = Repository::init('/var/www/git/suwako/test.git', 'sha1', 'main', 'group'); // or Repository::open(...)
257$me = Person::now('諏訪子', 'suwako@example.com');
258$tree = $repo->updateTree(null, ['README.md' => [Tree::FILE, "# Test\n"]]);
259$id = $repo->writeCommit(Commit::create($tree, [], $me, $me, "First commit\n"));
260$repo->refs->update('refs/heads/main', $id, $repo->zeroId()); // fails if the branch changed in the meantime
261
262foreach ($repo->log('main', ['path' => 'README.md', 'limit' => 20]) as $c) echo $c->id, ' ', $c->subject(), "\n";
263echo $repo->blob($repo->resolve('main~1:README.md'));
264echo Diff::unified($repo, 'main~1', 'main');
265$lines = Blame::file($repo, 'main', 'README.md');
266```
267* Storage: loose objects, packfiles (`.idx` v1/v2 and `.rev`; deltas are resolved), refs and `packed-refs`, `objects/info/alternates`, `core.sharedRepository`. Revisions such as `main~2`, `v1.0^{}`, `HEAD^2` and `main:path/file` are understood. `Diff` gives the same output as `git diff` (Myers), `Blame` follows the first parent that has the same line, and `Maintenance::gc()` packs everything reachable into one pack (run it from Cron).
268* Network: `Http::handle()` serves Smart HTTP (`git clone https://…`, `git fetch`, `git push`) and `Ssh::handle()` serves `git clone git@host:…` and `git push` over SSH, with protocol v0, v1 and v2, shallow clones (`--depth`, `--deepen`), `include-tag`, side-band progress, atomic pushes and checks of received objects. Packs that are sent reuse the deltas already on disk. Access checks are done by the caller; `ReceivePack` takes `check`, `update` and `after` callbacks (for example for protected branches).
269```php
270// GET …/repo.git/info/refs, POST …/repo.git/git-upload-pack and git-receive-pack
271Http::handle(Repository::open($dir), $action, ['push' => $userMayPush]);
272
273// SSH: authorized_keys of the git user
274// 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…
275exit(Ssh::handle((string)getenv('SSH_ORIGINAL_COMMAND'), fn(string $path, bool $write) => /* check access */ Repository::open('/var/www/git/'.$path)));
276```
277* On OpenBSD, php-fpm runs chrooted in `/var/www`, so keep the repositories inside it (for example `/var/www/git` as the home of the `git` user) and create them with `$shared = 'group'`, with `www` in the `git` group, so that both the website and SSH can write. For protocol v2 over SSH, add `AcceptEnv GIT_PROTOCOL` to `sshd_config`.
278* Not supported: creating new deltas (objects pushed as loose files are packed without them), thin packs, partial clone (`--filter`), `--shallow-since`/`--shallow-exclude`, `git archive --remote`, LFS, and writing commit-graph or multi-pack-index files.
279
280### Gitler (Git hosting)
281Gitler (`/gitler`) hosts Git repositories on the site with the Git library above: repository pages (files, README, history, diffs, blame, branches, tags, ZIP/TAR.GZ downloads), private repositories, collaborators (read or read/write), and clone/push over HTTPS and SSH. It needs no JavaScript.
282* Set `GITLER_ENABLED` to `true` (it needs `AUTH_ENABLED`) and `GITINFO` in `config/config.php`: `root` is the directory of the repositories (relative to the site root, so that the same setting works inside and outside the php-fpm chroot; for `/var/www/git`, `../git` when the site is in `/var/www/htdocs`, or `../../../git` when it is in `/var/www/htdocs/example.com/www`), `ssh` the SSH address shown for cloning (for example `git@example.com`, empty for HTTPS only), `sshPort`, `php` the PHP command outside the chroot, `objectFormat` and `branch` (`master` by default) for new repositories and `maxRepos` per user.
283* Roles: 「Gitのリポジトリ」 (create and manage your own repositories; nobody has it until an admin gives it), 「Gitのイシュー」 (everybody by default) and 「Gitのプルリクエスト」 (nobody by default). The issue tracker and pull requests come later. Admins can see and change every repository.
284* HTTPS: `git clone https://example.com/gitler/user/repo.git`. Public repositories can be cloned without signing in; private ones and pushes ask for the user name and an access token made at `/gitler/-/keys` (read-only or read/write). Users without two-factor authentication can also use their password. Pushes are limited to branches and tags. OpenBSD's httpd accepts request bodies up to 1 MB by default, so raise it for pushes in the `server` block, for example `connection max request body 536870912`.
285* SSH: users add their public keys at `/gitler/-/keys`. Create the `git` user with its home inside the chroot, put `www` in its group, and let sshd ask Gitler for the keys:
286```sh
287useradd -m -d /var/www/git -s /bin/sh git
288usermod -G git www # php-fpm can write the repositories
289usermod -G www git # gitler.php can read config/config.php and data/
290chmod 2775 /var/www/git
291```
292```
293# /etc/ssh/sshd_config
294Match User git
295 AuthorizedKeysCommand /usr/local/bin/php-8.5 /var/www/htdocs/gitler.php keys %u %t %k
296 AuthorizedKeysCommandUser git
297 AcceptEnv GIT_PROTOCOL
298 PasswordAuthentication no
299```
300 Each key runs `gitler.php serve <user>`, so the user only gets `git clone`/`git push` (`git@example.com:user/repo.git`). Without `AuthorizedKeysCommand`, `php gitler.php authorized-keys > /var/www/git/.ssh/authorized_keys` (for example from cron) writes the same lines.
301* Add the Cron job `gitlergc` (for example `php cron.php add "@daily" gitlergc`) to pack the repositories, or run `php gitler.php gc`.
302* The repository page shows the share of each language like GitHub (by bytes; file names, extensions and `#!`). Only programming and markup languages count: data (JSON, YAML…), prose (Markdown…), vendored code (`vendor/`, `node_modules/`…), documentation (`docs/`, `examples/`…) and generated files are left out. Change it with `linguist-language`, `linguist-vendored`, `linguist-generated`, `linguist-documentation` and `linguist-detectable` in the root `.gitattributes` (for example `*.h linguist-language=C++`). Results are stored per tree in `gitler-languages.json` inside the repository, and lists show the main language of the default branch.
303* Issues: labels, assignees, milestones, Markdown bodies and comments (`#3` links to another issue). Users who can read the repository and have the "Git issues" role open issues and comment; people with write access label and close them (authors can close their own). Commit messages pushed to the default branch close issues with `fixes #3`, `closes #3` or `resolves #3`, and any `#3` is recorded on the issue. Issues can be turned off per repository in its settings. The data lives in `gitler/` inside the repository.
304* Fediverse (when multi-user ActivityPub is on): public repositories become `@owner.name@host` accounts. Follow one from Mastodon and the like to receive issues and comments; replies become comments, and posts mentioning the repository open new issues. ForgeFed `Offer{Ticket}` and `Resolve` are supported too (the issue's Ticket is at `…/issues/3/ticket`). Repository settings can turn it off and block accounts or domains (site-wide domain blocks apply as well). Delivery uses the `apdeliver` cron job; issues closed by SSH pushes (the `git` user) are queued in `gitler/spool/` and sent by the `notify` cron job. Generating the keys needs GMP.
305* Notifications: owners and collaborators watch their repositories from the start (new issues and all activity); others get activity on issues they opened, commented on, are assigned to or are mentioned in (`@user`). The repository page's "通知" menu switches between watching, participating only and ignoring, and each issue page can subscribe or unsubscribe. Notifications appear on the site's notifications page, and by email too when `MAILER_ENABLED` is on (users can turn email off at `/gitler/-/keys`). Emails and notifications from SSH pushes are sent by the `notify` cron job, so add it: `php cron.php add "* * * * *" notify`.
306* Archiving: archiving a repository in its settings makes it read-only (no pushes, no changes to issues, comments, labels or milestones, no comments from the Fediverse). It can be undone at any time.
307* Pull requests (need the "Gitのプルリクエスト" role): open them from a branch in the same repository, from a branch in a fork ("フォークする" on the repository page, needs the "Gitのリポジトリ" role), or without forking via `git push origin HEAD:refs/for/master/topic` (AGit, also for repositories you can only read; pushing to the same topic again updates it). Pushing a new branch prints a URL for opening a pull request. Pull requests have a conversation, commits, and changed files (click a line number to comment on that line; collect draft comments into a review that comments, approves or requests changes). People with write access merge with a merge commit, squash, or rebase (fast-forward when possible); conflicts are checked with a 3-way merge in PHP and must be resolved locally. `fixes #3` in the description closes the issue on merge, and pull requests whose changes reach the target branch by a push are marked merged. They share numbering with issues, and the changes live in `refs/pull/N/head` (`git fetch origin refs/pull/3/head:pr-3`). They can be turned off per repository in its settings.
308
309### TLS 1.3 and 1.2 (no php_openssl)
310HTTPS requests (`Curl`, ActivityPub), SMTP STARTTLS (`Mailer`) and MySQL and PostgreSQL over TLS use `Std\Lib\Tls`, a TLS 1.3 and 1.2 client written in pure PHP (it needs `php_gmp`), so `php_openssl` is not needed. RSA keys and signatures (ActivityPub) use `Std\Lib\Crypto\Rsa`, and existing PEM keys keep working.
311* TLS 1.3 is preferred. Servers that only speak TLS 1.2 get TLS 1.2 with secure cipher suites only (ECDHE with AES-GCM or ChaCha20-Poly1305, Extended Master Secret when the server supports it, no renegotiation); CBC, RSA key exchange and DHE are not supported, and neither is TLS 1.1 or older. A TLS 1.3 server that is pushed down to TLS 1.2 by an attacker is detected. Use the option `'min_version' => '1.3'` to refuse TLS 1.2.
312* Certificates are checked against `TLS_CAFILE` in `config/config.php` (a PEM file of trusted CAs). When it is empty, the usual system locations are tried (`/etc/ssl/cert.pem` on OpenBSD). Inside the httpd/php-fpm chroot that path is `/var/www/etc/ssl/cert.pem`, so copy the system file there. Windows has no such file, so set `TLS_CAFILE` (for example to curl's `cacert.pem`).
313* Cipher suites: 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}`. Key exchange: x25519, secp256r1, secp384r1. Signatures: RSA-PSS, ECDSA (P-256, P-384, P-521) and RSA PKCS#1 v1.5 (certificates and TLS 1.2 only). Session resumption, 0-RTT, client certificates and revocation checks are not supported.
314* Encryption runs in PHP, so large downloads are slower than with openssl (a few MB/s).
315```php
316$s = \Std\Lib\Tls::connect('example.com', 443);
317fwrite($s, "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n");
318echo stream_get_contents($s);
319```
320
321### Comments
322Set `COMMENTS_ENABLED` to `true` in `config/config.php` to show a comment section under every blog post (posting needs `AUTH_ENABLED`).
323* Logged-in users can comment, reply and like (Markdown: bold, italics, strikethrough, code, quotes, lists and links). Users can delete their own comments, and admins and staff can delete any comment. No JavaScript is needed.
324* Blogs with `'federate' => true` in `BLOGS` are posted by the `FEDIINFO` account (`apfederate`), and public or unlisted Fediverse replies to those posts (and to replies) become comments. Edits and deletions on the Fediverse are followed.
325* Every new comment notifies all admins, and users are notified when their comments get replies or likes, at `/notifications`.
326* Comments are stored in `data/comments/` and notifications in `data/notifications/`.
327
328### Cron
329Scheduled tasks (for example `apfederate`, which sends new blog posts to ActivityPub followers) are run by the system cron.\
330Set `CRON_ENABLED` to `true` in `config/config.php` (and `FEDIINFO['domain']` when using ActivityPub),
331then add this line to the crontab of the user php-fpm runs as (`crontab -u www -e`):
332```
333* * * * * /usr/local/bin/php-8.5 /var/www/htdocs/cron.php
334```
335
336Jobs are managed from the command line (or the admin panel):
337```sh
338php cron.php add "*/10 * * * *" apfederate
339php cron.php list
340php cron.php help
341```
342
343Tasks are registered in `register_cron_tasks()` in `util.php` (used by `cron.php` and the admin panel). Built-in tasks:
344* `apfederate`: sends new blog posts to ActivityPub followers
345* `apupdate`: sends profile (actor) updates to ActivityPub followers
346* `authpurge`: deletes accounts whose email address was not verified within 7 days (when `AUTH_VERIFY_EMAIL` is enabled)
347* `apdeliver`: delivers microblog activities to other servers (multi-user mode, run every minute)
348* `apcleanup`: deletes remote posts and caches older than 90 days (multi-user mode, run daily)
349* `notify`: sends notification emails and processes the notifications and deliveries queued in Gitler repositories by SSH pushes (when `AUTH_ENABLED` is enabled, run every minute)
350* `gitlergc`: packs the Gitler repositories (when `GITLER_ENABLED` is enabled, run daily)
351
352### Microblog (multi-user Fediverse)
353By default ActivityPub is single-user: the blog actor (`/ap/actor`) sends new blog posts to its followers.\
354Setting `FEDIINFO['isSingleUser']` to `false` (with `AUTH_ENABLED` and `FEDIINFO['domain']`) turns on multi-user mode:
355* The blog actor keeps working as before.
356* Every user with the Fediverse role becomes their own actor (`@username@domain`). The role is given to everyone except banned users; an admin can take it away on the user's profile edit page.
357* `/microblog` has Home, Local and Federated timelines, posting (content warnings, visibility, up to 4 images), replies, likes, boosts, follows (with optional follow approval), blocks, reports and search (`@user@host`, post URLs, `#tags` and text). No JavaScript is needed.
358* Admins can block whole servers and read reports at `/microblog/admin`.
359
360Deliveries are queued, so add the cron jobs:
361```sh
362php cron.php add "* * * * *" apdeliver
363php cron.php add "@daily" apcleanup
364```
365Uploaded images (PNG/JPEG/GIF, up to 4 MiB; check PHP's `upload_max_filesize`) have their metadata (Exif, text chunks) removed.
366
367### Admin panel
368Set `ADMIN_ENABLED` to `true` in `config/config.php` (it needs `AUTH_ENABLED`). Users with the admin role then see 「管理」 in the menu, which opens `/admin`:
369* Users: list and search accounts, create accounts (even when registration is off, without email verification), change roles (removing every role bans the user), mark an email address as verified, sign a user out everywhere, and change the email address or password or turn off two-factor authentication (these three need your own password). Display name, icon and the rest stay on the user's profile edit page.
370* Logs: the files in `log/`, with the last lines, search, download, clear and delete.
371* Cron: the jobs, with add, edit, enable or disable, delete and run now, and when the system cron last ran.
372* Cache: `data/cache/` by kind (view, or delete one entry, a kind or everything) and the Fediverse caches of remote actors, WebFinger and images.
373* Database conversion: the converter above as a form. Sources and targets are JSON data (`data/`), a SQLite file, a MySQL/MariaDB or PostgreSQL server, an SQL dump (uploaded or a file on the server; as a target, downloaded or saved on the server) and the site's own database (`DBINFO`). Paths are relative to the site root and must stay inside it. Uploaded and converted files are kept in `data/tmp/convert/` for an hour. Large uploads need a higher `upload_max_filesize` and `post_max_size` in `php.ini`.
374
375A new site has no admin yet, and `data/` may be empty. Set `ADMIN_SETUP_CODE` to a long random string (for example the output of `openssl rand -hex 16`) and open `/admin`: it goes to `/admin/setup`, where the code creates the first admin account and signs it in. Then set `ADMIN_SETUP_CODE` back to `''`. The setup page closes as soon as an admin exists.
376
377## Required PHP module
378* php_gmp
379* php_zlib (for Git; usually built into PHP)