Merge branch 'master' of 192.168.10.104:suwako/LittleBeast into syntax

守矢諏訪子 <suwako@076.moe>、2026-11-10 17:20
コミット 9128438de352899e0fb84d28c9e20856704d257c ・親 7cc6b79 ・親 6287571 ・ファイルを見る

22個のファイル、+800 -303

変更 README.md
@@ -59,7 +59,7 @@
5959 ## How to install
6060 ```sh
6161 cd /var/www/htdocs
62-git clone https://github.com/TechnicalSuwako/LittleBeast.git .
62+git clone https://gitler.moe/suwako/LittleBeast.git .
6363 mv config/config.sample.php config/config.php
6464 ```
6565
変更 newpost.php
@@ -35,19 +35,22 @@
3535 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
3636 OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
3737 */
38-if (!isset($argv[1])) die('usage: php newpost.php [slug]');
39-if (file_exists("blog/{$argv[1]}.md")) die("エラー: ファイル「blog/{$argv[1]}.md」は既に存在します。\n");
38+if (!isset($argv[1])) die('usage: php newpost.php <slug> [dir]');
39+$dir = isset($argv[2]) ? $argv[2] : 'blog';
40+if (file_exists("{$dir}/{$argv[1]}.md")) die("エラー: ファイル「{$dir}/{$argv[1]}.md」は既に存在します。\n");
4041
4142 define('ROOT', realpath(__DIR__));
4243 require_once ROOT.'/config/config.php';
4344 include('util.php');
44-$post = fopen("blog/{$argv[1]}.md", "w");
45+$post = fopen("{$dir}/{$argv[1]}.md", "w");
4546 fwrite($post, "title: 【】\n");
4647 fwrite($post, "uuid: ".uuid()."\n");
4748 fwrite($post, "author: 諏訪子\n");
49+fwrite($post, "draft: true\n");
4850 fwrite($post, "date: ".date("Y-m-d H:i:s")."\n");
4951 fwrite($post, "thumbnail: \n");
5052 fwrite($post, "thumborient: center\n");
5153 fwrite($post, "category: \n");
54+fwrite($post, "css: \n");
5255 fwrite($post, "----\n");
5356 fclose($post);
@@ -6,6 +6,7 @@
66 overflow: auto;
77 font-family: Consolas,Monaco,monospace;
88 border: 1px solid #f545f5;
9+ overflow-x: auto;
910 }
1011
1112 /*pre > code {
@@ -71,6 +71,19 @@
7171 color: #fcfcfc;
7272 }
7373
74+div.op-grid {
75+ display: grid;
76+ grid-auto-flow: column;
77+ margin: 8px 0;
78+}
79+
80+div.op-grid-item {
81+ margin: 4px;
82+ padding: 2px;
83+ border: 1px solid #fcfcfc;
84+ border-radius: 2px;
85+}
86+
7487 tr.status-free { background-color: #34860e; color: #fff; }
7588 tr.status-reserved { background-color: #c59e1d; color: #fff; }
7689 tr.status-use { background-color: #850000; color: #fff; }
変更 route.php
@@ -122,7 +122,7 @@
122122 // .xhtmlで終わるURLのリダイレクト処理
123123 if (preg_match('/(.+)\.xhtml$/', $uri, $matches)) {
124124 // 末尾の.xhtmlを取り除く
125- $newUri = $matches[1];
125+ $newUri = safe_local_path($matches[1]);
126126 header('HTTP/1.1 301 Moved Permanently');
127127 header('Location: ' . $newUri);
128128 exit();
@@ -44,6 +44,20 @@
4444 private string $domain = 'technicalsuwako.moe';
4545
4646 /**
47+ * フィードは誰でも読めるので、会員・支援者・スタッフ限定の記事を除外する
48+ * (getPosts()はプレビューを隠すだけで、記事そのものは返す)
49+ *
50+ * @param array $posts 投稿の配列
51+ * @return array 公開記事だけの配列
52+ */
53+ private function publicOnly(array $posts): array {
54+ $restricted = ['memberonly', 'subscriberonly', 'staffonly', 'nintendodevonly'];
55+ return array_values(array_filter($posts, function($post) use ($restricted): bool {
56+ return empty(array_intersect($restricted, $post['category'] ?? []));
57+ }));
58+ }
59+
60+ /**
4761 * 最新の5記事のAtomフィードを生成する
4862 *
4963 * @param array $params パラメータ配列
@@ -54,7 +68,7 @@
5468 // 最新の投稿を取得
5569 $posts = $this->getPosts('/blog/', null);
5670 // 最新の5件に制限
57- $posts = array_slice($posts, 0, 5);
71+ $posts = array_slice($this->publicOnly($posts), 0, 5);
5872
5973 // サイトのドメインを取得
6074 $domain = $_SERVER['HTTP_HOST'];
@@ -90,20 +90,32 @@
9090 exit;
9191 }
9292
93- $input = file_get_contents('php://input');
93+ $maxBody = 65536;
94+ $input = file_get_contents('php://input', false, null, 0, $maxBody + 1);
95+ if ($input === false || strlen($input) > $maxBody) {
96+ header('HTTP/1.1 413 Payload Too Large');
97+ exit;
98+ }
99+
94100 $activity = json_decode($input, true);
95- if (!$activity || !isset($activity['type'])) {
101+ if (!is_array($activity) || !isset($activity['type']) || !is_string($activity['type'])) {
96102 header('HTTP/1.1 400 Bad Request');
97103 header('Content-Type: application/activity+json');
98104 echo json_encode(['error' => '不正なアクティビティ']);
99105 exit;
100106 }
101107
102- logger(LogType::ActivityPub, "受付に入れた:".json_encode($activity));
103-
104108 try {
105109 header('Content-Type: application/activity+json');
106110 $ap = new Activitypub();
111+
112+ if (!$ap->verifyInboxSignature($input, $activity)) {
113+ header('HTTP/1.1 401 Unauthorized');
114+ echo json_encode(['error' => '署名の検証に失敗']);
115+ exit;
116+ }
117+
118+ logger(\LogType::ActivityPub, "受付に入れた:".json_encode($activity));
107119 $ap->postInbox($activity);
108120 exit;
109121 } catch (\Exception $e) {
@@ -308,20 +308,26 @@
308308 return $text;
309309 }
310310
311- $highlightedText = $text;
311+ $alts = [];
312312
313313 foreach ($keywords as $keyword) {
314314 $keyword = trim($keyword);
315- if (empty($keyword)) continue;
316-
317- // キーワードを大文字小文字を区別せずに置換
318- $highlightedText = preg_replace(
319- '/('.preg_quote($keyword, '/').')/iu',
320- '<span class="search-highlight">$1</span>',
321- $highlightedText
322- );
315+ $escaped = htmlspecialchars($keyword, ENT_QUOTES, 'UTF-8');
316+ if ($escaped === '') continue;
317+ $alts[] = preg_quote($escaped, '/');
323318 }
319+ if (empty($alts)) return $text;
320+ $pattern = '/('.implode('|', $alts).')/iu';
321+
322+ $parts = preg_split('/(<[^>]*>|&[#\w]+;)/u', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
323+ if ($parts === false) return $text;
324+
325+ foreach ($parts as $i => $part) {
326+ if ($part === '' || $part[0] === '<' || $part[0] === '&') continue;
327+ $res = preg_replace($pattern, '<span class="search-highlight">$1</span>', $part);
328+ if ($res !== null) $parts[$i] = $res;
329+ }
324330
325- return $highlightedText;
331+ return implode('', $parts);
326332 }
327333 }
@@ -39,7 +39,7 @@
3939
4040 trait Mods {
4141 public function getMenu(): array {
42- return [
42+ $menu = [
4343 [
4444 'class' => 'menu-item',
4545 'href' => '/',
@@ -82,13 +82,18 @@
8282 'text' => 'スタッフ限定',
8383 'show' => true,
8484 ],
85- [
85+ ];
86+
87+ if (OPENPROVIDER_ENABLED) {
88+ $menu[] = [
8689 'class' => 'menu-item',
8790 'href' => '/openprovider',
8891 'page' => 'openprovider',
8992 'text' => 'OpenProvider管理',
9093 'show' => true,
91- ],
92- ];
94+ ];
95+ }
96+
97+ return $menu;
9398 }
9499 }
@@ -78,6 +78,10 @@
7878 $this->tmpl->assign('pagetit', $this->pagetit);
7979
8080 if (!$this->user || $this->user->role !== Roles::ADMIN) goto noaccess;
81+ $data = $this->op->getReseller(['with_statistics' => true]);
82+ $domain = $this->op->listDomains(['order_by.renewal_date' => 'asc', 'status' => 'ACT']);
83+ $this->tmpl->assign('data', $data->data);
84+ $this->tmpl->assign('domain', $domain->data);
8185 $this->tmpl->render('index');
8286 exit();
8387
@@ -259,7 +263,7 @@
259263 try {
260264 $handle = '';
261265 if (isset($params['handle'])) $handle = $params['handle'];
262- if ($handle === '') {
266+ if (!$this->isValidHandle($handle)) {
263267 header('Location: /openprovider/listcustomers');
264268 exit();
265269 }
@@ -283,7 +287,7 @@
283287 try {
284288 $handle = '';
285289 if (isset($params['handle'])) $handle = $params['handle'];
286- if ($handle === '') {
290+ if (!$this->isValidHandle($handle)) {
287291 header('Location: /openprovider/listcustomers');
288292 exit();
289293 }
@@ -338,7 +342,7 @@
338342 try {
339343 $tld = '';
340344 if (isset($params['tld'])) $tld = $params['tld'];
341- if ($tld === '') {
345+ if (!preg_match('/^[A-Za-z0-9-]{1,63}$/', $tld)) {
342346 header('Location: /');
343347 exit();
344348 }
@@ -443,7 +447,7 @@
443447 try {
444448 $domain = '';
445449 if (isset($params['domain'])) $domain = $params['domain'];
446- if ($domain === '') {
450+ if (!preg_match('/^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z0-9-]{2,63}$/', $domain)) {
447451 header('Location: /');
448452 exit();
449453 }
@@ -461,5 +465,18 @@
461465 } catch (\Exception $e) {
462466 throw new \Exception($e->getMessage());
463467 }
468+ }
469+
470+ //////////
471+
472+ /**
473+ * 顧客ハンドルの形式を確認する(例:AB123456-XX)
474+ * URLの値はAPIのパスとキャッシュのファイル名にそのまま入るので、先に確認する
475+ *
476+ * @param string $handle 顧客ハンドル
477+ * @return bool 正しい形式ならtrue
478+ */
479+ private function isValidHandle(string $handle): bool {
480+ return preg_match('/^[A-Za-z0-9-]{1,64}$/', $handle) === 1;
464481 }
465482 }
@@ -59,7 +59,7 @@
5959 $nyuU = '';
6060
6161 if ($doLogin) {
62- if (!\verify_csrf_token($_POST['csrf_token'])) {
62+ if (!is_string($_POST['csrf_token'] ?? null) || !\verify_csrf_token($_POST['csrf_token'])) {
6363 header('Location: /');
6464 exit();
6565 }
@@ -81,16 +81,25 @@
8181 exit();
8282 }
8383
84- $auth = new Auth($a[0]);
85- $res = $auth->isUserExist($a[0]);
86- if (!$res->isSuccess) {
87- $error = $res->message;
84+ $loginError = 'エラー:ユーザー名又はパスワードが一致していません。';
85+ $username = is_string($a[0] ?? null) ? $a[0] : '';
86+ $password = is_string($a[1] ?? null) ? $a[1] : '';
87+
88+ if ($username === '' || $password === '') {
89+ $error = $loginError;
90+ } else if ($auth->isLoginThrottled($username)) {
91+ $nyuU = $username;
92+ $error = 'エラー:ログインの試行回数が多過ぎます。暫くしてから再度お試し下さい。';
8893 } else {
89- $result = $auth->setToken($a[0], $a[1]);
94+ $auth = new Auth($username);
95+ $res = $auth->isUserExist($username);
96+ $result = $res->isSuccess ? $auth->setToken($username, $password) : \Result::Error($loginError);
9097 if (!$result->isSuccess) {
91- $nyuU = $a[0];
92- $error = $result->message;
98+ $auth->recordLoginFailure($username);
99+ $nyuU = $username;
100+ $error = $res->isSuccess ? $result->message : $loginError;
93101 } else {
102+ $auth->clearLoginFailures();
94103 header('Location: /');
95104 exit();
96105 }
@@ -148,7 +157,7 @@
148157 $nyuE = '';
149158
150159 if ($doRegister) {
151- if (!\verify_csrf_token($_POST['csrf_token'])) {
160+ if (!is_string($_POST['csrf_token'] ?? null) || !\verify_csrf_token($_POST['csrf_token'])) {
152161 header('Location: /');
153162 exit();
154163 }
@@ -43,7 +43,7 @@
4343
4444 /**
4545 * ActivityPubプロトコルの実装クラス
46- *
46+ *
4747 * このクラスはActivityPubプロトコルを利用して分散型
4848 * ソーシャルネットワーキングを実装します。
4949 */
@@ -64,7 +64,7 @@
6464 public function __construct(array $posts = []) {
6565 if (!ACTIVITYPUB_ENABLED) return;
6666 $this->https = 'http'.((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 's' : '').'://';
67- $this->domain = $this->https.($_SERVER['SERVER_NAME'] ?? '127.0.0.1:8000');
67+ $this->domain = $this->https.($_SERVER['HTTP_HOST'] ?? '127.0.0.1:8000');
6868 $this->actor = FEDIINFO['actor'];
6969 $this->actorNick = FEDIINFO['actorNick'];
7070 $this->desc = FEDIINFO['desc'];
@@ -423,9 +423,160 @@
423423 foreach ($f as $inbox) $this->sendActivity($inbox, $update);
424424 }
425425
426+ /**
427+ * 受付ボックスへのリクエストのHTTP署名を検証する
428+ *
429+ * @param string $body リクエストの本文(生データ)
430+ * @param array $activity デコード済みのアクティビティ
431+ * @return bool 署名が正しく、署名者がactorと一致すればtrue
432+ */
433+ public function verifyInboxSignature(string $body, array $activity): bool {
434+ if (!ACTIVITYPUB_ENABLED) return false;
435+
436+ // Signature: keyId="…",algorithm="…",headers="…",signature="…"
437+ $sigHeader = $_SERVER['HTTP_SIGNATURE'] ?? '';
438+ if ($sigHeader === '') return false;
439+ preg_match_all('/(\w+)="([^"]*)"/', $sigHeader, $m, PREG_SET_ORDER);
440+ $sig = [];
441+ foreach ($m as $kv) $sig[$kv[1]] = $kv[2];
442+ if (empty($sig['keyId']) || empty($sig['signature'])) return false;
443+ if (isset($sig['algorithm'])
444+ && !in_array(strtolower($sig['algorithm']), ['rsa-sha256', 'hs2019'], true)) return false;
445+
446+ // 最低限これらのヘッダーが署名されている事
447+ $signed = explode(' ', strtolower(trim($sig['headers'] ?? 'date')));
448+ foreach (['(request-target)', 'host', 'date', 'digest'] as $req) {
449+ if (!in_array($req, $signed, true)) return false;
450+ }
451+
452+ // 再送攻撃対策:Dateは前後5分以内
453+ $date = strtotime($_SERVER['HTTP_DATE'] ?? '');
454+ if (!$date || abs(time() - $date) > 300) return false;
455+
456+ // Digestが本文と一致する事 / Digest must match the body
457+ $expected = base64_encode(hash('sha256', $body, true));
458+ $digestOk = false;
459+ foreach (explode(',', $_SERVER['HTTP_DIGEST'] ?? '') as $d) {
460+ $d = trim($d);
461+ if (stripos($d, 'SHA-256=') === 0 && hash_equals($expected, substr($d, 8))) $digestOk = true;
462+ }
463+ if (!$digestOk) return false;
464+
465+ // 署名対象の文字列を組み立てる
466+ $lines = [];
467+ foreach ($signed as $h) {
468+ if ($h === '(request-target)') {
469+ $lines[] = '(request-target): '.strtolower($_SERVER['REQUEST_METHOD']).' '.$_SERVER['REQUEST_URI'];
470+ continue;
471+ }
472+ $key = match ($h) {
473+ 'content-type' => 'CONTENT_TYPE',
474+ 'content-length' => 'CONTENT_LENGTH',
475+ default => 'HTTP_'.strtoupper(str_replace('-', '_', $h)),
476+ };
477+ if (!isset($_SERVER[$key])) return false;
478+ $lines[] = "{$h}: {$_SERVER[$key]}";
479+ }
480+
481+ // 公開鍵を取得し、鍵の持ち主がアクティビティのactorと一致する事を確認する
482+ $actor = $activity['actor'] ?? null;
483+ if (is_array($actor)) $actor = $actor['id'] ?? null;
484+ if (!is_string($actor)) return false;
485+
486+ $doc = $this->fetchRemoteJson(strtok($sig['keyId'], '#'));
487+ if (!$doc) return false;
488+ $keys = $doc['publicKey'] ?? $doc;
489+ if (array_is_list($keys)) {
490+ $keys = array_values(array_filter($keys, fn($k) => is_array($k) && ($k['id'] ?? '') === $sig['keyId']))[0] ?? [];
491+ }
492+ if (($keys['id'] ?? '') !== $sig['keyId']) return false;
493+ if (($keys['owner'] ?? $doc['id'] ?? null) !== $actor) return false;
494+
495+ $pub = openssl_pkey_get_public($keys['publicKeyPem'] ?? '');
496+ $raw = base64_decode($sig['signature'], true);
497+ if (!$pub || $raw === false) return false;
498+
499+ return openssl_verify(implode("\n", $lines), $raw, $pub, OPENSSL_ALGO_SHA256) === 1;
500+ }
501+
426502 // 機能性メソッド
427503
428504 /**
505+ * リモートのURLに接続しても安全か確認する(SSRF対策)
506+ * check a remote URL is safe to connect to (SSRF protection)
507+ *
508+ * httpsのみ、443番ポートのみ、認証情報なし、制御文字・空白なし(CRLFインジェクション対策)、
509+ * 全ての解決先IPが公開アドレスである事。
510+ * https only, port 443 only, no credentials, no control chars or spaces (CRLF injection),
511+ * and every resolved IP must be a public address.
512+ * 注意:Curlは改めて名前解決するので、DNSリバインディングまでは防げない。
513+ * note: Curl resolves the name again, so this does not stop DNS rebinding.
514+ *
515+ * @param mixed $url 確認するURL
516+ * @return bool 安全ならtrue
517+ */
518+ private function isSafeRemoteUrl(mixed $url): bool {
519+ if (!is_string($url) || strlen($url) > 2048) return false;
520+ if (preg_match('/[\x00-\x20\x7f]/', $url)) return false;
521+
522+ $p = parse_url($url);
523+ if (!$p || strtolower($p['scheme'] ?? '') !== 'https' || empty($p['host'])) return false;
524+ if (isset($p['user']) || isset($p['pass'])) return false;
525+ if (isset($p['port']) && (int)$p['port'] !== 443) return false;
526+
527+ $host = trim($p['host'], '[]');
528+ if (filter_var($host, FILTER_VALIDATE_IP)) {
529+ $ips = [$host];
530+ } else {
531+ $ips = [];
532+ foreach ((@dns_get_record($host, DNS_A | DNS_AAAA) ?: []) as $r) {
533+ if (isset($r['ip'])) $ips[] = $r['ip'];
534+ if (isset($r['ipv6'])) $ips[] = $r['ipv6'];
535+ }
536+ }
537+ if (empty($ips)) return false;
538+
539+ foreach ($ips as $ip) {
540+ if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) return false;
541+ }
542+
543+ return true;
544+ }
545+
546+ /**
547+ * リモートのActivityPubオブジェクトを取得する
548+ * fetch a remote ActivityPub object
549+ *
550+ * @param string $url 取得するURL
551+ * @return array|null デコード済みのJSON、失敗した場合はnull
552+ */
553+ private function fetchRemoteJson(string $url): ?array {
554+ if (!$this->isSafeRemoteUrl($url)) {
555+ logger(\LogType::ActivityPub, "安全ではないURLを拒否: {$url}");
556+ return null;
557+ }
558+
559+ $curl = new Curl($url);
560+ // リダイレクト先は検証されないので追わない / redirect targets aren't re-checked, so don't follow them
561+ $curl->setHeaders(['Accept: application/activity+json'])
562+ ->setFollowRedirects(false)
563+ ->setTimeout(10)
564+ ->setCaInfo('/etc/ssl/cert.pem');
565+
566+ logger(\LogType::ActivityPub, "リモートにリクエスト: {$url}");
567+ $res = $curl->execute();
568+ if (!$res->isSuccess || $curl->getResponseCode() !== 200) {
569+ logger(\LogType::ActivityPub, "取得に失敗: HTTP {$curl->getResponseCode()}, エラー: {$curl->getError()}");
570+ return null;
571+ }
572+
573+ $body = $curl->getResponseBody();
574+ if (strlen($body) > 1048576) return null;
575+ $data = json_decode($body, true);
576+ return is_array($data) ? $data : null;
577+ }
578+
579+ /**
429580 * 指定されたインボックスURLにアクティビティを送信する
430581 *
431582 * @param string $inboxUrl 送信先のインボックスURL
@@ -433,6 +584,11 @@
433584 * @return void
434585 */
435586 private function sendActivity(string $inboxUrl, array $activity): void {
587+ if (!$this->isSafeRemoteUrl($inboxUrl)) {
588+ logger(\LogType::ActivityPub, "安全ではない受付ボックスへの送信を拒否: {$inboxUrl}");
589+ return;
590+ }
591+
436592 $privFile = FEDIINFO['privkey'];
437593 $priv = file_get_contents($privFile);
438594 if ($priv === false) {
@@ -501,14 +657,21 @@
501657 */
502658 private function acceptFollower(array $activity): void {
503659 $followerActor = $activity['actor'] ?? null;
504- if (!$followerActor) {
660+ if (!is_string($followerActor) || !$this->isSafeRemoteUrl($followerActor)) {
505661 header('HTTP/1.1 400 Bad Request');
506662 header('Content-Type: application/activity+json');
507663 echo json_encode(['error' => 'アクターがない']);
508664 exit;
509665 }
510666
511- $this->storeFollower($followerActor);
667+ $object = $activity['object'] ?? null;
668+ if (is_array($object)) $object = $object['id'] ?? null;
669+ if ($object !== "{$this->domain}/ap/actor") {
670+ header('HTTP/1.1 400 Bad Request');
671+ header('Content-Type: application/activity+json');
672+ echo json_encode(['error' => '不正なフォロー対象']);
673+ exit;
674+ }
512675
513676 $inbox = $this->getInboxFromActor($followerActor);
514677 if (!$inbox) {
@@ -518,6 +681,8 @@
518681 exit;
519682 }
520683
684+ $this->storeFollower($followerActor);
685+
521686 $accept = [
522687 '@context' => [
523688 'https://www.w3.org/ns/activitystreams',
@@ -567,29 +732,10 @@
567732 * @return string|null インボックスURL、取得に失敗した場合はnull
568733 */
569734 private function getInboxFromActor(string $actor): ?string {
570- $curl = new Curl($actor);
571- $curl->setHeaders(['Accept: application/activity+json'])
572- ->setFollowRedirects(true)
573- ->setMaxRedirects(5)
574- ->setCaInfo('/etc/ssl/cert.pem');
735+ $data = $this->fetchRemoteJson($actor);
736+ if (!$data) return null;
575737
576- logger(LogType::ActivityPub, "アクターURLにリクエスト: {$actor}");
577- $success = $curl->execute();
578- if (!$success) {
579- logger(LogType::ActivityPub, "アクターリクエストに失敗: ".$curl->getError());
580- return null;
581- }
582-
583- $res = $curl->getResponseBody();
584- $code = $curl->getResponseCode();
585- $err = $curl->getError();
586-
587- if ($code !== 200) {
588- logger(LogType::ActivityPub, "アクター取得に失敗: HTTP {$code}, エラー: {$err}");
589- return null;
590- }
591-
592- $data = json_decode($res, true);
593- return $data['inbox'] ?? null;
738+ $inbox = $data['inbox'] ?? null;
739+ return $this->isSafeRemoteUrl($inbox) ? $inbox : null;
594740 }
595741 }
@@ -115,22 +115,17 @@
115115
116116 $verify = $this->verifyLogin($username, $password);
117117 if (!$verify->isSuccess) {
118- return Result::error($verify->message);
118+ return Result::Error($verify->message);
119119 }
120120
121121 $ip = $_SERVER['REMOTE_ADDR'];
122- if (!empty($_SERVER['HTTP_X_FORMARDED_FOR'])) {
123- $ipList = explode(',', $_SERVER['HTTP_X_FORMARDED_FOR']);
124- $ip = trim($ipList[0]);
125- }
126-
127122 $res = $this->purgeOldTokens($userData);
128123 if (!$res->isSuccess) return Result::Error($res->message);
129124 $userData = $res->data;
130125 $token = bin2hex(random_bytes(64));
131126 $expire = time() + $this->tokenDuration;
132127 $newToken = [
133- 'token' => $token,
128+ 'token' => hash('sha256', $token),
134129 'ip' => $ip,
135130 'createDate' => time(),
136131 'expDate' => $expire,
@@ -145,22 +140,22 @@
145140
146141 if (file_put_contents($path, $json) === false) {
147142 logger(LogType::Auth, '【setToken】JSONファイルにユーザーデータを保存出来なかった。パス:'.$path);
148- return Result::error('エラー:ユーザーデータの保存に失敗。');
143+ return Result::Error('エラー:ユーザーデータの保存に失敗。');
149144 }
150145
151146 if (!setcookie('kerozen', $token, [
152147 'expires' => $expire,
153148 'path' => '/',
154- 'domain' => $_SERVER['SERVER_NAME'],
149+ 'domain' => $_SERVER['HTTP_HOST'],
155150 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
156151 'httponly' => true,
157152 'samesite' => 'Strict'
158153 ])) {
159- logger(LogType::Auth, '【setToken】クッキーを設定出来なかった。トークン:'.$token);
160- return Result::error('エラー:クッキーを設定に失敗。');
154+ logger(LogType::Auth, '【setToken】クッキーを設定出来なかった。');
155+ return Result::Error('エラー:クッキーを設定に失敗。');
161156 }
162157
163- return Result::success('ログイン成功');
158+ return Result::Success('ログイン成功');
164159 }
165160
166161 public function getToken(): ?string {
@@ -175,25 +170,95 @@
175170 public function logout(?string $token = null): Result {
176171 if (!AUTH_ENABLED) return Result::Error('エラー:認証システムは無効です。');
177172 $userData = $this->getUserData();
173+ $token = $token ?? $this->token;
178174
179- if (!$token) {
180- $userData->tokens = array_filter($userData->tokens, function($t): bool {
181- return ($t->expires ?? 0) > time();
182- });
175+ if ($userData instanceof \stdClass && $token) {
176+ $hashed = hash('sha256', $token);
177+ $userData->tokens = array_values(array_filter($userData->tokens ?? [], function($t) use ($hashed): bool {
178+ return isset($t->token) && !hash_equals((string)$t->token, $hashed) && ($t->expDate ?? 0) > time();
179+ }));
180+
181+ $path = $this->dataDir.$this->id.'.'.$userData->username.'.json';
182+ $json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
183+ if (file_put_contents($path, $json) === false) {
184+ logger(\LogType::Auth, '【logout】JSONファイルを変更出来なかった。chownをご確認下さい。パス:'.$path);
185+ }
183186 }
184187
185- setcookie('kerozen', null, [
188+ setcookie('kerozen', '', [
186189 'expires' => 0,
187190 'path' => '/',
188- 'domain' => $_SERVER['SERVER_NAME'],
191+ 'domain' => $_SERVER['HTTP_HOST'],
189192 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
190193 'httponly' => true,
191194 'samesite' => 'Strict'
192195 ]);
193196
194- return Result::success();
197+ return Result::Success();
195198 }
196199
200+ /**
201+ * ログインの試行回数を確認する(IP毎:15分で5回、ユーザー名毎:1時間で20回)
202+ *
203+ * @param string $username ユーザー名
204+ * @return bool 制限中ならtrue
205+ */
206+ public function isLoginThrottled(string $username): bool {
207+ $now = time();
208+ $ip = $this->countFailures('ip-'.($_SERVER['REMOTE_ADDR'] ?? ''), $now - 900);
209+ $usr = $this->countFailures('user-'.$username, $now - 3600);
210+ return $ip >= 5 || $usr >= 20;
211+ }
212+
213+ /**
214+ * ログインの失敗を記録する
215+ *
216+ * @param string $username ユーザー名
217+ * @return void
218+ */
219+ public function recordLoginFailure(string $username): void {
220+ $this->addFailure('ip-'.($_SERVER['REMOTE_ADDR'] ?? ''), time() - 900);
221+ $this->addFailure('user-'.$username, time() - 3600);
222+ }
223+
224+ /**
225+ * ログイン成功時に、そのIPの失敗記録を消す
226+ *
227+ * @return void
228+ */
229+ public function clearLoginFailures(): void {
230+ $path = $this->throttlePath('ip-'.($_SERVER['REMOTE_ADDR'] ?? ''));
231+ if (is_file($path)) unlink($path);
232+ }
233+
234+ private function throttlePath(string $key): string {
235+ $dir = ROOT.'/data/ratelimit/';
236+ if (!is_dir($dir)) mkdir($dir, 0700, true);
237+ return $dir.hash('sha256', $key).'.json';
238+ }
239+
240+ private function countFailures(string $key, int $since): int {
241+ $path = $this->throttlePath($key);
242+ if (!is_file($path)) return 0;
243+ $list = json_decode((string)file_get_contents($path), true);
244+ if (!is_array($list)) return 0;
245+ return count(array_filter($list, fn($t) => is_int($t) && $t > $since));
246+ }
247+
248+ private function addFailure(string $key, int $since): void {
249+ $fp = fopen($this->throttlePath($key), 'c+');
250+ if (!$fp) return;
251+ flock($fp, LOCK_EX);
252+ $list = json_decode((string)stream_get_contents($fp), true);
253+ $list = is_array($list) ? array_values(array_filter($list, fn($t) => is_int($t) && $t > $since)) : [];
254+ $list[] = time();
255+ ftruncate($fp, 0);
256+ rewind($fp);
257+ fwrite($fp, json_encode($list));
258+ flock($fp, LOCK_UN);
259+ fclose($fp);
260+ }
261+
197262 public function isUserExist(?string $username = null): Result {
198263 if (!AUTH_ENABLED) return Result::Error('エラー:認証システムは無効です。');
199264 if (null === $username) return Result::Error('エラー:ユーザー名をご入力下さい。');
@@ -316,28 +381,32 @@
316381 private function getUserId(?string $username = null, ?string $token = null): int {
317382 if (!AUTH_ENABLED) return 0;
318383 if (!$username && !$token) return 0;
384+ if (!$username && !preg_match('/^[0-9a-f]{128}$/', $token)) return 0;
385+
319386 $file = scandir($this->dataDir);
320- $matches = [];
321- $id = 0;
387+ $now = time();
388+ $hashed = $token ? hash('sha256', $token) : '';
322389
323390 foreach ($file as $f) {
324- if ($f === '.kara' || $f === '.' || $f === '..') continue;
391+ if (!preg_match('/^(\d+)\.[A-Za-z0-9]+\.json$/', $f, $m)) continue;
325392
326393 $path = "{$this->dataDir}{$f}";
327394 if (!is_file($path)) continue;
328- $content = file_get_contents($path);
329- if ($token && str_contains($content, $token)) {
330- $matches[] = $path;
331- break;
332- } else if (!$token && str_contains($content, '"username": "'.$username.'",')) {
333- $matches[] = $path;
334- break;
395+ $data = json_decode(file_get_contents($path));
396+ if (!$data instanceof \stdClass) continue;
397+
398+ if ($username) {
399+ if (isset($data->username) && $data->username === $username) return (int)$m[1];
400+ continue;
335401 }
402+
403+ foreach (($data->tokens ?? []) as $t) {
404+ if (!isset($t->token, $t->expDate)) continue;
405+ if ($t->expDate > $now && hash_equals((string)$t->token, $hashed)) return (int)$m[1];
406+ }
336407 }
337- if (!isset($matches[0])) return 0;
338- $id = (int)preg_replace('/^(.*?)\.(.*?)$/', "$1", str_replace($this->dataDir, '', $matches[0]));
339408
340- return $id;
409+ return 0;
341410 }
342411
343412 private function getUserData(): ?\stdClass {
@@ -47,7 +47,7 @@
4747 protected int $dataDuration = 1800; // 30分
4848
4949 public function __construct(string $dataDir) {
50- $this->dataDir = ROOT.'/data/cache/openprovider/'.$dataDir.'/';
50+ $this->dataDir = ROOT.'/data/cache/'.$dataDir.'/';
5151 }
5252
5353 /**
@@ -58,6 +58,7 @@
5858 * @return Result
5959 */
6060 public function set(string $type, array $data): Result {
61+ if (!$this->isSafeName($type)) return Result::Error('エラー:不正なキャッシュ名。');
6162 if (!file_exists($this->dataDir)) {
6263 if (!mkdir($this->dataDir, 0755))
6364 return Result::Error('エラー:ユーザーのアイコンディレクトリの作成に失敗。');
@@ -77,6 +78,7 @@
7778 * @return array
7879 */
7980 public function get(string $type): array {
81+ if (!$this->isSafeName($type)) return [];
8082 if (!file_exists("{$this->dataDir}{$type}.json")) return [];
8183
8284 $content = file_get_contents("{$this->dataDir}{$type}.json");
@@ -95,6 +97,7 @@
9597 * @return Result
9698 */
9799 public function murder(string $name): Result {
100+ if (!$this->isSafeName($name)) return Result::Error('エラー:不正なキャッシュ名。');
98101 if (!file_exists("{$this->dataDir}{$name}.json")) return Result::Error('キャッシュファイルが存在しない。');
99102 if (!unlink("{$this->dataDir}{$name}.json")) return Result::Error('キャッシュファイルの削除に失敗。');
100103 return Result::Success();
@@ -114,5 +117,17 @@
114117 && (isset($cache[$dataPoint])
115118 && $cache[$dataPoint] === $data)
116119 && (isset($cache['lifespan']) && time() < ($cache['lifespan'] + $this->dataDuration)));
120+ }
121+
122+ ///////
123+
124+ /**
125+ * キャッシュ名はファイル名になるので、英数字・「.」「_」「-」だけを許可する(「..」は不可)
126+ *
127+ * @param string $name キャッシュ名
128+ * @return bool 安全ならtrue
129+ */
130+ private function isSafeName(string $name): bool {
131+ return preg_match('/^[A-Za-z0-9._-]{1,200}$/', $name) === 1 && !str_contains($name, '..');
117132 }
118133 }
@@ -64,6 +64,8 @@
6464 private string $token = '';
6565 private string $referer = '';
6666 private string $authType = '';
67+ private int $maxResponseSize = 10485760; // 10 MiB
68+ private bool $blockPrivate = true;
6769
6870 // デバッグ用
6971 private string $DEBUG_REQ_HDR = '';
@@ -316,12 +318,35 @@
316318 }
317319
318320 /**
321+ * レスポンスの最大バイト数を設定する
322+ *
323+ * @param int $bytes 最大バイト数
324+ * @return Curl このインスタンス(メソッドチェーン用)
325+ */
326+ public function setMaxResponseSize(int $bytes): Curl {
327+ $this->maxResponseSize = $bytes;
328+ return $this;
329+ }
330+
331+ /**
332+ * ローカル・プライベートアドレスへの接続を拒否するかどうかを設定する(デフォルトはtrue)
333+ * 有効の場合、名前解決は1回だけ行い、確認したIPに直接接続する(DNSリバインディング対策)
334+ *
335+ * @param bool $block 拒否するかどうか
336+ * @return Curl このインスタンス(メソッドチェーン用)
337+ */
338+ public function setBlockPrivateAddresses(bool $block): Curl {
339+ $this->blockPrivate = $block;
340+ return $this;
341+ }
342+
343+ /**
319344 * リクエストを実行する
320345 *
321346 * @return Result 成功または失敗
322347 */
323348 public function execute(): Result {
324- if (!CURL_ENABLED) return Result::Error('エラー:認証システムは無効です。');
349+ if (!CURL_ENABLED) return Result::Error('エラー:Curlは無効です。');
325350 if (empty($this->url)) {
326351 $this->responseError = 'URLがありません';
327352 return Result::Error($this->responseError);
@@ -355,14 +380,38 @@
355380 fwrite($this->stderr, "* 接続中: {$currentUrl}\n");
356381 }
357382
358- $uri = new Uri($currentUrl);
359- if (!$uri) {
383+ $uri = preg_match('/[\x00-\x20\x7f]/', $currentUrl) ? null : Uri::parse($currentUrl);
384+ if (!$uri || !$uri->getHost()) {
360385 $this->responseError = "無効なURL: {$currentUrl}";
386+ $this->method = $originalMethod;
361387 return Result::Error($this->responseError);
362388 }
363389
364- $scheme = $uri->getScheme() ?? 'http';
390+ $scheme = strtolower($uri->getScheme() ?? 'http');
391+ if ($scheme !== 'http' && $scheme !== 'https') {
392+ $this->responseError = "未対応のスキーム: {$scheme}";
393+ $this->method = $originalMethod;
394+ return Result::Error($this->responseError);
395+ }
396+ $host = $uri->getHost();
365397 $port = $uri->getPort() ?? ($scheme === 'https' ? 443 : 80);
398+
399+ // 名前解決は1回だけ行い、確認したIPに直接接続する(DNSリバインディング対策)
400+ $connectHost = $host;
401+ if ($this->blockPrivate) {
402+ $ip = $this->resolvePublicIp($host);
403+ if ($ip === null) {
404+ $this->responseError = "非公開アドレスへの接続を拒否: {$host}";
405+ $this->method = $originalMethod;
406+ return Result::Error($this->responseError);
407+ }
408+ $connectHost = $ip;
409+ }
410+
411+ // 最初と違うオリジンへリダイレクトされた場合、認証情報とクッキーを送らない
412+ $origin = "{$scheme}://{$host}:{$port}";
413+ if (!isset($firstOrigin)) $firstOrigin = $origin;
414+ $sameOrigin = $origin === $firstOrigin;
366415 $path = $uri->getPath();
367416 if ($path === '') $path = '/';
368417 if (NULL !== $uri->getQuery()) {
@@ -371,7 +420,9 @@
371420
372421 // 認証
373422 $authHeader = '';
374- if (!empty($this->authType) && $this->authType === 'Basic') {
423+ if (!$sameOrigin) {
424+ // 送らない
425+ } else if (!empty($this->authType) && $this->authType === 'Basic') {
375426 assert_not_null($this->username);
376427 assert_not_null($this->password);
377428 $authHeader = "Authorization: Basic "
@@ -388,7 +439,7 @@
388439 if ($method === 'POST' || $method === 'PUT') {
389440 if (!empty($this->postRaw)) {
390441 $httpData = $this->postRaw;
391- } elseif (!empty($this->postFields)) {
442+ } else if (!empty($this->postFields)) {
392443 $httpData = http_build_query($this->postFields);
393444 if (!isset($this->headers['Content-Type'])) {
394445 $this->headers['Content-Type'] = 'application/x-www-form-urlencoded';
@@ -398,44 +449,44 @@
398449 }
399450
400451 // HTTPリクエストを構築
401- $accept = 'Accept: */*';
402- foreach ($this->headers as $h) {
403- if (str_contains($h, 'Accept:')) $accept = $h;
452+ // ['Name: value']形式と['Name' => 'value']形式の両方を受け付け、改行等を含むヘッダーは拒否する
453+ $hdrs = $this->normalizedHeaders();
454+ if ($hdrs === null || preg_match('/[\r\n\0]/', $authHeader === '' ? '' : rtrim($authHeader, "\r\n"))) {
455+ $this->responseError = '不正なヘッダー(改行等を含む)';
456+ $this->method = $originalMethod;
457+ return Result::Error($this->responseError);
404458 }
405459
460+ $accept = 'Accept: '.($hdrs['accept'][1] ?? '*/*');
461+ $userAgent = $hdrs['user-agent'][1] ?? $this->userAgent;
462+ // 自分で付けるヘッダーは重複させない
463+ unset($hdrs['accept'], $hdrs['user-agent'], $hdrs['host'], $hdrs['connection']);
464+ // 302/303でGETに変わった後に古いContent-Lengthを送らない
465+ if ($method !== 'POST' && $method !== 'PUT') unset($hdrs['content-length']);
466+ if (!$sameOrigin) unset($hdrs['authorization'], $hdrs['cookie']);
467+
406468 $request = "{$method} {$path} HTTP/1.1\r\n";
407- $request .= "Host: {$uri->getHost()}\r\n";
408- $request .= "User-Agent: {$this->userAgent}\r\n";
469+ $request .= "Host: {$host}\r\n";
470+ $request .= "User-Agent: {$userAgent}\r\n";
409471 $request .= "{$accept}\r\n";
410472 $request .= "Connection: close\r\n";
411473
474+ // 認証ヘッダー
412475 if (!empty($authHeader)) {
413476 $request .= $authHeader;
414477 }
415478
416479 // ヘッダーを追加
417- foreach ($this->headers as $name => $value) {
418- $request .= "{$name}: {$value}\r\n";
480+ $ext = $this->buildHeaderString($hdrs, $sameOrigin);
481+ if (!empty($ext)) {
482+ $request .= $ext;
419483 }
420484
421- // リファラーが設定されていれば追加
422- if (!empty($this->referer) && !isset($this->headers['Referer'])) {
423- $request .= "Referer: {$this->referer}\r\n";
424- }
425-
426- // クッキーヘッダーを追加
427- if (!empty($this->cookies) && !isset($this->headers['Cookie'])) {
428- $cookieStrings = [];
429- foreach ($this->cookies as $name => $value) {
430- $cookieStrings[] = $name.'='.urlencode($value);
431- }
432- $request .= 'Cookie: '.implode('; ', $cookieStrings)."\r\n";
433- }
434-
485+ // ヘッダー終了
435486 $request .= "\r\n";
436487
437488 // POSTデータを追加
438- if ($method === 'POST' || $method === 'PUT') {
489+ if ($httpData !== '') {
439490 $request .= $httpData;
440491 }
441492
@@ -452,7 +503,8 @@
452503 if ($scheme === 'https') {
453504 $sslOptions = [
454505 'verify_peer' => $this->verifySSL,
455- 'verify_peer_name' => $this->verifySSL
506+ 'verify_peer_name' => $this->verifySSL,
507+ 'peer_name' => trim($host, '[]'),
456508 ];
457509
458510 if (!empty($this->caInfoPath) && file_exists($this->caInfoPath)) {
@@ -462,7 +514,7 @@
462514 $context = stream_context_create(['ssl' => $sslOptions]);
463515
464516 $socket = @stream_socket_client(
465- "tls://{$uri->getHost()}:{$port}",
517+ "tls://{$connectHost}:{$port}",
466518 $errno,
467519 $errstr,
468520 $this->timeout,
@@ -470,7 +522,7 @@
470522 $context
471523 );
472524 } else {
473- $socket = @fsockopen($uri->getHost(), $port, $errno, $errstr, $this->timeout);
525+ $socket = @fsockopen($connectHost, $port, $errno, $errstr, $this->timeout);
474526 }
475527
476528 if (!$socket) {
@@ -478,6 +530,8 @@
478530 if ($this->verbose && $this->stderr) {
479531 fwrite($this->stderr, "* エラー: {$this->responseError}\n");
480532 }
533+
534+ $this->method = $originalMethod;
481535 return Result::Error($this->responseError);
482536 }
483537
@@ -493,10 +547,32 @@
493547 $body = '';
494548 $headersComplete = false;
495549
496- // ヘッダーとボディを分けて読み込む
497- while (!feof($socket)) $rawResponse .= fread($socket, 8192);
550+ // 読み込み(大きさと合計時間に上限を付ける)
551+ // 以前はタイムアウトしてもfeof()がfalseのままで、無限ループになった
552+ $deadline = microtime(true) + $this->timeout;
553+ $readError = '';
554+ while (!feof($socket)) {
555+ $chunk = fread($socket, 8192);
556+ if ($chunk !== false) $rawResponse .= $chunk;
557+ if (strlen($rawResponse) > $this->maxResponseSize) {
558+ $readError = "レスポンスが大き過ぎます(上限:{$this->maxResponseSize}バイト)";
559+ break;
560+ }
561+ if (stream_get_meta_data($socket)['timed_out'] || microtime(true) > $deadline) {
562+ $readError = "タイムアウト({$this->timeout}秒)";
563+ break;
564+ }
565+ if ($chunk === false) break;
566+ }
567+
498568 fclose($socket);
499569
570+ if ($readError !== '') {
571+ $this->responseError = $readError;
572+ $this->method = $originalMethod;
573+ return Result::Error($this->responseError);
574+ }
575+
500576 if ($rawResponse === '') {
501577 $this->responseError = 'リスポンスエラー';
502578 return Result::Error($this->responseError);
@@ -507,6 +583,7 @@
507583 // ヘッダーを解析
508584 $this->responseHeaders = [];
509585 $this->responseCode = 0;
586+ $redirectUrl = '';
510587
511588 foreach ($headerLines as $index => $header) {
512589 if ($index === 0) {
@@ -520,6 +597,10 @@
520597 $name = trim($name);
521598 $value = trim($value);
522599 $this->responseHeaders[$name] = $value;
600+
601+ // 以前はLocationの後のヘッダーで空文字に上書きされていた
602+ $red = $this->checkReds($name, $value, $uri);
603+ if ($red !== '') $redirectUrl = $red;
523604 }
524605 }
525606
@@ -541,20 +622,23 @@
541622 }
542623
543624 // リダイレクトが必要な場合
544- if (!empty($redirectUrl) && $redirectCount < $this->maxRedirects) {
625+ if ($redirectUrl !== '' && $redirectCount < $this->maxRedirects) {
545626 $currentUrl = $redirectUrl;
546627 $redirectCount++;
547628 $this->info['redirect_count'] = $redirectCount;
629+ $this->info['redirect_url'] = $redirectUrl;
548630
549631 // 302や303リダイレクトはGETにメソッドを変更
550- if ($this->responseCode == 302 || $this->responseCode == 303) {
632+ if (in_array($this->responseCode, [302, 303], true)) {
551633 $this->method = 'GET';
552634 $this->postRaw = '';
553635 $this->postFields = [];
554636 }
555- } else {
556- break;
637+
638+ continue;
557639 }
640+
641+ break;
558642 } while (true);
559643
560644 // リクエスト完了後、元のメソッドに戻す
@@ -621,6 +705,59 @@
621705 // 機能性メソッド
622706
623707 /**
708+ * ヘッダーを「小文字の名前 => [名前, 値]」の形にまとめ、改行等の注入を拒否する
709+ *
710+ * @return array|null ヘッダーの配列、不正な文字を含む場合はnull
711+ */
712+ private function normalizedHeaders(): ?array {
713+ $out = [];
714+ foreach ($this->headers as $name => $value) {
715+ if (is_int($name)) {
716+ if (!is_string($value) || !str_contains($value, ':')) continue;
717+ [$name, $value] = explode(':', $value, 2);
718+ }
719+ $name = trim((string)$name);
720+ $value = trim((string)$value);
721+ if (!preg_match('/^[A-Za-z0-9!#$%&\'*+.^_`|~-]+$/', $name)) return null;
722+ if (preg_match('/[\r\n\0]/', $value)) return null;
723+ $out[strtolower($name)] = [$name, $value];
724+ }
725+
726+ if (preg_match('/[\r\n\0]/', $this->referer)) return null;
727+ foreach ($this->cookies as $name => $value) {
728+ if (preg_match('/[\x00-\x20\x7f;=,]/', (string)$name)) return null;
729+ }
730+
731+ return $out;
732+ }
733+
734+ /**
735+ * ホスト名を解決し、全てのIPが公開アドレスの場合だけ接続先のIPを返す
736+ *
737+ * @param string $host ホスト名又はIPアドレス
738+ * @return string|null 接続先のIP(IPv6は[]付き)、拒否する場合はnull
739+ */
740+ private function resolvePublicIp(string $host): ?string {
741+ $host = trim($host, '[]');
742+ if (filter_var($host, FILTER_VALIDATE_IP)) {
743+ $ips = [$host];
744+ } else {
745+ $ips = @gethostbynamel($host) ?: [];
746+ foreach ((@dns_get_record($host, DNS_AAAA) ?: []) as $r) {
747+ if (isset($r['ipv6'])) $ips[] = $r['ipv6'];
748+ }
749+ }
750+ if (empty($ips)) return null;
751+
752+ foreach ($ips as $ip) {
753+ if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) return null;
754+ }
755+
756+ $v4 = array_values(array_filter($ips, fn($ip) => filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)));
757+ return $v4[0] ?? '['.$ips[0].']';
758+ }
759+
760+ /**
624761 * リダイレクトURLを確認する
625762 *
626763 * @param string $name ヘッダー名
@@ -628,26 +765,18 @@
628765 * @param string $currentUrl 現在のURL
629766 * @return string リダイレクトURL、リダイレクトがない場合は空文字
630767 */
631- private function checkReds(string $name, string $value, string $currentUrl): string {
632- $redirectUrl = '';
768+ private function checkReds(string $name, string $value, Uri $current): string {
769+ if (!$this->followRedirects) return '';
770+ if (strtolower($name) !== 'location') return '';
771+ if ($this->responseCode < 300 || $this->responseCode >= 400) return '';
772+ if ($value === '') return '';
633773
634- if ($this->followRedirects && (strtolower($name) === 'location'
635- && $this->responseCode >= 300 && $this->responseCode < 400)) {
636- $redirectUrl = $value;
637-
638- if (strpos($redirectUrl, 'http') !== 0) {
639- if ($redirectUrl[0] === '/') {
640- $uri = new Uri($currentUrl);
641- $redirectUrl = $uri->getScheme().'://'.$uri->getHost()
642- .(NULL !== $uri->getPort() ? ':'.$uri->getPort() : '')
643- .$redirectUrl;
644- } else {
645- $redirectUrl = dirname($currentUrl).'/'.$redirectUrl;
646- }
647- }
774+ // 相対URL(「/x」「x」「//host/x」等)はRFC 3986の通りに解決する
775+ try {
776+ return $current->resolve($value)->toString();
777+ } catch (\Throwable $e) {
778+ return '';
648779 }
649-
650- return $redirectUrl;
651780 }
652781
653782 /**
@@ -655,30 +784,30 @@
655784 *
656785 * @return string 構築されたヘッダー文字列
657786 */
658- private function buildHeaderString(): string {
659- $headers = [];
787+ private function buildHeaderString(array $hdrs, bool $sameOrigin = true): string {
788+ $request = '';
660789
661- // ユーザー指定のヘッダーを追加
662- foreach ($this->headers as $name => $value) {
663- $headers[] = "{$name}: {$value}";
790+ // ユーザー指定のヘッダーを追加(normalizedHeaders()で確認済み)
791+ foreach ($hdrs as [$name, $value]) {
792+ $request .= "{$name}: {$value}\r\n";
664793 }
665794
666795 // リファラーが設定されていれば追加
667- if (!empty($this->referer) && !isset($this->headers['Referer'])) {
668- $headers[] = "Referer: {$this->referer}";
796+ if (!empty($this->referer) && !isset($hdrs['referer'])) {
797+ $request .= "Referer: {$this->referer}\r\n";
669798 }
670799
671- // 必要に応じてクッキーヘッダーを追加
672- if (!empty($this->cookies) && !isset($this->headers['Cookie'])) {
800+ // 必要に応じてクッキーヘッダーを追加(別のオリジンには送らない)
801+ if ($sameOrigin && !empty($this->cookies) && !isset($hdrs['cookie'])) {
673802 $cookieStrings = [];
674803 foreach ($this->cookies as $name => $value) {
675804 $cookieStrings[] = $name.'='.urlencode($value);
676805 }
677806
678- $headers[] = 'Cookie: '.implode('; ', $cookieStrings);
807+ $request .= 'Cookie: '.implode('; ', $cookieStrings)."\r\n";
679808 }
680809
681- return implode("\r\n", $headers)."\r\n";
810+ return $request;
682811 }
683812
684813 /**
@@ -266,8 +266,8 @@
266266 file_put_contents($tmp, $message);
267267 $sig = shell_exec(
268268 "gpg --batch "
269- .($this->pgpPass ? "--passphrase {$this->pgpPass} " : '')
270- ."--detach-sign --armor {$tmp} 2>&1"
269+ .($this->pgpPass ? "--passphrase ".escapeshellarg($this->pgpPass)." " : '')
270+ ."--detach-sign --armor ".escapeshellarg($tmp)." 2>&1"
271271 );
272272 unlink($tmp);
273273 return $sig;
@@ -120,8 +120,9 @@
120120
121121 foreach ($lines as $line) {
122122 $i++;
123- $hasBR = substr($line, -1) === '\\';
124- $line = rtrim($line, " \t\r\n\\");
123+ $line = rtrim($line, "\r\n");
124+ $hasBR = str_ends_with($line, '\\');
125+ $line = rtrim($line, "\\");
125126
126127 // コメント
127128 // if (str_starts_with($line, '//')) {
@@ -52,9 +52,9 @@
5252 private string $ip = '';
5353 private int $resellerId = 0;
5454 private int $lastAuth = 0;
55- private string $BASEURL = 'https://api.openprovider.eu/v1beta';
56- // private string $BASEURL = DEBUG_MODE ? 'http://api.sandbox.openprovider.nl:8480/v1beta'
57- // : 'https://api.openprovider.eu/v1beta';
55+ // private string $BASEURL = 'https://api.openprovider.eu/v1beta';
56+ private string $BASEURL = DEBUG_MODE ? 'http://api.sandbox.openprovider.nl:8480/v1beta'
57+ : 'https://api.openprovider.eu/v1beta';
5858
5959 public function __construct() {
6060 if (!OPENPROVIDER_ENABLED) return;
@@ -64,6 +64,10 @@
6464 $this->cache = new Cache($this->cacheDir);
6565 }
6666
67+ public function deleteCache(string $name): void {
68+ $this->cache->murder($name);
69+ }
70+
6771 /**
6872 * トークンの受け取り。
6973 * このライブリリーを使ったら、一回「login()」を実行する事が必須となります。
@@ -71,12 +75,14 @@
7175 * @return Result 結果
7276 */
7377 public function login(): Result {
74- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
78+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
7579 $cacheName = 'login';
7680 $cache = $this->cache->get($cacheName);
77- if (time() < ($this->lastAuth + $this->tokenDuration)) {
81+
82+ // 以前は$lastAuthが0のまま比較していたので、キャッシュのトークンが使われず毎回パスワードを送っていた
83+ $this->lastAuth = (int)($cache['last_auth'] ?? 0);
84+ if (isset($cache['data']['token']) && time() < ($this->lastAuth + $this->tokenDuration)) {
7885 $this->token = $cache['data']['token'];
79- $this->lastAuth = $cache['last_auth'];
8086 return Result::Success('', $cache);
8187 }
8288
@@ -104,7 +110,7 @@
104110 $err = json_decode($body, true);
105111 assert_not_null($err, "エラーの受け取りに失敗。");
106112 assert($curl->getResponseCode() == 200, $err['desc']);
107- return Result::Error();
113+ return Result::Error($err);
108114 }
109115 assert_not_null($body, "返事ボディーは空です。");
110116
@@ -129,7 +135,7 @@
129135 * @return Result
130136 */
131137 public function generateLoginUrlSpamExpert(array $payload = []): Result {
132- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
138+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
133139
134140 $uri = '/spam-expert-generate-login-url';
135141 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -147,21 +153,21 @@
147153
148154 /**
149155 * ドメイン一覧。
150- * @todo テスト
151156 *
152157 * @return Result 結果。
153158 */
154159 public function listDomains(array $query = []): Result {
155- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
160+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
156161 $cacheName = 'listdomains';
157162 $cache = $this->cache->get($cacheName);
158163 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
159164
160- $curl = $this->setupCurl('/domains');
165+ $uri = "/domains?".http_build_query($query, '', '&', PHP_QUERY_RFC3986);
166+ $curl = $this->setupCurl($uri);
161167 $res = $this->curlResult($curl);
162- if (isset($res['data']['results'])) {
168+ if (isset($res->data['data']['results'])) {
163169 $this->cache->set($cacheName, $res->data);
164- return Result::Success('', $res['data']['results']);
170+ return Result::Success('', $res->data['data']['results']);
165171 }
166172
167173 return Result::Error('ドメインの確認に失敗。');
@@ -177,7 +183,7 @@
177183 * @return Result 結果。配列の場合:'activation_date'(リアルタイムで成功したのみ), 'auth_code'(TLDが対応している場合のみ), 'expiration_date', 'id', 'renewal_date', 'status'(ACT又はREQ)
178184 */
179185 public function createDomain(string $name, array $info, int $period = 1): Result {
180- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
186+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
181187 if (!isset($info['owner_handle']) || $info['owner_handle'] === '') $info['owner_handle'] = OPENPROVIDER['owner_handle'];
182188 if (!isset($info['admin_handle']) || $info['admin_handle'] === '') $info['admin_handle'] = OPENPROVIDER['owner_handle'];
183189 if (!isset($info['tech_handle']) || $info['tech_handle'] === '') $info['tech_handle'] = OPENPROVIDER['owner_handle'];
@@ -213,7 +219,8 @@
213219 'autorenew' => $autorenew,
214220 ];
215221
216- $curl = $this->setupCurl('/domains/', 'POST', $payload);
222+ $uri = '/domains/';
223+ $curl = $this->setupCurl($uri, 'POST', $payload);
217224 kys('TODO');
218225 $res = $this->curlResult($curl);
219226 if (isset($res['data'])) return Result::Success('', $res);
@@ -229,7 +236,7 @@
229236 * @return Result 結果。配列の場合:'domain'と'status'はいつでもあり、'reason', 'premium', 'is_premium'が多分あり、そして'price'が「with_price」が「true」の場合のみ。
230237 */
231238 public function checkDomainAvailable(array $domains, bool $with_price = false): Result {
232- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
239+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
233240 $domainList = [];
234241 foreach ($domains as $d) {
235242 $domain = explode('.', $d);
@@ -250,7 +257,8 @@
250257 'with_price' => $with_price,
251258 ];
252259
253- $curl = $this->setupCurl('/domains/check', 'POST', $payload);
260+ $uri = '/domains/check';
261+ $curl = $this->setupCurl($uri, 'POST', $payload);
254262 $res = $this->curlResult($curl);
255263 if (isset($res->data['data']['results'])) return Result::Success('', $res->data['data']['results']);
256264
@@ -265,9 +273,10 @@
265273 * @return Result 結果。
266274 */
267275 public function suggestDomainname(array $payload = []): Result {
268- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
276+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
269277
270- $curl = $this->setupCurl('/domains/suggest-name', 'POST', $payload);
278+ $uri = '/domains/suggest-name';
279+ $curl = $this->setupCurl($uri, 'POST', $payload);
271280 $res = $this->curlResult($curl);
272281 if (isset($res->data['data']['results'])) {
273282 return Result::Success('ドメインを勧められる事に成功。', $res->data['data']['results']);
@@ -284,9 +293,10 @@
284293 * @return Result 結果。
285294 */
286295 public function tradeDomainname(array $payload = []): Result {
287- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
296+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
288297
289- $curl = $this->setupCurl('/domains/trade', 'POST', $payload);
298+ $uri = '/domains/trade';
299+ $curl = $this->setupCurl($uri, 'POST', $payload);
290300 $res = $this->curlResult($curl);
291301 if (isset($res->data['data']['results'])) {
292302 $this->cache->murder('listdomains');
@@ -304,9 +314,10 @@
304314 * @return Result 結果。
305315 */
306316 public function transferDomainname(array $payload = []): Result {
307- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
317+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
308318
309- $curl = $this->setupCurl('/domains/transfer', 'POST', $payload);
319+ $uri = '/domains/transfer';
320+ $curl = $this->setupCurl($uri, 'POST', $payload);
310321 $res = $this->curlResult($curl);
311322 if (isset($res->data['data']['results'])) {
312323 $this->cache->murder('listdomains');
@@ -325,7 +336,7 @@
325336 * @return Result 結果。
326337 */
327338 public function getDomainname(int $id, array $payload = []): Result {
328- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
339+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
329340 $cacheName = "getdomain-{$id}";
330341 $cache = $this->cache->get($cacheName);
331342 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -349,7 +360,7 @@
349360 * @return Result
350361 */
351362 public function updateDomainname(int $id, array $payload = []): Result {
352- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
363+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
353364
354365 $uri = "/domains/{$id}";
355366 $curl = $this->setupCurl($uri, 'PUT', $payload);
@@ -372,7 +383,7 @@
372383 * @return Result
373384 */
374385 public function deleteDomainname(int $id, array $payload = []): Result {
375- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
386+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
376387
377388 $uri = "/domains/{$id}";
378389 $curl = $this->setupCurl($uri, 'DELETE', $payload);
@@ -395,7 +406,7 @@
395406 * @return Result 結果。
396407 */
397408 public function retryLastDomainnameOperation(int $id, array $payload = []): Result {
398- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
409+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
399410
400411 $curl = $this->setupCurl("/domains/{$id}/last-operation/restart", 'POST', $payload);
401412 $res = $this->curlResult($curl);
@@ -417,7 +428,7 @@
417428 * @return Result 結果。
418429 */
419430 public function renewDomainname(int $id, array $payload = []): Result {
420- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
431+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
421432
422433 $curl = $this->setupCurl("/domains/{$id}/renew", 'POST', $payload);
423434 $res = $this->curlResult($curl);
@@ -439,7 +450,7 @@
439450 * @return Result 結果。
440451 */
441452 public function restoreDomainname(int $id, array $payload = []): Result {
442- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
453+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
443454
444455 $curl = $this->setupCurl("/domains/{$id}/restore", 'POST', $payload);
445456 $res = $this->curlResult($curl);
@@ -461,7 +472,7 @@
461472 * @return Result 結果。
462473 */
463474 public function approveDomainnameTransfer(int $id, array $payload = []): Result {
464- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
475+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
465476
466477 $curl = $this->setupCurl("/domains/{$id}/transfer/approve", 'POST', $payload);
467478 $res = $this->curlResult($curl);
@@ -483,7 +494,7 @@
483494 * @return Result 結果。
484495 */
485496 public function sendDomainnameFoa1(int $id, array $payload = []): Result {
486- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
497+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
487498
488499 $curl = $this->setupCurl("/domains/{$id}/transfer/send-foa1", 'POST', $payload);
489500 $res = $this->curlResult($curl);
@@ -506,7 +517,7 @@
506517 * @return Result
507518 */
508519 public function getDomainAdditionalData(array $query): Result {
509- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
520+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
510521 $name = '';
511522 $ext = '';
512523 if (isset($query['domain.name'])) $name = $query['domain.name'];
@@ -537,7 +548,7 @@
537548 * @return Result
538549 */
539550 public function getDomainCustomerAdditionalData(array $query): Result {
540- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
551+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
541552 $name = '';
542553 $ext = '';
543554 if (isset($query['domain.name'])) $name = $query['domain.name'];
@@ -568,7 +579,7 @@
568579 * @return Result
569580 */
570581 public function getDomainPrices(array $query): Result {
571- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
582+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
572583 $name = '';
573584 $ext = '';
574585 if (isset($query['domain.name'])) $name = $query['domain.name'];
@@ -600,7 +611,7 @@
600611 * @return Result
601612 */
602613 public function getAuthCode(int $id, array $query = []): Result {
603- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
614+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
604615
605616 $uri = "/domains/{$id}/authcode?".http_build_query($query, '', '&', PHP_QUERY_RFC3986);
606617 $curl = $this->setupCurl($uri);
@@ -621,7 +632,7 @@
621632 * @return Result
622633 */
623634 public function resetAuthCode(int $id, array $payload = []): Result {
624- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
635+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
625636
626637 $uri = "/domains/{$id}/authcode/reset";
627638 $curl->setupCurl($uri, 'POST', $payload);
@@ -643,7 +654,7 @@
643654 * @return Result
644655 */
645656 public function listTlds(array $query = []): Result {
646- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
657+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
647658 $cacheName = 'listtlds';
648659 $cache = $this->cache->get($cacheName);
649660 if ($this->cache->resurrect($cache)) return Result::Success('', $cache);
@@ -670,7 +681,7 @@
670681 * @return Result
671682 */
672683 public function getTld(string $tld, array $query = []): Result {
673- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
684+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
674685 $cacheName = "gettld-{$tld}";
675686 $cache = $this->cache->get($cacheName);
676687 if ($this->cache->resurrect($cache)) return Result::Success('', $cache);
@@ -694,13 +705,12 @@
694705
695706 /**
696707 * 請求書一覧
697- * @todo テスト
698708 *
699709 * @param array $query 検索クエリー
700710 * @return Result
701711 */
702712 public function listInvoices(array $query = []): Result {
703- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
713+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
704714 $cacheName = 'listinvoices';
705715 $cache = $this->cache->get($cacheName);
706716 if ($this->cache->resurrect($cache)) return Result::Success('', $cache);
@@ -723,13 +733,12 @@
723733
724734 /**
725735 * 支払一覧
726- * @todo テスト
727736 *
728737 * @param array $query 検索クエリー
729738 * @return Result
730739 */
731740 public function listPayments(array $query = []): Result {
732- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
741+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
733742 $cacheName = 'listpayments';
734743 $cache = $this->cache->get($cacheName);
735744 if ($this->cache->resurrect($cache)) return Result::Success('', $cache);
@@ -752,13 +761,12 @@
752761
753762 /**
754763 * トランザクション一覧
755- * @todo テスト
756764 *
757765 * @param array $query 検索クエリー
758766 * @return Result
759767 */
760768 public function listTransactions(array $query = []): Result {
761- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
769+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
762770 $cacheName = 'listtransactions';
763771 $cache = $this->cache->get($cacheName);
764772 if ($this->cache->resurrect($cache)) return Result::Success('', $cache);
@@ -788,7 +796,7 @@
788796 * @return Result
789797 */
790798 public function createDomainToken(array $payload = []): Result {
791- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
799+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
792800
793801 $uri = '/dns/domain-token';
794802 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -810,7 +818,7 @@
810818 * @return Result
811819 */
812820 public function listNameservers(array $query = []): Result {
813- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
821+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
814822 $cacheName = 'listnameservers';
815823 $cache = $this->cache->get($cacheName);
816824 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -837,7 +845,7 @@
837845 * @return Result
838846 */
839847 public function createNameserver(array $payload = []): Result {
840- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
848+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
841849
842850 $uri = '/dns/nameservers';
843851 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -859,7 +867,7 @@
859867 * @return Result
860868 */
861869 public function getNameserver(string $domain, array $query = []): Result {
862- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
870+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
863871 $cacheName = "listnameserver-{$domain}";
864872 $cache = $this->cache->get($cacheName);
865873 if ($this->cache->resurrect($cache)) return Result::Success('', $cache);
@@ -887,7 +895,7 @@
887895 * @return Result
888896 */
889897 public function updateNameserver(string $domain, array $payload = []): Result {
890- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
898+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
891899
892900 $uri = "/dns/nameservers/{$domain}";
893901 $curl = $this->setupCurl($uri, 'PUT', $payload);
@@ -909,7 +917,7 @@
909917 * @return Result
910918 */
911919 public function deleteNameserver(string $domain): Result {
912- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
920+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
913921
914922 $uri = "/dns/nameservers/{$domain}";
915923 $curl = $this->setupCurl($uri, 'DELETE');
@@ -933,7 +941,7 @@
933941 * @return Result
934942 */
935943 public function listNsGroups(array $query = []): Result {
936- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
944+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
937945 $cacheName = 'listnsgroups';
938946 $cache = $this->cache->get($cacheName);
939947 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -960,7 +968,7 @@
960968 * @return Result
961969 */
962970 public function createNsGroup(array $payload = []): Result {
963- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
971+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
964972
965973 $uri = '/dns/nameservers/groups';
966974 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -982,7 +990,7 @@
982990 * @return Result
983991 */
984992 public function getNsGroup(string $nsGroup, array $query = []): Result {
985- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
993+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
986994 $cacheName = "listnsgroup-{$nsGroup}";
987995 $cache = $this->cache->get($cacheName);
988996 if ($this->cache->resurrect($cache)) return Result::Success('', $cache);
@@ -1010,7 +1018,7 @@
10101018 * @return Result
10111019 */
10121020 public function updateNsGroup(string $nsGroup, array $payload = []): Result {
1013- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1021+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
10141022
10151023 $uri = "/dns/nameservers/groups/{$nsGroup}";
10161024 $curl = $this->setupCurl($uri, 'PUT', $payload);
@@ -1032,7 +1040,7 @@
10321040 * @return Result
10331041 */
10341042 public function deleteNsGroup(string $nsGroup): Result {
1035- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1043+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
10361044
10371045 $uri = "/dns/nameservers/groups/{$nsGroup}";
10381046 $curl = $this->setupCurl($uri, 'DELETE');
@@ -1056,7 +1064,7 @@
10561064 * @return Result
10571065 */
10581066 public function listDnsTemplate(array $query = []): Result {
1059- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1067+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
10601068 $cacheName = 'listdnstemplates';
10611069 $cache = $this->cache->get($cacheName);
10621070 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -1082,7 +1090,7 @@
10821090 * @return Result
10831091 */
10841092 public function createDnsTemplate(array $payload = []): Result {
1085- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1093+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
10861094
10871095 $uri = '/dns/templates';
10881096 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -1103,7 +1111,7 @@
11031111 * @return Result
11041112 */
11051113 public function getDnsTemplate(int $id): Result {
1106- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1114+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
11071115 $cacheName = "getdnstemplate-{$id}";
11081116 $cache = $this->cache->get($cacheName);
11091117 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -1127,7 +1135,7 @@
11271135 * @return Result
11281136 */
11291137 public function deleteDnsTemplate(int $id): Result {
1130- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1138+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
11311139
11321140 $uri = "/dns/templates/{$id}";
11331141 $curl = $this->setupCurl($uri, 'DELETE');
@@ -1150,7 +1158,7 @@
11501158 * @return Result
11511159 */
11521160 public function listDnsZones(array $query = []): Result {
1153- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1161+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
11541162 $cacheName = 'listdnszones';
11551163 $cache = $this->cache->get($cacheName);
11561164 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -1177,7 +1185,7 @@
11771185 * @return Result
11781186 */
11791187 public function createDnsZone(array $payload = []): Result {
1180- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1188+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
11811189
11821190 $uri = '/dns/zones';
11831191 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -1198,7 +1206,7 @@
11981206 * @return Result
11991207 */
12001208 public function getDnsZone(string $domain, array $query = []): Result {
1201- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1209+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
12021210 $cacheName = "getdnszone-{$domain}";
12031211 $cache = $this->cache->get($cacheName);
12041212 if ($this->cache->resurrect($cache)) return Result::Success('', $cache);
@@ -1226,7 +1234,7 @@
12261234 * @return Result
12271235 */
12281236 public function updateDnsZone(string $domain, array $payload = []): Result {
1229- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1237+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
12301238
12311239 $uri = "/dns/zones/{$domain}";
12321240 $curl = $this->setupCurl($uri, 'PUT', $payload);
@@ -1248,7 +1256,7 @@
12481256 * @return Result
12491257 */
12501258 public function deleteDnsZone(string $domain): Result {
1251- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1259+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
12521260
12531261 $uri = "/dns/zones/{$domain}";
12541262 $curl = $this->setupCurl($uri, 'DELETE');
@@ -1271,7 +1279,7 @@
12711279 * @return Result
12721280 */
12731281 public function listZoneRecords(string $domain): Result {
1274- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1282+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
12751283 $cacheName = "getzonerecords-{$domain}";
12761284 $cache = $this->cache->get($cacheName);
12771285 if ($this->cache->resurrect($cache)) return Result::Success('', $cache);
@@ -1296,7 +1304,7 @@
12961304 * @return Result
12971305 */
12981306 public function getEasydmarc(string $domain): Result {
1299- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1307+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
13001308 $cacheName = "listeasydmarc-{$domain}";
13011309 $cache = $this->cache->get($cacheName);
13021310 if ($this->cache->resurrect($cache)) return Result::Success('', $cache);
@@ -1325,7 +1333,7 @@
13251333 * @return Result
13261334 */
13271335 public function createEasydmarc(string $domain): Result {
1328- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1336+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
13291337
13301338 $parts = explode('.', $domain, 2);
13311339 assert_exists($parts[0]);
@@ -1351,7 +1359,7 @@
13511359 * @return Result
13521360 */
13531361 public function listEasydmarcs(array $query = []): Result {
1354- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1362+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
13551363 $cacheName = 'listeasydmarcs';
13561364 $cache = $this->cache->get($cacheName);
13571365 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -1386,7 +1394,7 @@
13861394 * @return Result
13871395 */
13881396 public function deleteEasydmarc(int $orderId): Result {
1389- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1397+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
13901398
13911399 $uri = "/dns/easydmarcs/{$domain}";
13921400 $curl = $this->setupCurl($uri, 'DELETE');
@@ -1409,7 +1417,7 @@
14091417 * @return Result
14101418 */
14111419 public function retryEasydmarc(int $orderId): Result {
1412- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1420+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
14131421
14141422 $uri = "/dns/easydmarcs/{$domain}/retry";
14151423 $curl = $this->setupCurl($uri, 'POST');
@@ -1432,7 +1440,7 @@
14321440 * @return Result
14331441 */
14341442 public function getEasydmarcSso(int $orderId): Result {
1435- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1443+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
14361444 $cacheName = "geteasydmarcssso-{$orderId}";
14371445 $cache = $this->cache->get($cacheName);
14381446 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -1462,7 +1470,7 @@
14621470 * @return Result
14631471 */
14641472 public function listEmails(array $query = []): Result {
1465- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1473+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
14661474 $cacheName = 'listemails';
14671475 $cache = $this->cache->get($cacheName);
14681476 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -1488,7 +1496,7 @@
14881496 * @return Result
14891497 */
14901498 public function createEmail(array $payload = []): Result {
1491- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1499+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
14921500
14931501 $uri = '/emails';
14941502 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -1510,7 +1518,7 @@
15101518 * @return Result
15111519 */
15121520 public function updateEmail(int $id, array $payload = []): Result {
1513- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1521+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
15141522
15151523 $uri = "/emails/{$id}";
15161524 $curl = $this->setupCurl($uri, 'PUT', $payload);
@@ -1531,7 +1539,7 @@
15311539 * @return Result
15321540 */
15331541 public function deleteEmail(int $id): Result {
1534- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1542+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
15351543
15361544 $uri = "/emails/{$id}";
15371545 $curl = $this->setupCurl($uri, 'DELETE');
@@ -1554,7 +1562,7 @@
15541562 * @return Result
15551563 */
15561564 public function listLicenses(array $query = []): Result {
1557- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1565+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
15581566 $cacheName = 'listlicenses';
15591567 $cache = $this->cache->get($cacheName);
15601568 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -1581,7 +1589,7 @@
15811589 * @return Result
15821590 */
15831591 public function resetLicenseHwid(string $product, int $keyId): Result {
1584- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1592+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
15851593
15861594 $payload = ['key_id' => $keyId, 'product' => $product];
15871595 $uri = "/licenses/hwids/reset/{$product}/{$keyId}";
@@ -1603,7 +1611,7 @@
16031611 * @return Result
16041612 */
16051613 public function listLicenseItems(array $query = []): Result {
1606- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1614+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
16071615 $cacheName = 'listitems';
16081616 $cache = $this->cache->get($cacheName);
16091617 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -1629,7 +1637,7 @@
16291637 * @return Result
16301638 */
16311639 public function listPleskLicenses(array $query = []): Result {
1632- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1640+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
16331641 $cacheName = 'listplesklicenses';
16341642 $cache = $this->cache->get($cacheName);
16351643 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -1655,7 +1663,7 @@
16551663 * @return Result
16561664 */
16571665 public function createPleskLicense(array $payload): Result {
1658- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1666+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
16591667
16601668 $uri = "/licenses/plesk";
16611669 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -1677,7 +1685,7 @@
16771685 * @return Result
16781686 */
16791687 public function getPleskKey(int $keyId): Result {
1680- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1688+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
16811689 $cacheName = "getpleskkey-{$keyId}";
16821690 $cache = $this->cache->get($cacheName);
16831691 if ($this->cache->resurrect($cache)) return Result::Success('', $cache);
@@ -1702,7 +1710,7 @@
17021710 * @return Result
17031711 */
17041712 public function getPleskLicense(int $keyId): Result {
1705- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1713+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
17061714 $cacheName = "getplesklicense-{$keyId}";
17071715 $cache = $this->cache->get($cacheName);
17081716 if ($this->cache->resurrect($cache)) return Result::Success('', $cache);
@@ -1728,7 +1736,7 @@
17281736 * @return Result
17291737 */
17301738 public function updatePleskLicense(int $keyId, array $payload = []): Result {
1731- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1739+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
17321740
17331741 $uri = "/licenses/plesk/{$keyId}";
17341742 $curl = $this->setupCurl($uri, 'PUT', $payload);
@@ -1752,7 +1760,7 @@
17521760 * @return Result
17531761 */
17541762 public function deletePleskLicense(int $keyId): Result {
1755- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1763+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
17561764
17571765 $uri = "/licenses/plesk/{$keyId}";
17581766 $curl = $this->setupCurl($uri, 'DELETE');
@@ -1780,7 +1788,7 @@
17801788 * @return Result
17811789 */
17821790 public function listContacts(array $query = []): Result {
1783- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1791+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
17841792 $cacheName = 'listcontacts';
17851793 $cache = $this->cache->get($cacheName);
17861794 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -1806,7 +1814,7 @@
18061814 * @return Result
18071815 */
18081816 public function createContact(array $payload): Result {
1809- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1817+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
18101818
18111819 $uri = '/contacts';
18121820 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -1828,7 +1836,7 @@
18281836 * @return Result
18291837 */
18301838 public function getContact(int $id, bool $withAdditionalData = false): Result {
1831- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1839+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
18321840 $cacheName = "getcontact-{$id}";
18331841 $cache = $this->cache->get($cacheName);
18341842 if ($this->cache->resurrect($cache, $id, 'id')) return Result::Success('', $cache);
@@ -1856,7 +1864,7 @@
18561864 * @return Result
18571865 */
18581866 public function updateContact(int $id, array $payload = []): Result {
1859- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1867+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
18601868
18611869 $uri = "/contacts/{$id}";
18621870 $curl = $this->setupCurl($uri, 'PUT', $payload);
@@ -1878,7 +1886,7 @@
18781886 * @return Result
18791887 */
18801888 public function deleteContact(int $id): Result {
1881- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1889+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
18821890
18831891 $uri = "/contacts/{$id}";
18841892 $curl = $this->setupCurl($uri, 'DELETE');
@@ -1901,7 +1909,7 @@
19011909 * @return Result
19021910 */
19031911 public function listCustomers(array $query = []): Result {
1904- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1912+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
19051913 $cacheName = 'listcustomers';
19061914 $cache = $this->cache->get($cacheName);
19071915 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -1926,7 +1934,7 @@
19261934 * @return Result
19271935 */
19281936 public function createCustomer(array $payload): Result {
1929- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1937+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
19301938
19311939 $uri = '/customers';
19321940 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -1947,7 +1955,7 @@
19471955 * @return Result
19481956 */
19491957 public function getCustomer(string $handle, bool $withAdditionalData = false): Result {
1950- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1958+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
19511959 $cacheName = "getcustomer-{$handle}";
19521960 $cache = $this->cache->get($cacheName);
19531961 if ($this->cache->resurrect($cache, $handle, 'handle')) return Result::Success('', $cache);
@@ -1975,7 +1983,7 @@
19751983 * @return Result
19761984 */
19771985 public function updateCustomer(string $handle, array $payload = []): Result {
1978- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
1986+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
19791987
19801988 $uri = "/customers/{$handle}";
19811989 $curl = $this->setupCurl($uri, 'PUT', $payload);
@@ -1996,7 +2004,7 @@
19962004 * @return Result
19972005 */
19982006 public function deleteCustomer(string $handle): Result {
1999- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2007+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
20002008
20012009 $uri = "/customers/{$handle}";
20022010 $curl = $this->setupCurl($uri, 'DELETE');
@@ -2020,7 +2028,7 @@
20202028 * @return Result
20212029 */
20222030 public function listEmailVerifications(array $query = []): Result {
2023- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2031+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
20242032 $cacheName = 'listemailverifications';
20252033 $cache = $this->cache->get($cacheName);
20262034 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -2046,7 +2054,7 @@
20462054 * @return Result
20472055 */
20482056 public function restartEmailVerifications(string $email, string $language = '', string $tag = ''): Result {
2049- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2057+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
20502058
20512059 $uri = '/customers/verifications/emails/restart';
20522060 $curl = $this->setupCurl($uri, 'POST', ['email' => $email, 'language' => $language, 'tag' => $tag]);
@@ -2070,7 +2078,7 @@
20702078 * @return Result
20712079 */
20722080 public function startEmailVerifications(string $email, string $handle = '', string $language = '', string $tag = ''): Result {
2073- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2081+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
20742082
20752083 $uri = '/customers/verifications/emails/start';
20762084 $curl = $this->setupCurl($uri, 'POST', ['email' => $email, 'handle' => $handle, 'language' => $language, 'tag' => $tag]);
@@ -2087,13 +2095,12 @@
20872095
20882096 /**
20892097 * リセラーの受け取り
2090- * @todo テスト
20912098 *
20922099 * @param array $query 検索クエリー
20932100 * @return Result
20942101 */
20952102 public function getReseller(array $query = []): Result {
2096- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2103+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
20972104 $cacheName = 'getreseller';
20982105 $cache = $this->cache->get($cacheName);
20992106 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -2120,7 +2127,7 @@
21202127 * @return Result
21212128 */
21222129 public function updateReseller(int $id, array $payload = []): Result {
2123- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2130+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
21242131
21252132 $uri = "/resellers/{$id}";
21262133 $curl = $this->setupCurl($uri, 'PUT', $payload);
@@ -2142,7 +2149,7 @@
21422149 * @return Result
21432150 */
21442151 public function listSettings(): Result {
2145- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2152+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
21462153 $cacheName = 'listsettings';
21472154 $cache = $this->cache->get($cacheName);
21482155 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -2163,12 +2170,11 @@
21632170
21642171 /**
21652172 * 統計の受け取り
2166- * @todo テスト
21672173 *
21682174 * @return Result
21692175 */
21702176 public function listStatistics(array $query = []): Result {
2171- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2177+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
21722178 $cacheName = 'liststatistics';
21732179 $cache = $this->cache->get($cacheName);
21742180 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -2195,7 +2201,7 @@
21952201 * @return Result
21962202 */
21972203 public function listTags(array $query = []): Result {
2198- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2204+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
21992205 $cacheName = 'listtags';
22002206 $cache = $this->cache->get($cacheName);
22012207 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -2221,7 +2227,7 @@
22212227 * @return Result
22222228 */
22232229 public function createTag(array $payload = []): Result {
2224- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2230+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
22252231
22262232 $uri = '/tags';
22272233 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -2242,7 +2248,7 @@
22422248 * @return Result
22432249 */
22442250 public function deleteTag(array $payload = []): Result {
2245- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2251+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
22462252
22472253 $uri = "/tags/{$id}";
22482254 $curl = $this->setupCurl($uri, 'DELETE', $payload);
@@ -2267,7 +2273,7 @@
22672273 * @return Result
22682274 */
22692275 public function createSeDomain(array $payload): Result {
2270- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2276+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
22712277
22722278 $uri = '/spam-expert/domains';
22732279 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -2289,7 +2295,7 @@
22892295 * @return Result
22902296 */
22912297 public function getSeDomain(string $domain, array $query = []): Result {
2292- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2298+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
22932299 $cacheName = "getsedomain-{$domain}";
22942300 $cache = $this->cache->get($cacheName);
22952301 if ($this->cache->resurrect($cache, $domain, 'domain')) return Result::Success('', $cache);
@@ -2316,7 +2322,7 @@
23162322 * @return Result
23172323 */
23182324 public function updateSeDomain(string $domain, array $payload = []): Result {
2319- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2325+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
23202326
23212327 $uri = "/spam-expert/domains/{$domain}";
23222328 $curl = $this->setupCurl($uri, 'PUT', $payload);
@@ -2338,7 +2344,7 @@
23382344 * @return Result
23392345 */
23402346 public function deleteSeDomain(string $domain): Result {
2341- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2347+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
23422348
23432349 $uri = "/spam-expert/domains/{$domain}";
23442350 $curl = $this->setupCurl($uri, 'DELETE');
@@ -2364,7 +2370,7 @@
23642370 * @return Result
23652371 */
23662372 public function listApproverEmails(array $query = []): Result {
2367- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2373+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
23682374 $cacheName = 'listapproveremails';
23692375 $cache = $this->cache->get($cacheName);
23702376 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -2392,7 +2398,7 @@
23922398 * @return Result
23932399 */
23942400 public function createCsr(array $payload): Result {
2395- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2401+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
23962402
23972403 $uri = '/ssl/csr';
23982404 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -2412,7 +2418,7 @@
24122418 * @return Result
24132419 */
24142420 public function decodeCsr(string $csr): Result {
2415- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2421+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
24162422
24172423 $uri = '/ssl/csr/decode';
24182424 $curl = $this->setupCurl($uri, 'POST', ['csr' => $csr]);
@@ -2434,7 +2440,7 @@
24342440 * @return Result
24352441 */
24362442 public function listSslOrders(array $query = []): Result {
2437- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2443+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
24382444 $cacheName = 'listsslorders';
24392445 $cache = $this->cache->get($cacheName);
24402446 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -2460,7 +2466,7 @@
24602466 * @return Result
24612467 */
24622468 public function createSslOrder(array $payload): Result {
2463- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2469+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
24642470
24652471 $uri = '/ssl/orders';
24662472 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -2481,7 +2487,7 @@
24812487 * @return Result
24822488 */
24832489 public function getSslOrder(int $id): Result {
2484- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2490+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
24852491 $cacheName = "getsslorder-{$id}";
24862492 $cache = $this->cache->get($cacheName);
24872493 if ($this->cache->resurrect($cache)) return Result::Success('', $cache);
@@ -2508,7 +2514,7 @@
25082514 * @return Result
25092515 */
25102516 public function updateSslOrder(int $id, array $payload = []): Result {
2511- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2517+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
25122518
25132519 $uri = "/ssl/orders/{$id}";
25142520 $curl = $this->setupCurl($uri, 'PUT', $payload);
@@ -2530,7 +2536,7 @@
25302536 * @return Result
25312537 */
25322538 public function cancelSslOrder(int $id): Result {
2533- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2539+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
25342540
25352541 $payload = ['id' => $id];
25362542 $uri = "/ssl/orders/{$id}/cancel";
@@ -2554,7 +2560,7 @@
25542560 * @return Result
25552561 */
25562562 public function reissueSslOrder(int $id, array $payload = []): Result {
2557- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2563+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
25582564
25592565 $uri = "/ssl/orders/{$id}/reissue";
25602566 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -2577,7 +2583,7 @@
25772583 * @return Result
25782584 */
25792585 public function renewSslOrder(int $id, array $payload = []): Result {
2580- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2586+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
25812587
25822588 $uri = "/ssl/orders/{$id}/renew";
25832589 $curl = $this->setupCurl($uri, 'POST', $payload);
@@ -2602,7 +2608,7 @@
26022608 * @return Result
26032609 */
26042610 public function updateSslApproverEmail(int $id, string $approverEmail): Result {
2605- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2611+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
26062612
26072613 $uri = "/ssl/orders/{$id}/approver-email";
26082614 $curl = $this->setupCurl($uri, 'PUT', ['id' => $id, 'approver_email' => $approverEmail]);
@@ -2623,7 +2629,7 @@
26232629 * @return Result
26242630 */
26252631 public function resendSslApproverEmail(int $id): Result {
2626- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2632+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
26272633
26282634 $uri = "/ssl/orders/{$id}/approver-email/resend";
26292635 $curl = $this->setupCurl($uri, 'POST', ['id' => $id]);
@@ -2646,7 +2652,7 @@
26462652 * @return Result
26472653 */
26482654 public function createSslOtpToken(int $id): Result {
2649- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2655+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
26502656
26512657 $uri = "/ssl/orders/{$id}/otp-tokens";
26522658 $curl = $this->setupCurl($uri, 'POST');
@@ -2668,7 +2674,7 @@
26682674 * @return Result
26692675 */
26702676 public function listSslProducts(array $query = []): Result {
2671- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2677+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
26722678 $cacheName = 'listsslproducts';
26732679 $cache = $this->cache->get($cacheName);
26742680 if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache);
@@ -2694,7 +2700,7 @@
26942700 * @return Result
26952701 */
26962702 public function getSslProduct(int $id): Result {
2697- if (!OPENPROVIDER_ENABLED) return Result::error('エラー:OpenProviderは無効です。');
2703+ if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
26982704 $cacheName = "getsslproduct-{$id}";
26992705 $cache = $this->cache->get($cacheName);
27002706 if ($this->cache->resurrect($cache, $id, 'id')) return Result::Success('', $cache);
@@ -39,6 +39,7 @@
3939
4040 require_once __DIR__.'/../../../autoload.php';
4141
42+use Result;
4243 use Std\Lib\Tester;
4344 use Std\Lib\Openprovider;
4445
@@ -52,7 +53,7 @@
5253 $op = new OpenProvider();
5354 $res = $op->login();
5455
55- $expect = Res::Success();
56+ $expect = Result::Success();
5657 $test->assertEquals($expect, $res);
5758 });
5859
@@ -81,17 +82,16 @@
8182 'status' => 'free',
8283 ],
8384 ];
84- $test->assertEquals($expect, $res);
85+ $test->assertEquals($expect, $res->data);
8586 });
8687
8788 $test->it('ドメイン名(値段付き)があるかのうか確認出来るはず', function($test): void {
8889 $op = new OpenProvider();
8990 $op->login();
90- $res = isset($op->checkDomainAvailable(['076.moe'], true)[0]['price']['product']);
91+ $res = $op->checkDomainAvailable(['076.moe'], true)[0]['price']['product'];
9192
9293 $expect = true;
93- $expect = Res::Success();
94- $test->assertEquals($expect, $res);
94+ $test->assertEquals($expect, $res->isSuccess);
9595 });
9696 });
9797
変更 sync.sh
@@ -7,5 +7,5 @@
77 rsync ${RSYNCOPT} *.php ${SRV}:${HTTPHOME}
88 rsync ${RSYNCOPT} blog ${SRV}:${HTTPHOME}
99 #rsync ${RSYNCOPT} config ${SRV}:${HTTPHOME}
10-rsync ${RSYNCOPT} src/Site/Controller ${SRV}:${HTTPHOME}/src/Site
10+rsync ${RSYNCOPT} src ${SRV}:${HTTPHOME}
1111 rsync ${RSYNCOPT} view ${SRV}:${HTTPHOME}
変更 util.php
@@ -253,17 +253,21 @@
253253
254254 switch ($section) {
255255 case LogType::ActivityPub:
256- $logfile .= 'ap_log.text';
256+ $logfile .= 'ap_log.txt';
257257 $success = true;
258258 break;
259259 case LogType::Auth:
260- $logfile .= 'auth_log.text';
260+ $logfile .= 'auth_log.txt';
261261 $success = true;
262262 break;
263263 case LogType::Mailer:
264- $logfile .= 'mail_log.text';
264+ $logfile .= 'mail_log.txt';
265265 $success = true;
266266 break;
267+ case LogType::Image:
268+ $logfile .= 'img_log.txt';
269+ $success = true;
270+ break;
267271 default:
268272 break;
269273 }
@@ -307,6 +311,7 @@
307311 }
308312
309313 function assert_exists(mixed $assertion, Throwable|string|null $description = null): bool {
314+ if (!DEBUG_MODE) return false;
310315 if (ini_get('zend.assertions') < 1) trigger_error('assert機能性が無効です。有効するには、php.iniで「zend.assertions = 1」に設定して下さい。', E_USER_WARNING);
311316 if (isset($assertion)) return true;
312317 assert(false, $description ?? 'Assertion failed: value is not null');
@@ -314,6 +319,7 @@
314319 }
315320
316321 function assert_null(mixed $assertion, Throwable|string|null $description = null): bool {
322+ if (!DEBUG_MODE) return false;
317323 if (ini_get('zend.assertions') < 1) trigger_error('assert機能性が無効です。有効するには、php.iniで「zend.assertions = 1」に設定して下さい。', E_USER_WARNING);
318324 if (is_null($assertion)) return true;
319325 assert(false, $description ?? 'Assertion failed: value is not null');
@@ -321,6 +327,7 @@
321327 }
322328
323329 function assert_not_null(mixed $assertion, Throwable|string|null $description = null): bool {
330+ if (!DEBUG_MODE) return false;
324331 if (ini_get('zend.assertions') < 1) trigger_error('assert機能性が無効です。有効するには、php.iniで「zend.assertions = 1」に設定して下さい。', E_USER_WARNING);
325332 if (!is_null($assertion)) return true;
326333 assert(false, $description ?? 'Assertion failed: value is null');
@@ -328,6 +335,7 @@
328335 }
329336
330337 function assert_unless_success(Result $assertion, Throwable|string|null $description = null): bool {
338+ if (!DEBUG_MODE) return false;
331339 if (ini_get('zend.assertions') < 1) trigger_error('assert機能性が無効です。有効するには、php.iniで「zend.assertions = 1」に設定して下さい。', E_USER_WARNING);
332340 if ($assertion->isSuccess) return true;
333341 assert(false, $description ?? $assertion->message ?? 'Assertion failed: Result is not successful');
@@ -346,8 +354,10 @@
346354
347355 $gender = 'color: '.($userData->gender === Gender::MALE ? $male : ($userData->gender === Gender::FEMALE ? $female : $ungender)).';';
348356 $style = $userData->namecolor ?: ($userData->role !== Roles::BANNED ? $gender : 'color: '.$ban.';');
357+ $style = htmlspecialchars($style, ENT_QUOTES, 'UTF-8');
349358
350359 $showname = $userData->displayname ?: $userData->username;
360+ $showname = htmlspecialchars($showname, ENT_QUOTES, 'UTF-8');
351361
352362 $color = "<span style=\"{$style}\">{$showname}</span>";
353363 if ($userData->role & (Roles::ADMIN | Roles::STAFF))
@@ -363,12 +373,18 @@
363373 setcookie('csrf_token', $token, [
364374 'expires' => time() + 300, // 5分
365375 'path' => '/',
366- 'domain' => $_SERVER['SERVER_NAME'],
376+ 'domain' => $_SERVER['HTTP_HOST'],
367377 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
368378 'httponly' => true,
369379 'samesite' => 'Strict'
370380 ]);
371381 return $token;
382+}
383+
384+function safe_local_path(mixed $path, string $default = '/'): string {
385+ if (!is_string($path) || $path === '' || $path[0] !== '/') return $default;
386+ if (preg_match('/[\x00-\x1f\x7f]/', $path)) return $default;
387+ return '/'.ltrim($path, '/\\');
372388 }
373389
374390 function verify_csrf_token(string $token): bool {
@@ -1,5 +1,5 @@
11 {@ include(common/header) @}
2- <h1>{{ $description }}</h1>
2+ <h1>お帰り、{{ $data['data']['name']['full_name'] }}様</h1>
33 <ul>
44 <li>請求</li>
55 <ul>
@@ -23,4 +23,39 @@
2323 <li><a href="/openprovider/listdnszones">DNSゾーン一覧</a></li>
2424 </ul>
2525 </ul>
26+
27+ <div class="op-grid">
28+ <div class="op-grid-item">
29+ <div style="color: #00bb00; font-size: 24px;">利用可能な残高: € {{ $data['data']['balance'] }}</div>
30+ <div style="color: #777; font-size: 11px;">使用残高: € {{ $data['data']['reserved_balance'] }}</div>
31+ </div>
32+ <div class="op-grid-item">
33+ <a href="/openprovider/listdomains">ドメイン名</a>数:{{ $data['data']['statistics']['domain']['by_status']['ACT'] }}
34+ </div>
35+ <div class="op-grid-item">
36+ <a href="/openprovider/listsslorders">SSL証明書</a>数:{{ $data['data']['statistics']['ssl']['by_status']['ACT'] }}
37+ </div>
38+ <div class="op-grid-item">
39+ <a href="/openprovider/listlicenses">ライセンス</a>数:{{ $data['data']['statistics']['license']['total'] }}
40+ </div>
41+ </div>
42+
43+ <h2>間もなく更新が必要なドメイン名</h2>
44+ <table>
45+ <thead>
46+ <tr>
47+ <th>ドメイン名</th>
48+ <th>更新日</th>
49+ </tr>
50+ </thead>
51+ <tbody>
52+{@ foreach ($domain as $d) @}
53+ <tr>
54+{$ $dom = $d['domain']['name'].'.'.$d['domain']['extension'] $}
55+ <td><a href="/openprovider/getdomain/{{ $dom }}">{{ $dom }}</a></td>
56+ <td>{{ $d['renewal_date'] }}</td>
57+ </tr>
58+{@ endforeach @}
59+ </tbody>
60+ </table>
2661 {@ include(common/footer) @}