isSuccess = $isSuccess; $this->message = $message; $this->data = $data; } public static function Success(?string $message = null, array $data = []): self { return new self(true, $message, $data); } public static function Error(string $message, array $data = []): self { return new self(false, $message, $data); } } class Roles { // 例: if ($user->role & (Roles::STAFF | Roles::NINTENDONDA)) public const int BANNED = 0; public const int MEMBER = 1 << 0; // 1 public const int NINTENDONDA = 1 << 1; // 2 public const int PLAYSTATIONNDA = 1 << 2; // 4 public const int FULLNDA = 1 << 3; // 8 (NINTENDONDA + PLAYSTATIONNDA) public const int SUBSCRIBER = 1 << 4; // 16 public const int NSUBSCRIBER = 1 << 5; // 32 (NINTENDNDA + SUBSCRIBER) public const int PSUBSCRIBER = 1 << 6; // 64 (PLAYSTATIONNDA + SUBSCRIBER) public const int FSUBSCRIBER = 1 << 7; // 128 (NINTENDNDA + PLAYSTATIONNDA + SUBSCRIBER) public const int STAFF = 1 << 8; // 256 public const int ADMIN = 1 << 9; // 512 public const int FEDIVERSE = 1 << 10; // 1024(マルチユーザーのFediverseを使える) public const int GIT_REPOS = 1 << 11; // 2048(Gitlerで自分のリポジトリを作る・管理する、既定は無し) public const int GIT_ISSUES = 1 << 12; // 4096(Gitlerのイシュー、既定は全員) public const int GIT_PULLS = 1 << 13; // 8192(Gitlerのプルリクエスト、既定は無し) // 支援者(以前の「role >= SUBSCRIBER」と同じ範囲。ビットを足しても壊れない様に&で確認する) public const int PAID = self::SUBSCRIBER | self::NSUBSCRIBER | self::PSUBSCRIBER | self::FSUBSCRIBER | self::STAFF | self::ADMIN; // プロフィールの編集画面で管理者が付け外し出来るロール public const array LABELS = [ self::MEMBER => 'メンバー', self::NINTENDONDA => '任天堂NDA', self::PLAYSTATIONNDA => 'PlayStation NDA', self::FULLNDA => '全NDA', self::SUBSCRIBER => '支援者', self::NSUBSCRIBER => '支援者(任天堂NDA)', self::PSUBSCRIBER => '支援者(PlayStation NDA)', self::FSUBSCRIBER => '支援者(全NDA)', self::STAFF => 'スタッフ', self::ADMIN => '管理者', self::FEDIVERSE => 'Fediverse', self::GIT_REPOS => 'Gitのリポジトリ', self::GIT_ISSUES => 'Gitのイシュー', self::GIT_PULLS => 'Gitのプルリクエスト', ]; } class Gender { public const int UNKNOWN = -1; // 不明 public const int MALE = 0; // 男性 public const int FEMALE = 1; // 女性 } $colorPalette = [ 'ultradark' => [ 'black' => '#020102', 'white' => '#b3b1b3', 'grey' => '#5c535c', 'yellow' => '#8d8b0d', 'orange' => '#724e0b', 'green' => '#1e6907', 'purple' => '#410a5a', 'lime' => '#198d5b', 'pink' => '#9e0ea3', 'cyan' => '#1e8c9b', 'red' => '#861623', 'blue' => '#164a85', ], 'dark' => [ 'black' => '#120f12', 'white' => '#cfcbcf', 'grey' => '#746c75', 'yellow' => '#b8b515', 'orange' => '#ac7718', 'green' => '#2c980c', 'purple' => '#550f75', 'lime' => '#10c074', 'pink' => '#c016c6', 'cyan' => '#1cbcd0', 'red' => '#bc1729', 'blue' => '#1a6ecf', ], 'medium' => [ 'black' => '#232023', 'white' => '#f6f6f6', 'grey' => '#988f98', 'yellow' => '#f1ed25', 'orange' => '#f7a717', 'green' => '#2de12c', 'purple' => '#b421f8', 'lime' => '#20f398', 'pink' => '#f545f5', 'cyan' => '#29d3ff', 'red' => '#ee4030', 'blue' => '#2687f7', ], 'light' => [ 'black' => '#443b44', 'white' => '#fcfcfc', 'grey' => '#bdb4bd', 'yellow' => '#ecea71', 'orange' => '#f8c56a', 'green' => '#6cf344', 'purple' => '#ae6bdb', 'lime' => '#88ecc1', 'pink' => '#ea79d8', 'cyan' => '#8ae5ff', 'red' => '#f35869', 'blue' => '#6aa6eb', ], 'ultralight' => [ 'black' => '#574d57', 'white' => '#ffffff', 'grey' => '#d6cfd6', 'yellow' => '#f5f4cf', 'orange' => '#f3d6a3', 'green' => '#baf3a8', 'purple' => '#d2bae2', 'lime' => '#b6e9d3', 'pink' => '#e6b9de', 'cyan' => '#c2e8f3', 'red' => '#ecb0b7', 'blue' => '#bbd4f0', ], ]; function uuid(): string { $data = random_bytes(16); $data[6] = chr(ord($data[6]) & 0x0f | 0x40); $data[8] = chr(ord($data[8]) & 0x3f | 0x80); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } function kys(mixed $arg): void { if (!DEBUG_MODE) return; if (gettype($arg) == 'boolean') $arg = $arg === true ? 'true' : 'false'; if (gettype($arg) == 'array' || gettype($arg) == 'object') { foreach ($arg as $a) { if (gettype($a) == 'boolean') $a = $a === true ? 'true' : 'false'; } } echo ''; echo '
KILL
YOUR
SELF
'; echo '
';
  print_r($arg);
  echo '
';
  die();
}

function ffs(): void {
  if (!DEBUG_MODE) return;
  echo '';
  echo '
FOR
FUCKS
SAKE
'; $stack = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 50); $st = []; unset($stack[0]); $i = 0; echo '
';
  print_r($stack);
  echo '
';
  foreach ($stack as $s) {
    if (isset($s['file'])) $st[$i]['file'] = $s['file'].(isset($s['line']) ? ':'.$s['line'] : '');
    else $st[$i]['file'] = '';
    if (isset($s['function'])) $st[$i]['func'] = (isset($s['class']) ? $s['class'].(isset($s['type']) ? $s['type'] : '::') : '').$s['function'];
    else $st[$i]['func'] = '';
    if (isset($s['object'])) $st[$i]['objs'] = $s['object'];
    else $st[$i]['objs'] = new \stdClass;
    if (isset($s['args'])) $st[$i]['args'] = $s['args'];
    else $st[$i]['args'] = [];
    $i++;
  }
  unset($stack[$i]);

  foreach ($st as $s) {
    echo '
'; echo 'ファイル:'; echo $s['file'].'
'; echo '関数:'; echo $s['func'].'
'; echo 'オブジェクト:'; echo print_r($s['objs']).'
'; echo 'その他:'; echo print_r($s['args']).'
'; echo '
'; } die(); } function base58btc_encode(string $bin): string { $a = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; $base = 58; $num = \gmp_import($bin, 1, GMP_LSW_FIRST | GMP_NATIVE_ENDIAN); $res = ''; if (\gmp_cmp($num, 0) == 0) return '1'; while (\gmp_cmp($num, 0) > 0) { $mod = \gmp_intval(\gmp_mod($num, $base)); $res = $a[$mod].$res; $num = \gmp_div_q($num, $base); } $bytes = str_split($bin); foreach ($bytes as $byte) { if (ord($byte) === 0) { $res = '1'.$res; } else { break; } } return $res; } /** * Cronのタスクの登録(cron.phpと管理画面で使う) * ジョブの「タスク名」と処理を結び付ける。 * 処理はジョブ(CronTab)を受け取り、Result・bool・nullを返す(falseかResult::Errorは失敗)。 * * @param \Std\Lib\Cron $cron * @return void */ function register_cron_tasks(\Std\Lib\Cron $cron): void { if (ACTIVITYPUB_ENABLED) { // 新しいブログ投稿をフォロワーに送る $cron->register('apfederate', fn() => (new \Std\Lib\Activitypub((new \Site\Controller\BlogPost())->federatedPosts()))->federate()); // プロフィール(アクター)の更新をフォロワーに送る $cron->register('apupdate', fn() => (new \Std\Lib\Activitypub())->sendActorUpdate()); } if (ACTIVITYPUB_MULTIUSER) { // マイクロブログの配信(毎分) $cron->register('apdeliver', fn() => \Std\Lib\Fedi\Delivery::run()); // 90日より古いリモートの投稿とキャッシュを消す(1日1回) $cron->register('apcleanup', fn() => \Std\Lib\Fedi\Timeline::cleanup()); } if (AUTH_ENABLED && AUTH_VERIFY_EMAIL) { // メールアドレスを7日以内に確認しなかったアカウントを削除する $cron->register('authpurge', fn() => (new \Std\Lib\Auth())->purgeUnverified()); } if (AUTH_ENABLED) { // 通知のメールを送り、SSHのプッシュでGitlerのリポジトリに溜めた通知と配信を処理する(毎分) $cron->register('notify', function(): \Result { $n = \Std\Lib\Gitler\Gitler::enabled() ? \Std\Lib\Gitler\Gitler::flushDeferred() : 0; $res = \Std\Lib\Notifications::sendMail(); return \Result::Success("Gitler:{$n}件、".$res->message, $res->data + ['gitler' => $n]); }); } if (\Std\Lib\Gitler\Gitler::enabled()) { // Gitlerのリポジトリを整理する(プッシュで増えたファイルを1つのパックに、1日1回) $cron->register('gitlergc', fn() => \Std\Lib\Gitler\Gitler::gcAll()); } } function logger(LogType $section, mixed $arg): void { if (!LOGGING_ENABLED) return; $success = false; $logfile = ROOT.'/log/'; switch ($section) { case LogType::ActivityPub: $logfile .= 'ap_log.txt'; $success = true; break; case LogType::Auth: $logfile .= 'auth_log.txt'; $success = true; break; case LogType::Mailer: $logfile .= 'mail_log.txt'; $success = true; break; case LogType::Image: $logfile .= 'img_log.txt'; $success = true; break; case LogType::Cron: $logfile .= 'cron_log.txt'; $success = true; break; case LogType::Db: $logfile .= 'db_log.txt'; $success = true; break; default: break; } // log/が無ければ作る(新しいサイト等) if ($success && !is_dir(ROOT.'/log')) @mkdir(ROOT.'/log', 0755, true); if ($success) file_put_contents($logfile, $arg."\n", FILE_APPEND); } function to_money($amount, $lang) { $amount = floatval($amount); switch (strtolower($lang)) { case 'ja': if ($amount >= 100000000) { $oku = $amount / 100000000; return $oku.'億円'; } else if ($amount >= 10000) { $man = $amount / 10000; return $man.'万円'; } return number_format($amount, 0).'円'; case 'en': if ($amount >= 1000000000) { $billion = $amount / 1000000000; return '¥ '.$billion.' billion'; } else if ($amount >= 1000000) { $million = $amount / 1000000; return '¥ '.$million.' million'; } else if ($amount >= 1000) { $thousand = $amount / 1000; return '¥ '.$thousand.' thousand'; } return '¥ '.number_format($amount, 0); default: return '¥ '.number_format($amount, 0); } } function randstr(): string { $len = random_int(1, 20); return bin2hex(random_bytes($len)); } function assert_exists(mixed $assertion, Throwable|string|null $description = null): bool { if (!DEBUG_MODE) return false; if (ini_get('zend.assertions') < 1) trigger_error('assert機能性が無効です。有効するには、php.iniで「zend.assertions = 1」に設定して下さい。', E_USER_WARNING); if (isset($assertion)) return true; assert(false, $description ?? 'Assertion failed: value is not null'); return false; } function assert_null(mixed $assertion, Throwable|string|null $description = null): bool { if (!DEBUG_MODE) return false; if (ini_get('zend.assertions') < 1) trigger_error('assert機能性が無効です。有効するには、php.iniで「zend.assertions = 1」に設定して下さい。', E_USER_WARNING); if (is_null($assertion)) return true; assert(false, $description ?? 'Assertion failed: value is not null'); return false; } function assert_not_null(mixed $assertion, Throwable|string|null $description = null): bool { if (!DEBUG_MODE) return false; if (ini_get('zend.assertions') < 1) trigger_error('assert機能性が無効です。有効するには、php.iniで「zend.assertions = 1」に設定して下さい。', E_USER_WARNING); if (!is_null($assertion)) return true; assert(false, $description ?? 'Assertion failed: value is null'); return false; } function assert_unless_success(Result $assertion, Throwable|string|null $description = null): bool { if (!DEBUG_MODE) return false; if (ini_get('zend.assertions') < 1) trigger_error('assert機能性が無効です。有効するには、php.iniで「zend.assertions = 1」に設定して下さい。', E_USER_WARNING); if ($assertion->isSuccess) return true; assert(false, $description ?? $assertion->message ?? 'Assertion failed: Result is not successful'); return false; } /** * ブログの一覧(config.phpのBLOGS、無ければ「blog」だけ) * federateを省略した場合は「blog」だけ連合する(今まで通り) * * @return array ディレクトリ名 => ['title' => フィードの名前, 'federate' => ActivityPubで配信するか] */ function blogs(): array { $blogs = defined('BLOGS') ? BLOGS : ['blog' => []]; foreach ($blogs as $name => $b) $blogs[$name] = $b + ['title' => SITEINFO['title'], 'federate' => $name === 'blog']; return $blogs; } function getcookie(string $name): string|null { return $_COOKIE[$name] ?? null; } /** * 表示の設定(テーマ等)。ログイン中ならプロフィールに保存した値、無ければクッキー * * @param string $name 設定の名前(クッキーと同じ名前) * @return string|null */ function pref(string $name): string|null { static $prefs = null; if ($prefs === null) { $user = AUTH_ENABLED ? (new \Std\Lib\Auth())->getLoggedInUser() : null; $prefs = (array)($user->prefs ?? []); } return isset($prefs[$name]) ? (string)$prefs[$name] : getcookie($name); } function namecolor(\stdClass $userData): string { $ban = "#888888"; $male = "#97ACEF"; $female = "#F185C9"; $ungender = "#7C60B0"; $gender = 'color: '.($userData->gender === Gender::MALE ? $male : ($userData->gender === Gender::FEMALE ? $female : $ungender)).';'; $style = $userData->namecolor ?: ($userData->role !== Roles::BANNED ? $gender : 'color: '.$ban.';'); $style = htmlspecialchars($style, ENT_QUOTES, 'UTF-8'); $showname = $userData->displayname ?: $userData->username; $showname = htmlspecialchars($showname, ENT_QUOTES, 'UTF-8'); $color = "{$showname}"; if ($userData->role & (Roles::ADMIN | Roles::STAFF)) $color .= "✓"; $suffix = $userData->gender === Gender::MALE ? 'くん' : ($userData->gender === Gender::FEMALE ? 'ちゃん' : 'さん'); return $color.$suffix; } /** * Fediverseの機能を使えるか * マルチユーザーモードでは、管理者が外していなければ全員(BANされたユーザー以外)が使える。 * 管理者がロールを変えると「fediDefault」が記録され、その後はFEDIVERSEのビットで判断する。 */ function can_federate(\stdClass $user): bool { if (!ACTIVITYPUB_MULTIUSER || ($user->role ?? 0) === Roles::BANNED) return false; if (($user->role ?? 0) & Roles::FEDIVERSE) return true; return !isset($user->fediDefault); } /** * 静的ファイル(画像・CSS等)のURL(config.phpのASS_DOMAIN、既定は「/static」) * ASS_DOMAINがパス(例:「/static」)ならこのサイトのURL、URL(例:「https://ass.example.com/static」)ならそのサーバー * * @param string $path ASS_DOMAINの中のパス(例:「article/a.png」) * @param string $base ASS_DOMAINがパスの場合に前に付けるサイトのURL(Fediverse等、完全なURLが必要な時) * @param string|null $domain ASS_DOMAINの代わり(テスト用) * @return string */ function ass_url(string $path = '', string $base = '', ?string $domain = null): string { $ass = rtrim($domain ?? (defined('ASS_DOMAIN') ? (string)ASS_DOMAIN : '/static'), '/'); $url = $ass.($path !== '' ? '/'.ltrim($path, '/') : ''); if (preg_match('#^https?://#i', $url)) return $url; return rtrim($base, '/').($url !== '' ? $url : '/'); } /** * 静的ファイルのディスク上のパス(ROOTからのパス。例:「/public/static/article/a.png」) * ASS_DOMAINがURLの場合は、そのパスの部分(例:「/static」)をpublic/の下に置く(ASS_DOMAINのサーバーと同じ物を置くか同期する) * * @param string $path ASS_DOMAINの中のパス * @param string|null $domain ASS_DOMAINの代わり(テスト用) * @return string */ function ass_path(string $path = '', ?string $domain = null): string { $ass = $domain ?? (defined('ASS_DOMAIN') ? (string)ASS_DOMAIN : '/static'); if (preg_match('#^https?://#i', $ass)) $ass = (string)parse_url($ass, PHP_URL_PATH); $dir = trim($ass, '/'); return '/public'.($dir !== '' ? "/{$dir}" : '').($path !== '' ? '/'.ltrim($path, '/') : ''); } /** * アイコンのURL(無ければnoicon.png) */ function avatar_url(\stdClass $user): string { $avatar = (string)($user->avatar ?? ''); $name = (string)($user->username ?? ''); if ($avatar === '' || $name === '' || !is_file(ROOT.ass_path("user/{$name}/{$avatar}"))) return ass_url('user/noicon.png'); return ass_url("user/{$name}/".rawurlencode($avatar)); } /** * CSRFのトークン(クッキーとフォームの両方に入れる) * * ページを開く度に有効期限(1日)を延ばす。以前は最初に作ってから5分で切れたので、 * 開いたままのフォームや、少し前に開いたページから送ると失敗した。 * * @param bool|null $force 新しいトークンにする * @return string */ function make_csrf_token(?bool $force = false): string { static $sent = null; $token = getcookie('csrf_token'); if ($force || !is_string($token) || !preg_match('/^[0-9a-f]{64}$/', $token)) $token = bin2hex(random_bytes(32)); if ($sent !== $token && !headers_sent()) { $sent = $token; setcookie('csrf_token', $token, [ 'expires' => time() + 86400, // 1日(使う度に延ばす) 'path' => '/', 'domain' => $_SERVER['HTTP_HOST'] ?? '', 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), 'httponly' => true, 'samesite' => 'Strict' ]); } // 同じリクエストでもう一度呼ばれても同じトークン(以前は別のトークンになり、最初のフォームが失敗した) $_COOKIE['csrf_token'] = $token; return $token; } function safe_local_path(mixed $path, string $default = '/'): string { if (!is_string($path) || $path === '' || $path[0] !== '/') return $default; if (preg_match('/[\x00-\x1f\x7f]/', $path)) return $default; return '/'.ltrim($path, '/\\'); } function verify_csrf_token(string $token): bool { return null !== getcookie('csrf_token') && hash_equals(getcookie('csrf_token'), $token); } function count_special_chars(string $str): int { $count = 0; $len = strlen($str); for ($i = 0; $i < $len; ++$i) { if (str_contains('!', $str[$i])) ++$count; if (str_contains('"', $str[$i])) ++$count; if (str_contains('#', $str[$i])) ++$count; if (str_contains('$', $str[$i])) ++$count; if (str_contains('%', $str[$i])) ++$count; if (str_contains('&', $str[$i])) ++$count; if (str_contains('\'', $str[$i])) ++$count; if (str_contains('(', $str[$i])) ++$count; if (str_contains(')', $str[$i])) ++$count; if (str_contains('-', $str[$i])) ++$count; if (str_contains('=', $str[$i])) ++$count; if (str_contains('^', $str[$i])) ++$count; if (str_contains('~', $str[$i])) ++$count; if (str_contains('\\', $str[$i])) ++$count; if (str_contains('|', $str[$i])) ++$count; if (str_contains('[', $str[$i])) ++$count; if (str_contains(']', $str[$i])) ++$count; if (str_contains(':', $str[$i])) ++$count; if (str_contains('@', $str[$i])) ++$count; if (str_contains('`', $str[$i])) ++$count; if (str_contains('*', $str[$i])) ++$count; if (str_contains('{', $str[$i])) ++$count; if (str_contains('}', $str[$i])) ++$count; if (str_contains(';', $str[$i])) ++$count; if (str_contains('+', $str[$i])) ++$count; if (str_contains(',', $str[$i])) ++$count; if (str_contains('<', $str[$i])) ++$count; if (str_contains('.', $str[$i])) ++$count; if (str_contains('>', $str[$i])) ++$count; if (str_contains('/', $str[$i])) ++$count; if (str_contains('?', $str[$i])) ++$count; if (str_contains('_', $str[$i])) ++$count; } return $count; } function countmatch(string $str): bool { $len = strlen($str); $numUpper = preg_match_all('/[A-Z]/', $str) ?? 0; $numLower = preg_match_all('/[a-z]/', $str) ?? 0; $numDigit = preg_match_all('/[0-9]/', $str) ?? 0; $numSymbol = count_special_chars($str); $sum = $numUpper + $numLower + $numDigit + $numSymbol; return $len == $sum; } function getImageInfo(string $url): \Std\Lib\Image { $img = new \Std\Lib\Image($url); return $img; } function align(int $val): int { if ($val <= 0) return 1; if ($val === 1) return 1; $res = 0; $lower = 1; while ($lower * 2 <= $val) $lower *= 2; $upper = $lower * 2; if (($val - $lower) <= ($upper - $val)) return $lower; return $upper; } function align_up(int $val): int { if ($val <= 0) return 1; if ($val === 1) return 1; $res = 0; $lower = 1; while ($lower * 2 <= $val) $lower *= 2; $upper = $lower * 2; return $upper; } function align_down(int $val): int { if ($val <= 0) return 1; if ($val === 1) return 1; $res = 0; $lower = 1; while ($lower * 2 <= $val) $lower *= 2; return $lower; }