master
ブランチ master
LittleBeast / util.php
24.3 KB、666行 生 Blame 履歴
1<?php
2/*************************************************************
3# 076 License
4
5Copyright (c) テクニカル諏訪子
6
7Permission is hereby granted to any person obtaining a copy of the software
8Little Beast (the "Software") to use, modify, merge, copy, publish, distribute,
9sublicense, 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
31THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
32INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
33PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
34HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
35CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
36OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 */
38if (!CURL_ENABLED) define('ACTIVITYPUB_ENABLED', false);
39foreach (['MAILER_ENABLED', 'OPENPROVIDER_ENABLED', 'CRON_ENABLED', 'AUTH_VERIFY_EMAIL'] as $c) if (!defined($c)) define($c, false);
40define('ACTIVITYPUB_MULTIUSER', ACTIVITYPUB_ENABLED && AUTH_ENABLED && defined('FEDIINFO') && !(FEDIINFO['isSingleUser'] ?? true));
41
42enum LogType {
43 case ActivityPub;
44 case Auth;
45 case Mailer;
46 case Image;
47 case Cron;
48 case Db;
49}
50
51class Result {
52 public bool $isSuccess;
53 public ?string $message;
54 public array $data;
55
56 public function __construct(bool $isSuccess, ?string $message = null, array $data = []) {
57 $this->isSuccess = $isSuccess;
58 $this->message = $message;
59 $this->data = $data;
60 }
61
62 public static function Success(?string $message = null, array $data = []): self {
63 return new self(true, $message, $data);
64 }
65
66 public static function Error(string $message, array $data = []): self {
67 return new self(false, $message, $data);
68 }
69}
70
71class Roles {
72 // 例: if ($user->role & (Roles::STAFF | Roles::NINTENDONDA))
73 public const int BANNED = 0;
74 public const int MEMBER = 1 << 0; // 1
75 public const int NINTENDONDA = 1 << 1; // 2
76 public const int PLAYSTATIONNDA = 1 << 2; // 4
77 public const int FULLNDA = 1 << 3; // 8 (NINTENDONDA + PLAYSTATIONNDA)
78 public const int SUBSCRIBER = 1 << 4; // 16
79 public const int NSUBSCRIBER = 1 << 5; // 32 (NINTENDNDA + SUBSCRIBER)
80 public const int PSUBSCRIBER = 1 << 6; // 64 (PLAYSTATIONNDA + SUBSCRIBER)
81 public const int FSUBSCRIBER = 1 << 7; // 128 (NINTENDNDA + PLAYSTATIONNDA + SUBSCRIBER)
82 public const int STAFF = 1 << 8; // 256
83 public const int ADMIN = 1 << 9; // 512
84 public const int FEDIVERSE = 1 << 10; // 1024(マルチユーザーのFediverseを使える)
85 public const int GIT_REPOS = 1 << 11; // 2048(Gitlerで自分のリポジトリを作る・管理する、既定は無し)
86 public const int GIT_ISSUES = 1 << 12; // 4096(Gitlerのイシュー、既定は全員)
87 public const int GIT_PULLS = 1 << 13; // 8192(Gitlerのプルリクエスト、既定は無し)
88
89 // 支援者(以前の「role >= SUBSCRIBER」と同じ範囲。ビットを足しても壊れない様に&で確認する)
90 public const int PAID = self::SUBSCRIBER | self::NSUBSCRIBER | self::PSUBSCRIBER | self::FSUBSCRIBER
91 | self::STAFF | self::ADMIN;
92
93 // プロフィールの編集画面で管理者が付け外し出来るロール
94 public const array LABELS = [
95 self::MEMBER => 'メンバー',
96 self::NINTENDONDA => '任天堂NDA',
97 self::PLAYSTATIONNDA => 'PlayStation NDA',
98 self::FULLNDA => '全NDA',
99 self::SUBSCRIBER => '支援者',
100 self::NSUBSCRIBER => '支援者(任天堂NDA)',
101 self::PSUBSCRIBER => '支援者(PlayStation NDA)',
102 self::FSUBSCRIBER => '支援者(全NDA)',
103 self::STAFF => 'スタッフ',
104 self::ADMIN => '管理者',
105 self::FEDIVERSE => 'Fediverse',
106 self::GIT_REPOS => 'Gitのリポジトリ',
107 self::GIT_ISSUES => 'Gitのイシュー',
108 self::GIT_PULLS => 'Gitのプルリクエスト',
109 ];
110}
111
112class Gender {
113 public const int UNKNOWN = -1; // 不明
114 public const int MALE = 0; // 男性
115 public const int FEMALE = 1; // 女性
116}
117
118$colorPalette = [
119 'ultradark' => [
120 'black' => '#020102',
121 'white' => '#b3b1b3',
122 'grey' => '#5c535c',
123 'yellow' => '#8d8b0d',
124 'orange' => '#724e0b',
125 'green' => '#1e6907',
126 'purple' => '#410a5a',
127 'lime' => '#198d5b',
128 'pink' => '#9e0ea3',
129 'cyan' => '#1e8c9b',
130 'red' => '#861623',
131 'blue' => '#164a85',
132 ],
133 'dark' => [
134 'black' => '#120f12',
135 'white' => '#cfcbcf',
136 'grey' => '#746c75',
137 'yellow' => '#b8b515',
138 'orange' => '#ac7718',
139 'green' => '#2c980c',
140 'purple' => '#550f75',
141 'lime' => '#10c074',
142 'pink' => '#c016c6',
143 'cyan' => '#1cbcd0',
144 'red' => '#bc1729',
145 'blue' => '#1a6ecf',
146 ],
147 'medium' => [
148 'black' => '#232023',
149 'white' => '#f6f6f6',
150 'grey' => '#988f98',
151 'yellow' => '#f1ed25',
152 'orange' => '#f7a717',
153 'green' => '#2de12c',
154 'purple' => '#b421f8',
155 'lime' => '#20f398',
156 'pink' => '#f545f5',
157 'cyan' => '#29d3ff',
158 'red' => '#ee4030',
159 'blue' => '#2687f7',
160 ],
161 'light' => [
162 'black' => '#443b44',
163 'white' => '#fcfcfc',
164 'grey' => '#bdb4bd',
165 'yellow' => '#ecea71',
166 'orange' => '#f8c56a',
167 'green' => '#6cf344',
168 'purple' => '#ae6bdb',
169 'lime' => '#88ecc1',
170 'pink' => '#ea79d8',
171 'cyan' => '#8ae5ff',
172 'red' => '#f35869',
173 'blue' => '#6aa6eb',
174 ],
175 'ultralight' => [
176 'black' => '#574d57',
177 'white' => '#ffffff',
178 'grey' => '#d6cfd6',
179 'yellow' => '#f5f4cf',
180 'orange' => '#f3d6a3',
181 'green' => '#baf3a8',
182 'purple' => '#d2bae2',
183 'lime' => '#b6e9d3',
184 'pink' => '#e6b9de',
185 'cyan' => '#c2e8f3',
186 'red' => '#ecb0b7',
187 'blue' => '#bbd4f0',
188 ],
189];
190
191function uuid(): string {
192 $data = random_bytes(16);
193 $data[6] = chr(ord($data[6]) & 0x0f | 0x40);
194 $data[8] = chr(ord($data[8]) & 0x3f | 0x80);
195 return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
196}
197
198function kys(mixed $arg): void {
199 if (!DEBUG_MODE) return;
200 if (gettype($arg) == 'boolean') $arg = $arg === true ? 'true' : 'false';
201 if (gettype($arg) == 'array' || gettype($arg) == 'object') {
202 foreach ($arg as $a) { if (gettype($a) == 'boolean') $a = $a === true ? 'true' : 'false'; }
203 }
204 echo '<style>html { color: #fcfcfc; background-color: #232023; } body { margin: 0; }</style>';
205 echo '<header style="padding: 10px 0; display: flex; justify-content: space-evenly; background-color: #550f75; margin-bottom: 20px; position: sticky; top: 0;"><div><b>K</b>ILL</div> <div><b>Y</b>OUR</div> <div><b>S</b>ELF</div></header>';
206 echo '<pre>';
207 print_r($arg);
208 echo '<pre>';
209 die();
210}
211
212function ffs(): void {
213 if (!DEBUG_MODE) return;
214 echo '<style>html { color: #fcfcfc; background-color: #232023; } body { margin: 0; }</style>';
215 echo '<header style="padding: 10px 0; display: flex; justify-content: space-evenly; background-color: #b61729; margin-bottom: 20px; position: sticky; top: 0;"><div><b>F</b>OR</div> <div><b>F</b>UCKS</div> <div><b>S</b>AKE</div></header>';
216
217 $stack = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 50);
218 $st = [];
219 unset($stack[0]);
220 $i = 0;
221
222 echo '<pre>';
223 print_r($stack);
224 echo '<pre>';
225 foreach ($stack as $s) {
226 if (isset($s['file'])) $st[$i]['file'] = $s['file'].(isset($s['line']) ? ':'.$s['line'] : '');
227 else $st[$i]['file'] = '';
228 if (isset($s['function'])) $st[$i]['func'] = (isset($s['class']) ? $s['class'].(isset($s['type']) ? $s['type'] : '::') : '').$s['function'];
229 else $st[$i]['func'] = '';
230 if (isset($s['object'])) $st[$i]['objs'] = $s['object'];
231 else $st[$i]['objs'] = new \stdClass;
232 if (isset($s['args'])) $st[$i]['args'] = $s['args'];
233 else $st[$i]['args'] = [];
234 $i++;
235 }
236 unset($stack[$i]);
237
238 foreach ($st as $s) {
239 echo '<div>';
240 echo '<b>ファイル:</b>';
241 echo $s['file'].'<br />';
242 echo '<b>関数:</b>';
243 echo $s['func'].'<br />';
244 echo '<b>オブジェクト:</b>';
245 echo print_r($s['objs']).'<br />';
246 echo '<b>その他:</b>';
247 echo print_r($s['args']).'<br />';
248 echo '</div>';
249 }
250 die();
251}
252
253function base58btc_encode(string $bin): string {
254 $a = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
255 $base = 58;
256 $num = \gmp_import($bin, 1, GMP_LSW_FIRST | GMP_NATIVE_ENDIAN);
257 $res = '';
258
259 if (\gmp_cmp($num, 0) == 0) return '1';
260
261 while (\gmp_cmp($num, 0) > 0) {
262 $mod = \gmp_intval(\gmp_mod($num, $base));
263 $res = $a[$mod].$res;
264 $num = \gmp_div_q($num, $base);
265 }
266
267 $bytes = str_split($bin);
268 foreach ($bytes as $byte) {
269 if (ord($byte) === 0) {
270 $res = '1'.$res;
271 } else {
272 break;
273 }
274 }
275
276 return $res;
277}
278
279/**
280 * Cronのタスクの登録(cron.phpと管理画面で使う)
281 * ジョブの「タスク名」と処理を結び付ける。
282 * 処理はジョブ(CronTab)を受け取り、Result・bool・nullを返す(falseかResult::Errorは失敗)。
283 *
284 * @param \Std\Lib\Cron $cron
285 * @return void
286 */
287function register_cron_tasks(\Std\Lib\Cron $cron): void {
288 if (ACTIVITYPUB_ENABLED) {
289 // 新しいブログ投稿をフォロワーに送る
290 $cron->register('apfederate', fn() => (new \Std\Lib\Activitypub((new \Site\Controller\BlogPost())->federatedPosts()))->federate());
291 // プロフィール(アクター)の更新をフォロワーに送る
292 $cron->register('apupdate', fn() => (new \Std\Lib\Activitypub())->sendActorUpdate());
293 }
294
295 if (ACTIVITYPUB_MULTIUSER) {
296 // マイクロブログの配信(毎分)
297 $cron->register('apdeliver', fn() => \Std\Lib\Fedi\Delivery::run());
298 // 90日より古いリモートの投稿とキャッシュを消す(1日1回)
299 $cron->register('apcleanup', fn() => \Std\Lib\Fedi\Timeline::cleanup());
300 }
301
302 if (AUTH_ENABLED && AUTH_VERIFY_EMAIL) {
303 // メールアドレスを7日以内に確認しなかったアカウントを削除する
304 $cron->register('authpurge', fn() => (new \Std\Lib\Auth())->purgeUnverified());
305 }
306
307 if (AUTH_ENABLED) {
308 // 通知のメールを送り、SSHのプッシュでGitlerのリポジトリに溜めた通知と配信を処理する(毎分)
309 $cron->register('notify', function(): \Result {
310 $n = \Std\Lib\Gitler\Gitler::enabled() ? \Std\Lib\Gitler\Gitler::flushDeferred() : 0;
311 $res = \Std\Lib\Notifications::sendMail();
312 return \Result::Success("Gitler:{$n}件、".$res->message, $res->data + ['gitler' => $n]);
313 });
314 }
315
316 if (\Std\Lib\Gitler\Gitler::enabled()) {
317 // Gitlerのリポジトリを整理する(プッシュで増えたファイルを1つのパックに、1日1回)
318 $cron->register('gitlergc', fn() => \Std\Lib\Gitler\Gitler::gcAll());
319 }
320}
321
322function logger(LogType $section, mixed $arg): void {
323 if (!LOGGING_ENABLED) return;
324 $success = false;
325 $logfile = ROOT.'/log/';
326
327 switch ($section) {
328 case LogType::ActivityPub:
329 $logfile .= 'ap_log.txt';
330 $success = true;
331 break;
332 case LogType::Auth:
333 $logfile .= 'auth_log.txt';
334 $success = true;
335 break;
336 case LogType::Mailer:
337 $logfile .= 'mail_log.txt';
338 $success = true;
339 break;
340 case LogType::Image:
341 $logfile .= 'img_log.txt';
342 $success = true;
343 break;
344 case LogType::Cron:
345 $logfile .= 'cron_log.txt';
346 $success = true;
347 break;
348 case LogType::Db:
349 $logfile .= 'db_log.txt';
350 $success = true;
351 break;
352 default:
353 break;
354 }
355
356 // log/が無ければ作る(新しいサイト等)
357 if ($success && !is_dir(ROOT.'/log')) @mkdir(ROOT.'/log', 0755, true);
358 if ($success) file_put_contents($logfile, $arg."\n", FILE_APPEND);
359}
360
361function to_money($amount, $lang) {
362 $amount = floatval($amount);
363
364 switch (strtolower($lang)) {
365 case 'ja':
366 if ($amount >= 100000000) {
367 $oku = $amount / 100000000;
368 return $oku.'億円';
369 } else if ($amount >= 10000) {
370 $man = $amount / 10000;
371 return $man.'万円';
372 }
373 return number_format($amount, 0).'円';
374 case 'en':
375 if ($amount >= 1000000000) {
376 $billion = $amount / 1000000000;
377 return '¥ '.$billion.' billion';
378 } else if ($amount >= 1000000) {
379 $million = $amount / 1000000;
380 return '¥ '.$million.' million';
381 } else if ($amount >= 1000) {
382 $thousand = $amount / 1000;
383 return '¥ '.$thousand.' thousand';
384 }
385 return '¥ '.number_format($amount, 0);
386 default:
387 return '¥ '.number_format($amount, 0);
388 }
389}
390
391function randstr(): string {
392 $len = random_int(1, 20);
393 return bin2hex(random_bytes($len));
394}
395
396function assert_exists(mixed $assertion, Throwable|string|null $description = null): bool {
397 if (!DEBUG_MODE) return false;
398 if (ini_get('zend.assertions') < 1) trigger_error('assert機能性が無効です。有効するには、php.iniで「zend.assertions = 1」に設定して下さい。', E_USER_WARNING);
399 if (isset($assertion)) return true;
400 assert(false, $description ?? 'Assertion failed: value is not null');
401 return false;
402}
403
404function assert_null(mixed $assertion, Throwable|string|null $description = null): bool {
405 if (!DEBUG_MODE) return false;
406 if (ini_get('zend.assertions') < 1) trigger_error('assert機能性が無効です。有効するには、php.iniで「zend.assertions = 1」に設定して下さい。', E_USER_WARNING);
407 if (is_null($assertion)) return true;
408 assert(false, $description ?? 'Assertion failed: value is not null');
409 return false;
410}
411
412function assert_not_null(mixed $assertion, Throwable|string|null $description = null): bool {
413 if (!DEBUG_MODE) return false;
414 if (ini_get('zend.assertions') < 1) trigger_error('assert機能性が無効です。有効するには、php.iniで「zend.assertions = 1」に設定して下さい。', E_USER_WARNING);
415 if (!is_null($assertion)) return true;
416 assert(false, $description ?? 'Assertion failed: value is null');
417 return false;
418}
419
420function assert_unless_success(Result $assertion, Throwable|string|null $description = null): bool {
421 if (!DEBUG_MODE) return false;
422 if (ini_get('zend.assertions') < 1) trigger_error('assert機能性が無効です。有効するには、php.iniで「zend.assertions = 1」に設定して下さい。', E_USER_WARNING);
423 if ($assertion->isSuccess) return true;
424 assert(false, $description ?? $assertion->message ?? 'Assertion failed: Result is not successful');
425 return false;
426}
427
428/**
429 * ブログの一覧(config.phpのBLOGS、無ければ「blog」だけ)
430 * federateを省略した場合は「blog」だけ連合する(今まで通り)
431 *
432 * @return array ディレクトリ名 => ['title' => フィードの名前, 'federate' => ActivityPubで配信するか]
433 */
434function blogs(): array {
435 $blogs = defined('BLOGS') ? BLOGS : ['blog' => []];
436 foreach ($blogs as $name => $b) $blogs[$name] = $b + ['title' => SITEINFO['title'], 'federate' => $name === 'blog'];
437 return $blogs;
438}
439
440function getcookie(string $name): string|null {
441 return $_COOKIE[$name] ?? null;
442}
443
444/**
445 * 表示の設定(テーマ等)。ログイン中ならプロフィールに保存した値、無ければクッキー
446 *
447 * @param string $name 設定の名前(クッキーと同じ名前)
448 * @return string|null
449 */
450function pref(string $name): string|null {
451 static $prefs = null;
452 if ($prefs === null) {
453 $user = AUTH_ENABLED ? (new \Std\Lib\Auth())->getLoggedInUser() : null;
454 $prefs = (array)($user->prefs ?? []);
455 }
456 return isset($prefs[$name]) ? (string)$prefs[$name] : getcookie($name);
457}
458
459function namecolor(\stdClass $userData): string {
460 $ban = "#888888";
461 $male = "#97ACEF";
462 $female = "#F185C9";
463 $ungender = "#7C60B0";
464
465 $gender = 'color: '.($userData->gender === Gender::MALE ? $male : ($userData->gender === Gender::FEMALE ? $female : $ungender)).';';
466 $style = $userData->namecolor ?: ($userData->role !== Roles::BANNED ? $gender : 'color: '.$ban.';');
467 $style = htmlspecialchars($style, ENT_QUOTES, 'UTF-8');
468
469 $showname = $userData->displayname ?: $userData->username;
470 $showname = htmlspecialchars($showname, ENT_QUOTES, 'UTF-8');
471
472 $color = "<span style=\"{$style}\">{$showname}</span>";
473 if ($userData->role & (Roles::ADMIN | Roles::STAFF))
474 $color .= "<span style=\"font-size: x-small; background: #10c074; border: 1px solid #fcfcfc; border-radius: 10px; padding: 0 0.5em;\">✓</span>";
475 $suffix = $userData->gender === Gender::MALE ? 'くん' : ($userData->gender === Gender::FEMALE ? 'ちゃん' : 'さん');
476
477 return $color.$suffix;
478}
479
480/**
481 * Fediverseの機能を使えるか
482 * マルチユーザーモードでは、管理者が外していなければ全員(BANされたユーザー以外)が使える。
483 * 管理者がロールを変えると「fediDefault」が記録され、その後はFEDIVERSEのビットで判断する。
484 */
485function can_federate(\stdClass $user): bool {
486 if (!ACTIVITYPUB_MULTIUSER || ($user->role ?? 0) === Roles::BANNED) return false;
487 if (($user->role ?? 0) & Roles::FEDIVERSE) return true;
488 return !isset($user->fediDefault);
489}
490
491/**
492 * 静的ファイル(画像・CSS等)のURL(config.phpのASS_DOMAIN、既定は「/static」)
493 * ASS_DOMAINがパス(例:「/static」)ならこのサイトのURL、URL(例:「https://ass.example.com/static」)ならそのサーバー
494 *
495 * @param string $path ASS_DOMAINの中のパス(例:「article/a.png」)
496 * @param string $base ASS_DOMAINがパスの場合に前に付けるサイトのURL(Fediverse等、完全なURLが必要な時)
497 * @param string|null $domain ASS_DOMAINの代わり(テスト用)
498 * @return string
499 */
500function ass_url(string $path = '', string $base = '', ?string $domain = null): string {
501 $ass = rtrim($domain ?? (defined('ASS_DOMAIN') ? (string)ASS_DOMAIN : '/static'), '/');
502 $url = $ass.($path !== '' ? '/'.ltrim($path, '/') : '');
503 if (preg_match('#^https?://#i', $url)) return $url;
504 return rtrim($base, '/').($url !== '' ? $url : '/');
505}
506
507/**
508 * 静的ファイルのディスク上のパス(ROOTからのパス。例:「/public/static/article/a.png」)
509 * ASS_DOMAINがURLの場合は、そのパスの部分(例:「/static」)をpublic/の下に置く(ASS_DOMAINのサーバーと同じ物を置くか同期する)
510 *
511 * @param string $path ASS_DOMAINの中のパス
512 * @param string|null $domain ASS_DOMAINの代わり(テスト用)
513 * @return string
514 */
515function ass_path(string $path = '', ?string $domain = null): string {
516 $ass = $domain ?? (defined('ASS_DOMAIN') ? (string)ASS_DOMAIN : '/static');
517 if (preg_match('#^https?://#i', $ass)) $ass = (string)parse_url($ass, PHP_URL_PATH);
518 $dir = trim($ass, '/');
519 return '/public'.($dir !== '' ? "/{$dir}" : '').($path !== '' ? '/'.ltrim($path, '/') : '');
520}
521
522/**
523 * アイコンのURL(無ければnoicon.png)
524 */
525function avatar_url(\stdClass $user): string {
526 $avatar = (string)($user->avatar ?? '');
527 $name = (string)($user->username ?? '');
528 if ($avatar === '' || $name === '' || !is_file(ROOT.ass_path("user/{$name}/{$avatar}"))) return ass_url('user/noicon.png');
529 return ass_url("user/{$name}/".rawurlencode($avatar));
530}
531
532/**
533 * CSRFのトークン(クッキーとフォームの両方に入れる)
534 *
535 * ページを開く度に有効期限(1日)を延ばす。以前は最初に作ってから5分で切れたので、
536 * 開いたままのフォームや、少し前に開いたページから送ると失敗した。
537 *
538 * @param bool|null $force 新しいトークンにする
539 * @return string
540 */
541function make_csrf_token(?bool $force = false): string {
542 static $sent = null;
543 $token = getcookie('csrf_token');
544 if ($force || !is_string($token) || !preg_match('/^[0-9a-f]{64}$/', $token)) $token = bin2hex(random_bytes(32));
545 if ($sent !== $token && !headers_sent()) {
546 $sent = $token;
547 setcookie('csrf_token', $token, [
548 'expires' => time() + 86400, // 1日(使う度に延ばす)
549 'path' => '/',
550 'domain' => $_SERVER['HTTP_HOST'] ?? '',
551 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
552 'httponly' => true,
553 'samesite' => 'Strict'
554 ]);
555 }
556 // 同じリクエストでもう一度呼ばれても同じトークン(以前は別のトークンになり、最初のフォームが失敗した)
557 $_COOKIE['csrf_token'] = $token;
558 return $token;
559}
560
561function safe_local_path(mixed $path, string $default = '/'): string {
562 if (!is_string($path) || $path === '' || $path[0] !== '/') return $default;
563 if (preg_match('/[\x00-\x1f\x7f]/', $path)) return $default;
564 return '/'.ltrim($path, '/\\');
565}
566
567function verify_csrf_token(string $token): bool {
568 return null !== getcookie('csrf_token') && hash_equals(getcookie('csrf_token'), $token);
569}
570
571function count_special_chars(string $str): int {
572 $count = 0;
573 $len = strlen($str);
574
575 for ($i = 0; $i < $len; ++$i) {
576 if (str_contains('!', $str[$i])) ++$count;
577 if (str_contains('"', $str[$i])) ++$count;
578 if (str_contains('#', $str[$i])) ++$count;
579 if (str_contains('$', $str[$i])) ++$count;
580 if (str_contains('%', $str[$i])) ++$count;
581 if (str_contains('&', $str[$i])) ++$count;
582 if (str_contains('\'', $str[$i])) ++$count;
583 if (str_contains('(', $str[$i])) ++$count;
584 if (str_contains(')', $str[$i])) ++$count;
585 if (str_contains('-', $str[$i])) ++$count;
586 if (str_contains('=', $str[$i])) ++$count;
587 if (str_contains('^', $str[$i])) ++$count;
588 if (str_contains('~', $str[$i])) ++$count;
589 if (str_contains('\\', $str[$i])) ++$count;
590 if (str_contains('|', $str[$i])) ++$count;
591 if (str_contains('[', $str[$i])) ++$count;
592 if (str_contains(']', $str[$i])) ++$count;
593 if (str_contains(':', $str[$i])) ++$count;
594 if (str_contains('@', $str[$i])) ++$count;
595 if (str_contains('`', $str[$i])) ++$count;
596 if (str_contains('*', $str[$i])) ++$count;
597 if (str_contains('{', $str[$i])) ++$count;
598 if (str_contains('}', $str[$i])) ++$count;
599 if (str_contains(';', $str[$i])) ++$count;
600 if (str_contains('+', $str[$i])) ++$count;
601 if (str_contains(',', $str[$i])) ++$count;
602 if (str_contains('<', $str[$i])) ++$count;
603 if (str_contains('.', $str[$i])) ++$count;
604 if (str_contains('>', $str[$i])) ++$count;
605 if (str_contains('/', $str[$i])) ++$count;
606 if (str_contains('?', $str[$i])) ++$count;
607 if (str_contains('_', $str[$i])) ++$count;
608 }
609
610 return $count;
611}
612
613function countmatch(string $str): bool {
614 $len = strlen($str);
615
616 $numUpper = preg_match_all('/[A-Z]/', $str) ?? 0;
617 $numLower = preg_match_all('/[a-z]/', $str) ?? 0;
618 $numDigit = preg_match_all('/[0-9]/', $str) ?? 0;
619 $numSymbol = count_special_chars($str);
620 $sum = $numUpper + $numLower + $numDigit + $numSymbol;
621
622 return $len == $sum;
623}
624
625function getImageInfo(string $url): \Std\Lib\Image {
626 $img = new \Std\Lib\Image($url);
627 return $img;
628}
629
630function align(int $val): int {
631 if ($val <= 0) return 1;
632 if ($val === 1) return 1;
633
634 $res = 0;
635 $lower = 1;
636
637 while ($lower * 2 <= $val) $lower *= 2;
638 $upper = $lower * 2;
639
640 if (($val - $lower) <= ($upper - $val)) return $lower;
641 return $upper;
642}
643
644function align_up(int $val): int {
645 if ($val <= 0) return 1;
646 if ($val === 1) return 1;
647
648 $res = 0;
649 $lower = 1;
650
651 while ($lower * 2 <= $val) $lower *= 2;
652 $upper = $lower * 2;
653
654 return $upper;
655}
656
657function align_down(int $val): int {
658 if ($val <= 0) return 1;
659 if ($val === 1) return 1;
660
661 $res = 0;
662 $lower = 1;
663
664 while ($lower * 2 <= $val) $lower *= 2;
665 return $lower;
666}