色々の修正
守矢諏訪子 <suwako@076.moe>、2026-11-10 20:56
コミット 2e4f443c83d25314dc4506fd6b91d5806320022b
・親 7208562
・ファイルを見る
10個のファイル、+529 -239
変更
README-JP.md
| @@ -203,7 +203,10 @@ | ||
| 203 | 203 | php cron.php help |
| 204 | 204 | ``` |
| 205 | 205 | |
| 206 | -タスクは `cron.php` で登録します。 | |
| 206 | +タスクは `cron.php` で登録します。組み込みのタスク: | |
| 207 | +* `apfederate`:新しいブログ投稿を ActivityPub のフォロワーに送る | |
| 208 | +* `apupdate`:プロフィール(アクター)の更新を ActivityPub のフォロワーに送る | |
| 209 | +* `authpurge`:7日以内にメールアドレスを確認しなかったアカウントを削除する(`AUTH_VERIFY_EMAIL` が有効の場合) | |
| 207 | 210 | |
| 208 | 211 | ## 必要な PHP モジュール |
| 209 | 212 | * php_gmp |
変更
README.md
| @@ -203,7 +203,10 @@ | ||
| 203 | 203 | php cron.php help |
| 204 | 204 | ``` |
| 205 | 205 | |
| 206 | -Tasks are registered in `cron.php`. | |
| 206 | +Tasks are registered in `cron.php`. Built-in tasks: | |
| 207 | +* `apfederate`: sends new blog posts to ActivityPub followers | |
| 208 | +* `apupdate`: sends profile (actor) updates to ActivityPub followers | |
| 209 | +* `authpurge`: deletes accounts whose email address was not verified within 7 days (when `AUTH_VERIFY_EMAIL` is enabled) | |
| 207 | 210 | |
| 208 | 211 | ## Required PHP module |
| 209 | 212 | * php_gmp |
変更
route.php
| @@ -73,6 +73,7 @@ | ||
| 73 | 73 | $routes[] = Route::add('GET', 'ap/outbox', Fediverse::class.'@apoutbox'); |
| 74 | 74 | $routes[] = Route::add('GET', 'ap/activities/create/{uuid}', |
| 75 | 75 | Fediverse::class.'@apactivity'); |
| 76 | + $routes[] = Route::add('GET', 'ap/objects/{uuid}', Fediverse::class.'@apobject'); | |
| 76 | 77 | $routes[] = Route::add('POST', 'ap/inbox', Fediverse::class.'@apinbox'); |
| 77 | 78 | $routes[] = Route::add('GET', 'ap/actor', Fediverse::class.'@apactor'); |
| 78 | 79 | } |
| @@ -110,6 +110,12 @@ | ||
| 110 | 110 | $ap = new Activitypub(); |
| 111 | 111 | |
| 112 | 112 | if (!$ap->verifyInboxSignature($input, $activity)) { |
| 113 | + // 削除されたアカウントのDeleteは鍵がもう取得出来ないので検証出来ない。拒否すると何日も再送されるので受け流す | |
| 114 | + if ($activity['type'] === 'Delete') { | |
| 115 | + header('HTTP/1.1 202 Accepted'); | |
| 116 | + echo json_encode(['status' => '無視しました']); | |
| 117 | + exit; | |
| 118 | + } | |
| 113 | 119 | header('HTTP/1.1 401 Unauthorized'); |
| 114 | 120 | echo json_encode(['error' => '署名の検証に失敗']); |
| 115 | 121 | exit; |
| @@ -137,7 +143,36 @@ | ||
| 137 | 143 | header('Content-Type: application/activity+json'); |
| 138 | 144 | $posts = $this->getPosts('/blog/', null); |
| 139 | 145 | $ap = new Activitypub($posts); |
| 140 | - echo $ap->getActivity($uuid); | |
| 146 | + $json = $ap->getActivity($uuid); | |
| 147 | + if ($json === '') { | |
| 148 | + header('HTTP/1.1 404 Not Found'); | |
| 149 | + echo json_encode(['error' => '見つかりません']); | |
| 150 | + exit; | |
| 151 | + } | |
| 152 | + echo $json; | |
| 153 | + exit; | |
| 154 | + } catch (\Exception $e) { | |
| 155 | + header('Content-Type: text/plain; charset=utf-8'); | |
| 156 | + echo 'フェディバースの作成に失敗: '.$e->getMessage(); | |
| 157 | + exit; | |
| 158 | + } | |
| 159 | + } | |
| 160 | + | |
| 161 | + /** | |
| 162 | + * @param array $params パラメータ配列 | |
| 163 | + * @return void | |
| 164 | + */ | |
| 165 | + public function apobject(array $params): void { | |
| 166 | + try { | |
| 167 | + header('Content-Type: application/activity+json'); | |
| 168 | + $ap = new Activitypub($this->getPosts('/blog/', null)); | |
| 169 | + $json = $ap->getObject($params['uuid'] ?? ''); | |
| 170 | + if ($json === '') { | |
| 171 | + header('HTTP/1.1 404 Not Found'); | |
| 172 | + echo json_encode(['error' => '見つかりません']); | |
| 173 | + exit; | |
| 174 | + } | |
| 175 | + echo $json; | |
| 141 | 176 | exit; |
| 142 | 177 | } catch (\Exception $e) { |
| 143 | 178 | header('Content-Type: text/plain; charset=utf-8'); |
| @@ -91,7 +91,7 @@ | ||
| 91 | 91 | */ |
| 92 | 92 | public function getActor(): string { |
| 93 | 93 | if (!ACTIVITYPUB_ENABLED) return ''; |
| 94 | - $pubkey = file_get_contents(FEDIINFO['pubkey']); | |
| 94 | + $pubkey = @file_get_contents(FEDIINFO['pubkey']); | |
| 95 | 95 | if ($pubkey === false) { |
| 96 | 96 | throw new \Exception('公開鍵の受取に失敗。パス:'.FEDIINFO['pubkey']); |
| 97 | 97 | } |
| @@ -142,19 +142,31 @@ | ||
| 142 | 142 | */ |
| 143 | 143 | public function getActivity(string $uuid): string { |
| 144 | 144 | if (!ACTIVITYPUB_ENABLED) return ''; |
| 145 | - $items = []; | |
| 145 | + $post = $this->findPublicPost($uuid); | |
| 146 | + if ($post === null) return ''; | |
| 146 | 147 | |
| 147 | - foreach ($this->posts as $post) { | |
| 148 | - if ($post['uuid'] != $uuid) continue; | |
| 149 | - $items = $this->buildCreate($post); | |
| 150 | - break; | |
| 151 | - } | |
| 148 | + return json_encode($this->buildCreate($post), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 149 | + } | |
| 152 | 150 | |
| 153 | - return json_encode($items, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 151 | + /** | |
| 152 | + * 特定のUUIDに対応するNote(Createの中身)を取得する | |
| 153 | + * 相手のサーバーがオブジェクトのIDを取得しに来た時の為 | |
| 154 | + * | |
| 155 | + * @param string $uuid 投稿のUUID | |
| 156 | + * @return string JSONエンコードされたNote、無ければ空文字 | |
| 157 | + */ | |
| 158 | + public function getObject(string $uuid): string { | |
| 159 | + if (!ACTIVITYPUB_ENABLED) return ''; | |
| 160 | + $post = $this->findPublicPost($uuid); | |
| 161 | + if ($post === null) return ''; | |
| 162 | + | |
| 163 | + $create = $this->buildCreate($post); | |
| 164 | + $note = ['@context' => $create['@context']] + $create['object']; | |
| 165 | + return json_encode($note, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 154 | 166 | } |
| 155 | 167 | |
| 156 | 168 | /** |
| 157 | - * アウトボックスデータを取得する | |
| 169 | + * アウトボックスデータを取得する(公開投稿のみ、新しい順) | |
| 158 | 170 | * |
| 159 | 171 | * @return string JSONエンコードされたアウトボックスデータ |
| 160 | 172 | */ |
| @@ -162,7 +174,7 @@ | ||
| 162 | 174 | if (!ACTIVITYPUB_ENABLED) return ''; |
| 163 | 175 | $items = []; |
| 164 | 176 | |
| 165 | - foreach ($this->posts as $post) $items[] = $this->buildCreate($post); | |
| 177 | + foreach (array_reverse($this->federatablePosts()) as $post) $items[] = $this->buildCreate($post); | |
| 166 | 178 | |
| 167 | 179 | $outbox = [ |
| 168 | 180 | '@context' => [ |
| @@ -207,20 +219,7 @@ | ||
| 207 | 219 | */ |
| 208 | 220 | public function getFollowers(): string { |
| 209 | 221 | if (!ACTIVITYPUB_ENABLED) return ''; |
| 210 | - $f = array_filter(explode("\n", file_get_contents(ROOT.'/data/followers.txt'))); | |
| 211 | - | |
| 212 | - $followers = [ | |
| 213 | - '@context' => [ | |
| 214 | - 'https://www.w3.org/ns/activitystreams', | |
| 215 | - 'https://w3id.org/security/v1', | |
| 216 | - ], | |
| 217 | - 'id' => "{$this->domain}/ap/followers", | |
| 218 | - 'type' => 'OrderredCollection', | |
| 219 | - 'totalItems' => count($f), | |
| 220 | - 'orderedItems' => $f, | |
| 221 | - ]; | |
| 222 | - | |
| 223 | - return json_encode($followers, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 222 | + return $this->collection('followers', $this->getFollowersList()); | |
| 224 | 223 | } |
| 225 | 224 | |
| 226 | 225 | /** |
| @@ -230,20 +229,29 @@ | ||
| 230 | 229 | */ |
| 231 | 230 | public function getFollowing(): string { |
| 232 | 231 | if (!ACTIVITYPUB_ENABLED) return ''; |
| 233 | - $f = array_filter(explode("\n", file_get_contents(ROOT.'/data/following.txt'))); | |
| 232 | + return $this->collection('following', $this->readList(ROOT.'/data/following.txt')); | |
| 233 | + } | |
| 234 | 234 | |
| 235 | - $following = [ | |
| 235 | + /** | |
| 236 | + * フォロワー・フォローのOrderedCollectionを作る | |
| 237 | + * | |
| 238 | + * @param string $name followers又はfollowing | |
| 239 | + * @param array $items アクターのURL | |
| 240 | + * @return string JSON | |
| 241 | + */ | |
| 242 | + private function collection(string $name, array $items): string { | |
| 243 | + $collection = [ | |
| 236 | 244 | '@context' => [ |
| 237 | 245 | 'https://www.w3.org/ns/activitystreams', |
| 238 | 246 | 'https://w3id.org/security/v1', |
| 239 | 247 | ], |
| 240 | - 'id' => "{$this->domain}/ap/following", | |
| 241 | - 'type' => 'OrderredCollection', | |
| 242 | - 'totalItems' => count($f), | |
| 243 | - 'orderedItems' => $f, | |
| 248 | + 'id' => "{$this->domain}/ap/{$name}", | |
| 249 | + 'type' => 'OrderedCollection', | |
| 250 | + 'totalItems' => count($items), | |
| 251 | + 'orderedItems' => $items, | |
| 244 | 252 | ]; |
| 245 | 253 | |
| 246 | - return json_encode($following, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 254 | + return json_encode($collection, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 247 | 255 | } |
| 248 | 256 | |
| 249 | 257 | /** |
| @@ -254,24 +262,57 @@ | ||
| 254 | 262 | */ |
| 255 | 263 | public function postInbox(array $activity): void { |
| 256 | 264 | if (!ACTIVITYPUB_ENABLED) return; |
| 257 | - switch ($activity['type']) { | |
| 258 | - case 'Follow': | |
| 259 | - $this->acceptFollower($activity); | |
| 260 | - break; | |
| 261 | - default: | |
| 262 | - header('HTTP/1.1 501 Not Implemented'); | |
| 263 | - header('Content-Type: application/activity+json'); | |
| 264 | - echo json_encode(['error' => '未対応なアクティビティタイプ: '.$activity['type']]); | |
| 265 | - exit; | |
| 266 | - } | |
| 265 | + [$code, $body] = $this->handleInbox($activity); | |
| 267 | 266 | |
| 268 | - header('HTTP/1.1 200 OK'); | |
| 267 | + $status = [200 => 'OK', 202 => 'Accepted', 400 => 'Bad Request', 500 => 'Internal Server Error'][$code] ?? ''; | |
| 268 | + header("HTTP/1.1 {$code} {$status}"); | |
| 269 | 269 | header('Content-Type: application/activity+json'); |
| 270 | - echo json_encode(['status' => 'OK'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 270 | + echo json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); | |
| 271 | 271 | exit; |
| 272 | 272 | } |
| 273 | 273 | |
| 274 | 274 | /** |
| 275 | + * 受付ボックスに届いたアクティビティを処理する(署名は検証済みの事) | |
| 276 | + * | |
| 277 | + * Follow:フォロワーに追加してAcceptを返す | |
| 278 | + * Undo(Follow):フォロワーから外す | |
| 279 | + * Delete(アクター自身):フォロワーから外す | |
| 280 | + * それ以外:何もせず202を返す(5xxを返すと相手のサーバーが何日も再送する) | |
| 281 | + * | |
| 282 | + * @param array $activity アクティビティ | |
| 283 | + * @return array [HTTPステータス, レスポンスの本文] | |
| 284 | + */ | |
| 285 | + public function handleInbox(array $activity): array { | |
| 286 | + $actor = $activity['actor'] ?? null; | |
| 287 | + if (is_array($actor)) $actor = $actor['id'] ?? null; | |
| 288 | + | |
| 289 | + switch ($activity['type'] ?? '') { | |
| 290 | + case 'Follow': | |
| 291 | + return $this->acceptFollower($activity); | |
| 292 | + | |
| 293 | + case 'Undo': | |
| 294 | + // 中身がIDだけの場合はFollowかどうか分からないので無視する(Likeの取り消し等でフォロワーを外さない様に) | |
| 295 | + $object = $activity['object'] ?? null; | |
| 296 | + $inner = is_array($object) ? ($object['actor'] ?? null) : null; | |
| 297 | + if (is_array($inner)) $inner = $inner['id'] ?? null; | |
| 298 | + if (is_array($object) && ($object['type'] ?? '') === 'Follow' && is_string($actor) && $inner === $actor) { | |
| 299 | + $this->removeFollower($actor); | |
| 300 | + return [200, ['status' => 'OK']]; | |
| 301 | + } | |
| 302 | + return [202, ['status' => '無視しました']]; | |
| 303 | + | |
| 304 | + case 'Delete': | |
| 305 | + $object = $activity['object'] ?? null; | |
| 306 | + if (is_array($object)) $object = $object['id'] ?? null; | |
| 307 | + if (is_string($actor) && $object === $actor) $this->removeFollower($actor); | |
| 308 | + return [202, ['status' => 'OK']]; | |
| 309 | + | |
| 310 | + default: | |
| 311 | + return [202, ['status' => '未対応なアクティビティタイプ: '.(string)($activity['type'] ?? '')]]; | |
| 312 | + } | |
| 313 | + } | |
| 314 | + | |
| 315 | + /** | |
| 275 | 316 | * アクタープロフィールの更新アクティビティを作成する |
| 276 | 317 | * |
| 277 | 318 | * @return string JSONエンコードされた更新アクティビティ |
| @@ -539,15 +580,20 @@ | ||
| 539 | 580 | * @param string $url 取得するURL |
| 540 | 581 | * @return array|null デコード済みのJSON、失敗した場合はnull |
| 541 | 582 | */ |
| 542 | - private function fetchRemoteJson(string $url): ?array { | |
| 583 | + protected function fetchRemoteJson(string $url): ?array { | |
| 543 | 584 | if (!$this->isSafeRemoteUrl($url)) { |
| 544 | 585 | logger(\LogType::ActivityPub, "安全ではないURLを拒否: {$url}"); |
| 545 | 586 | return null; |
| 546 | 587 | } |
| 547 | 588 | |
| 589 | + // 「Authorized fetch」のサーバーは署名の無いGETを拒否するので、秘密鍵があれば署名する | |
| 590 | + $headers = ['Accept: application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"']; | |
| 591 | + $priv = $this->privateKey(false); | |
| 592 | + if ($priv !== null) $headers = array_merge($headers, $this->signRequest($url, '', $priv, 'get') ?? []); | |
| 593 | + | |
| 548 | 594 | $curl = new Curl($url); |
| 549 | 595 | // リダイレクト先は検証されないので追わない / redirect targets aren't re-checked, so don't follow them |
| 550 | - $curl->setHeaders(['Accept: application/activity+json']) | |
| 596 | + $curl->setHeaders($headers) | |
| 551 | 597 | ->setFollowRedirects(false) |
| 552 | 598 | ->setTimeout(10) |
| 553 | 599 | ->setCaInfo('/etc/ssl/cert.pem'); |
| @@ -606,40 +652,45 @@ | ||
| 606 | 652 | } |
| 607 | 653 | |
| 608 | 654 | /** |
| 609 | - * POSTリクエストのヘッダーを作り、HTTP署名を付ける | |
| 610 | - * 署名対象:(request-target) host date digest(Mastodon等が必須とする組み合わせ) | |
| 655 | + * リクエストのヘッダーを作り、HTTP署名を付ける | |
| 656 | + * POST:(request-target) host date digest(Mastodon等が必須とする組み合わせ) | |
| 657 | + * GET:(request-target) host date | |
| 611 | 658 | * |
| 612 | 659 | * @param string $url 送信先のURL |
| 613 | - * @param string $body 送信する本文 | |
| 660 | + * @param string $body 送信する本文(GETは空) | |
| 614 | 661 | * @param \OpenSSLAsymmetricKey $priv 秘密鍵 |
| 662 | + * @param string $method post又はget | |
| 615 | 663 | * @return array|null 「名前: 値」のヘッダー、署名に失敗した場合はnull |
| 616 | 664 | */ |
| 617 | - protected function signRequest(string $url, string $body, \OpenSSLAsymmetricKey $priv): ?array { | |
| 665 | + protected function signRequest(string $url, string $body, \OpenSSLAsymmetricKey $priv, string $method = 'post'): ?array { | |
| 618 | 666 | $p = parse_url($url); |
| 619 | 667 | if (!$p || empty($p['host'])) return null; |
| 668 | + $method = strtolower($method); | |
| 620 | 669 | $target = ($p['path'] ?? '') === '' ? '/' : $p['path']; |
| 621 | 670 | if (isset($p['query'])) $target .= '?'.$p['query']; |
| 622 | 671 | |
| 623 | 672 | $headers = [ |
| 624 | 673 | 'Host' => $p['host'].(isset($p['port']) && (int)$p['port'] !== 443 ? ':'.$p['port'] : ''), |
| 625 | 674 | 'Date' => gmdate('D, d M Y H:i:s \G\M\T'), |
| 626 | - 'Digest' => 'SHA-256='.base64_encode(hash('sha256', $body, true)), | |
| 627 | - 'Content-Type' => 'application/activity+json', | |
| 628 | 675 | ]; |
| 676 | + $signed = ['(request-target)', 'host', 'date']; | |
| 677 | + $lines = ["(request-target): {$method} {$target}", "host: {$headers['Host']}", "date: {$headers['Date']}"]; | |
| 629 | 678 | |
| 630 | - $stringToSign = "(request-target): post {$target}\n" | |
| 631 | - ."host: {$headers['Host']}\n" | |
| 632 | - ."date: {$headers['Date']}\n" | |
| 633 | - ."digest: {$headers['Digest']}"; | |
| 679 | + if ($method === 'post') { | |
| 680 | + $headers['Digest'] = 'SHA-256='.base64_encode(hash('sha256', $body, true)); | |
| 681 | + $headers['Content-Type'] = 'application/activity+json'; | |
| 682 | + $signed[] = 'digest'; | |
| 683 | + $lines[] = "digest: {$headers['Digest']}"; | |
| 684 | + } | |
| 634 | 685 | |
| 635 | - if (!openssl_sign($stringToSign, $signature, $priv, OPENSSL_ALGO_SHA256)) { | |
| 686 | + if (!openssl_sign(implode("\n", $lines), $signature, $priv, OPENSSL_ALGO_SHA256)) { | |
| 636 | 687 | logger(LogType::ActivityPub, 'エラー:署名に失敗: '.openssl_error_string()); |
| 637 | 688 | return null; |
| 638 | 689 | } |
| 639 | 690 | |
| 640 | 691 | $headers['Signature'] = "keyId=\"{$this->domain}/ap/actor#main-key\"," |
| 641 | 692 | .'algorithm="rsa-sha256",' |
| 642 | - .'headers="(request-target) host date digest",' | |
| 693 | + .'headers="'.implode(' ', $signed).'",' | |
| 643 | 694 | .'signature="'.base64_encode($signature).'"'; |
| 644 | 695 | |
| 645 | 696 | return array_map(fn($k, $v) => "{$k}: {$v}", array_keys($headers), $headers); |
| @@ -648,16 +699,17 @@ | ||
| 648 | 699 | /** |
| 649 | 700 | * 秘密鍵を読み込む(1回だけ) |
| 650 | 701 | * |
| 702 | + * @param bool $log 失敗をログに書くか | |
| 651 | 703 | * @return \OpenSSLAsymmetricKey|null 失敗した場合はnull |
| 652 | 704 | */ |
| 653 | - private function privateKey(): ?\OpenSSLAsymmetricKey { | |
| 705 | + private function privateKey(bool $log = true): ?\OpenSSLAsymmetricKey { | |
| 654 | 706 | if ($this->privKey !== null) return $this->privKey; |
| 655 | 707 | |
| 656 | - $file = FEDIINFO['privkey']; | |
| 657 | - $pem = is_readable($file) ? file_get_contents($file) : false; | |
| 708 | + $file = FEDIINFO['privkey'] ?? ''; | |
| 709 | + $pem = is_string($file) && is_readable($file) ? file_get_contents($file) : false; | |
| 658 | 710 | $key = $pem === false ? false : openssl_pkey_get_private($pem); |
| 659 | 711 | if ($key === false) { |
| 660 | - logger(LogType::ActivityPub, "エラー:秘密鍵「{$file}」の読込に失敗"); | |
| 712 | + if ($log) logger(LogType::ActivityPub, "エラー:秘密鍵「{$file}」の読込に失敗"); | |
| 661 | 713 | return null; |
| 662 | 714 | } |
| 663 | 715 | |
| @@ -704,7 +756,7 @@ | ||
| 704 | 756 | $note['attachment'] = [ |
| 705 | 757 | [ |
| 706 | 758 | 'digestMultibase' => 'z'.base58btc_encode(hash_file('sha256', $imgpath, true)), |
| 707 | - 'mediaType' => mime_content_type($imgpath), | |
| 759 | + 'mediaType' => self::imageType($imgpath), | |
| 708 | 760 | 'type' => 'Image', |
| 709 | 761 | 'url' => "{$this->domain}/static/article/".rawurlencode($thumb), |
| 710 | 762 | ], |
| @@ -737,6 +789,41 @@ | ||
| 737 | 789 | } |
| 738 | 790 | |
| 739 | 791 | /** |
| 792 | + * UUIDで公開投稿を探す | |
| 793 | + * | |
| 794 | + * @param string $uuid 投稿のUUID | |
| 795 | + * @return array|null 投稿データ、無い(又は非公開)場合はnull | |
| 796 | + */ | |
| 797 | + private function findPublicPost(string $uuid): ?array { | |
| 798 | + if ($uuid === '') return null; | |
| 799 | + foreach ($this->federatablePosts() as $post) { | |
| 800 | + if ($post['uuid'] === $uuid) return $post; | |
| 801 | + } | |
| 802 | + return null; | |
| 803 | + } | |
| 804 | + | |
| 805 | + /** | |
| 806 | + * 画像のMIMEタイプ(fileinfoが無い環境では拡張子で判断する) | |
| 807 | + * | |
| 808 | + * @param string $path 画像のパス | |
| 809 | + * @return string | |
| 810 | + */ | |
| 811 | + private static function imageType(string $path): string { | |
| 812 | + if (function_exists('mime_content_type')) { | |
| 813 | + $type = @mime_content_type($path); | |
| 814 | + if (is_string($type) && $type !== '') return $type; | |
| 815 | + } | |
| 816 | + return match (strtolower(pathinfo($path, PATHINFO_EXTENSION))) { | |
| 817 | + 'png' => 'image/png', | |
| 818 | + 'gif' => 'image/gif', | |
| 819 | + 'webp' => 'image/webp', | |
| 820 | + 'avif' => 'image/avif', | |
| 821 | + 'svg' => 'image/svg+xml', | |
| 822 | + default => 'image/jpeg', | |
| 823 | + }; | |
| 824 | + } | |
| 825 | + | |
| 826 | + /** | |
| 740 | 827 | * 連合できる投稿(UUIDあり、公開、日付が過去)を古い順に返す |
| 741 | 828 | * |
| 742 | 829 | * @return array 投稿データの配列 |
| @@ -801,32 +888,23 @@ | ||
| 801 | 888 | * フォロワーを受け入れる |
| 802 | 889 | * |
| 803 | 890 | * @param array $activity フォローアクティビティデータ |
| 804 | - * @return void | |
| 891 | + * @return array [HTTPステータス, レスポンスの本文] | |
| 805 | 892 | */ |
| 806 | - private function acceptFollower(array $activity): void { | |
| 893 | + private function acceptFollower(array $activity): array { | |
| 807 | 894 | $followerActor = $activity['actor'] ?? null; |
| 808 | 895 | if (!is_string($followerActor) || !$this->isSafeRemoteUrl($followerActor)) { |
| 809 | - header('HTTP/1.1 400 Bad Request'); | |
| 810 | - header('Content-Type: application/activity+json'); | |
| 811 | - echo json_encode(['error' => 'アクターがない']); | |
| 812 | - exit; | |
| 896 | + return [400, ['error' => 'アクターがない']]; | |
| 813 | 897 | } |
| 814 | 898 | |
| 815 | 899 | $object = $activity['object'] ?? null; |
| 816 | 900 | if (is_array($object)) $object = $object['id'] ?? null; |
| 817 | 901 | if ($object !== "{$this->domain}/ap/actor") { |
| 818 | - header('HTTP/1.1 400 Bad Request'); | |
| 819 | - header('Content-Type: application/activity+json'); | |
| 820 | - echo json_encode(['error' => '不正なフォロー対象']); | |
| 821 | - exit; | |
| 902 | + return [400, ['error' => '不正なフォロー対象']]; | |
| 822 | 903 | } |
| 823 | 904 | |
| 824 | 905 | $inbox = $this->getInboxFromActor($followerActor); |
| 825 | 906 | if (!$inbox) { |
| 826 | - header('HTTP/1.1 500 Internal Server Error'); | |
| 827 | - header('Content-Type: application/activity+json'); | |
| 828 | - echo json_encode(['error' => 'フォロワーの受付ボックスの受取に失敗']); | |
| 829 | - exit; | |
| 907 | + return [500, ['error' => 'フォロワーの受付ボックスの受取に失敗']]; | |
| 830 | 908 | } |
| 831 | 909 | |
| 832 | 910 | $this->storeFollower($followerActor); |
| @@ -836,7 +914,7 @@ | ||
| 836 | 914 | 'https://www.w3.org/ns/activitystreams', |
| 837 | 915 | 'https://w3id.org/security/v1', |
| 838 | 916 | ], |
| 839 | - 'id' => "{$this->domain}/ap/activities/".uniqid(), | |
| 917 | + 'id' => "{$this->domain}/ap/activities/accept/".uuid(), | |
| 840 | 918 | 'type' => 'Accept', |
| 841 | 919 | 'actor' => "{$this->domain}/ap/actor", |
| 842 | 920 | 'object' => $activity, |
| @@ -845,6 +923,7 @@ | ||
| 845 | 923 | if (!$this->sendActivity($inbox, $accept)) { |
| 846 | 924 | logger(LogType::ActivityPub, "「{$followerActor}」へのAcceptの送信に失敗"); |
| 847 | 925 | } |
| 926 | + return [200, ['status' => 'OK']]; | |
| 848 | 927 | } |
| 849 | 928 | |
| 850 | 929 | /** |
| @@ -854,35 +933,89 @@ | ||
| 854 | 933 | * @return void |
| 855 | 934 | */ |
| 856 | 935 | private function storeFollower(string $followerActor): void { |
| 857 | - $file = ROOT.'/data/followers.txt'; | |
| 858 | - if (!file_exists($file)) { | |
| 859 | - touch($file); | |
| 860 | - chmod($file, 0644); | |
| 936 | + $this->modifyFollowers(function(array $list) use ($followerActor): array { | |
| 937 | + if (!in_array($followerActor, $list, true)) $list[] = $followerActor; | |
| 938 | + return $list; | |
| 939 | + }); | |
| 940 | + } | |
| 941 | + | |
| 942 | + /** | |
| 943 | + * フォロワーを外す | |
| 944 | + * | |
| 945 | + * @param string $followerActor フォロワーのアクターURL | |
| 946 | + * @return void | |
| 947 | + */ | |
| 948 | + private function removeFollower(string $followerActor): void { | |
| 949 | + $this->modifyFollowers(fn(array $list): array => array_values(array_filter($list, fn($a) => $a !== $followerActor))); | |
| 950 | + } | |
| 951 | + | |
| 952 | + /** | |
| 953 | + * フォロワーのファイルをロックして読み書きする(同時のFollowで消えない様に) | |
| 954 | + * | |
| 955 | + * @param callable $fn リストを受け取り、新しいリストを返す | |
| 956 | + * @return void | |
| 957 | + */ | |
| 958 | + private function modifyFollowers(callable $fn): void { | |
| 959 | + $file = $this->followersFile(); | |
| 960 | + $fp = @fopen($file, 'c+'); | |
| 961 | + if ($fp === false) { | |
| 962 | + logger(LogType::ActivityPub, "「{$file}」を開けません"); | |
| 963 | + return; | |
| 861 | 964 | } |
| 862 | 965 | |
| 863 | - $followers = $this->getFollowersList(); | |
| 864 | - if (!in_array($followerActor, $followers)) file_put_contents($file, "$followerActor\n", FILE_APPEND); | |
| 966 | + flock($fp, LOCK_EX); | |
| 967 | + $list = self::parseList((string)stream_get_contents($fp)); | |
| 968 | + $new = $fn($list); | |
| 969 | + if ($new !== $list) { | |
| 970 | + ftruncate($fp, 0); | |
| 971 | + rewind($fp); | |
| 972 | + fwrite($fp, empty($new) ? '' : implode("\n", $new)."\n"); | |
| 973 | + fflush($fp); | |
| 974 | + } | |
| 975 | + flock($fp, LOCK_UN); | |
| 976 | + fclose($fp); | |
| 977 | + @chmod($file, 0644); | |
| 865 | 978 | } |
| 866 | 979 | |
| 867 | 980 | /** |
| 981 | + * フォロワーを保存するファイル(テストで変えられる様に) | |
| 982 | + * | |
| 983 | + * @return string | |
| 984 | + */ | |
| 985 | + protected function followersFile(): string { | |
| 986 | + return ROOT.'/data/followers.txt'; | |
| 987 | + } | |
| 988 | + | |
| 989 | + /** | |
| 868 | 990 | * フォロワーのリストを配列として取得する |
| 869 | 991 | * |
| 870 | 992 | * @return array フォロワーのURLの配列 |
| 871 | 993 | */ |
| 872 | 994 | private function getFollowersList(): array { |
| 873 | - $file = ROOT.'/data/followers.txt'; | |
| 874 | - if (!is_file($file)) return []; | |
| 875 | - $lines = file($file, FILE_IGNORE_NEW_LINES) ?: []; | |
| 876 | - return array_values(array_filter(array_map('trim', $lines), fn($l) => $l !== '')); | |
| 995 | + return $this->readList($this->followersFile()); | |
| 877 | 996 | } |
| 878 | 997 | |
| 879 | 998 | /** |
| 999 | + * 1行に1つのURLのファイルを読む | |
| 1000 | + * | |
| 1001 | + * @param string $file ファイル | |
| 1002 | + * @return array 空行を除いた行 | |
| 1003 | + */ | |
| 1004 | + private function readList(string $file): array { | |
| 1005 | + return is_file($file) ? self::parseList((string)file_get_contents($file)) : []; | |
| 1006 | + } | |
| 1007 | + | |
| 1008 | + private static function parseList(string $text): array { | |
| 1009 | + return array_values(array_filter(array_map('trim', explode("\n", $text)), fn($l) => $l !== '')); | |
| 1010 | + } | |
| 1011 | + | |
| 1012 | + /** | |
| 880 | 1013 | * アクターのインボックスURLを取得する |
| 881 | 1014 | * |
| 882 | 1015 | * @param string $actor アクターのURL |
| 883 | 1016 | * @return string|null インボックスURL、取得に失敗した場合はnull |
| 884 | 1017 | */ |
| 885 | - private function getInboxFromActor(string $actor): ?string { | |
| 1018 | + protected function getInboxFromActor(string $actor): ?string { | |
| 886 | 1019 | $data = $this->fetchRemoteJson($actor); |
| 887 | 1020 | if (!$data) return null; |
| 888 | 1021 |
| @@ -46,8 +46,14 @@ | ||
| 46 | 46 | private string $dataDir; |
| 47 | 47 | protected int $dataDuration = 1800; // 30分 |
| 48 | 48 | |
| 49 | - public function __construct(string $dataDir) { | |
| 50 | - $this->dataDir = ROOT.'/data/cache/'.$dataDir.'/'; | |
| 49 | + /** | |
| 50 | + * @param string $dataDir キャッシュの種類(data/cache/の下のディレクトリ名) | |
| 51 | + * @param string|null $baseDir 保存先(既定はdata/cache/、テスト用) | |
| 52 | + */ | |
| 53 | + public function __construct(string $dataDir, ?string $baseDir = null) { | |
| 54 | + // 「../」でdata/cacheの外に出ない様に | |
| 55 | + if (!$this->isSafeName($dataDir)) throw new \InvalidArgumentException("不正なキャッシュディレクトリ名: {$dataDir}"); | |
| 56 | + $this->dataDir = rtrim($baseDir ?? ROOT.'/data/cache', '/').'/'.$dataDir.'/'; | |
| 51 | 57 | } |
| 52 | 58 | |
| 53 | 59 | /** |
| @@ -59,14 +65,14 @@ | ||
| 59 | 65 | */ |
| 60 | 66 | public function set(string $type, array $data): Result { |
| 61 | 67 | if (!$this->isSafeName($type)) return Result::Error('エラー:不正なキャッシュ名。'); |
| 62 | - if (!file_exists($this->dataDir)) { | |
| 63 | - if (!mkdir($this->dataDir, 0755)) | |
| 64 | - return Result::Error('エラー:ユーザーのアイコンディレクトリの作成に失敗。'); | |
| 65 | - } | |
| 68 | + // 以前はdata/cache/が無いと作成に失敗した(再帰的ではなかった) | |
| 69 | + if (!is_dir($this->dataDir) && !@mkdir($this->dataDir, 0755, true) && !is_dir($this->dataDir)) | |
| 70 | + return Result::Error('エラー:キャッシュディレクトリの作成に失敗。'); | |
| 66 | 71 | |
| 67 | 72 | $data['lifespan'] = time(); |
| 68 | 73 | $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); |
| 69 | - if (file_put_contents("{$this->dataDir}{$type}.json", $json) === false) return Result::Error('エラー:ユーザーデータの保存に失敗。'); | |
| 74 | + if ($json === false) return Result::Error('エラー:キャッシュデータのエンコードに失敗。'); | |
| 75 | + if (file_put_contents("{$this->dataDir}{$type}.json", $json, LOCK_EX) === false) return Result::Error('エラー:キャッシュデータの保存に失敗。'); | |
| 70 | 76 | |
| 71 | 77 | return Result::Success(); |
| 72 | 78 | } |
| @@ -399,9 +399,10 @@ | ||
| 399 | 399 | // 名前解決は1回だけ行い、確認したIPに直接接続する(DNSリバインディング対策) |
| 400 | 400 | $connectHost = $host; |
| 401 | 401 | if ($this->blockPrivate) { |
| 402 | - $ip = $this->resolvePublicIp($host); | |
| 402 | + $ip = $this->resolvePublicIp($host, $unresolved); | |
| 403 | 403 | if ($ip === null) { |
| 404 | - $this->responseError = "非公開アドレスへの接続を拒否: {$host}"; | |
| 404 | + // 以前は名前解決の失敗も「非公開アドレス」と表示していた | |
| 405 | + $this->responseError = $unresolved ? "名前解決に失敗: {$host}" : "非公開アドレスへの接続を拒否: {$host}"; | |
| 405 | 406 | $this->method = $originalMethod; |
| 406 | 407 | return Result::Error($this->responseError); |
| 407 | 408 | } |
| @@ -735,9 +736,11 @@ | ||
| 735 | 736 | * ホスト名を解決し、全てのIPが公開アドレスの場合だけ接続先のIPを返す |
| 736 | 737 | * |
| 737 | 738 | * @param string $host ホスト名又はIPアドレス |
| 739 | + * @param bool|null $unresolved 名前解決に失敗した場合true | |
| 738 | 740 | * @return string|null 接続先のIP(IPv6は[]付き)、拒否する場合はnull |
| 739 | 741 | */ |
| 740 | - private function resolvePublicIp(string $host): ?string { | |
| 742 | + private function resolvePublicIp(string $host, ?bool &$unresolved = null): ?string { | |
| 743 | + $unresolved = false; | |
| 741 | 744 | $host = trim($host, '[]'); |
| 742 | 745 | if (filter_var($host, FILTER_VALIDATE_IP)) { |
| 743 | 746 | $ips = [$host]; |
| @@ -747,7 +750,10 @@ | ||
| 747 | 750 | if (isset($r['ipv6'])) $ips[] = $r['ipv6']; |
| 748 | 751 | } |
| 749 | 752 | } |
| 750 | - if (empty($ips)) return null; | |
| 753 | + if (empty($ips)) { | |
| 754 | + $unresolved = true; | |
| 755 | + return null; | |
| 756 | + } | |
| 751 | 757 | |
| 752 | 758 | foreach ($ips as $ip) { |
| 753 | 759 | if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) return null; |
| @@ -73,6 +73,8 @@ | ||
| 73 | 73 | ]; |
| 74 | 74 | |
| 75 | 75 | public function __construct(string $file) { |
| 76 | + // ファイルが無くてもfullInfoを読める様に(以前は未初期化のままで、記事のサムネイルが無いとページが落ちた) | |
| 77 | + $this->fullInfo = (object)['type' => $this->type, 'size' => $this->size, 'bytes' => 0, 'width' => 0, 'height' => 0]; | |
| 76 | 78 | $file = ROOT.$file; |
| 77 | 79 | // $file = ROOT.'/public/static/article/mock-screenshot.tga'; |
| 78 | 80 | if (!file_exists($file)) return; |
| @@ -38,6 +38,7 @@ | ||
| 38 | 38 | namespace Std\Lib; |
| 39 | 39 | |
| 40 | 40 | use LogType; |
| 41 | +use Result; | |
| 41 | 42 | |
| 42 | 43 | class Mailer { |
| 43 | 44 | private $socket; |
| @@ -46,46 +47,104 @@ | ||
| 46 | 47 | private ?string $user; |
| 47 | 48 | private ?string $pass; |
| 48 | 49 | |
| 49 | - private ?string $pgpKey; | |
| 50 | - private ?string $pgpPass; | |
| 50 | + private string $from; | |
| 51 | + private ?string $pgpKey = null; | |
| 52 | + private ?string $pgpPass = null; | |
| 53 | + private int $timeout = 30; | |
| 51 | 54 | |
| 52 | 55 | /** |
| 53 | - * コンストラクタ | |
| 56 | + * コンストラクタ | |
| 57 | + * | |
| 58 | + * @param array|null $info MAILINFOの代わりの設定(テスト用。指定した場合はMAILER_ENABLEDを確認しない) | |
| 54 | 59 | */ |
| 55 | - public function __construct() { | |
| 56 | - if (!MAILER_ENABLED) { | |
| 60 | + public function __construct(?array $info = null) { | |
| 61 | + if ($info === null && !MAILER_ENABLED) { | |
| 57 | 62 | throw new \Exception("メーラーは無効です。"); |
| 58 | 63 | } |
| 59 | 64 | |
| 60 | - $this->host = MAILINFO['host']; | |
| 61 | - $this->port = MAILINFO['port']; | |
| 62 | - $this->user = MAILINFO['user']; | |
| 63 | - $this->pass = MAILINFO['pass']; | |
| 65 | + $info ??= MAILINFO; | |
| 66 | + $this->host = (string)($info['host'] ?? ''); | |
| 67 | + $this->port = (int)($info['port'] ?? 587); | |
| 68 | + $this->user = $info['user'] ?? null; | |
| 69 | + $this->pass = $info['pass'] ?? null; | |
| 70 | + $this->from = (string)($info['from'] ?? ''); | |
| 64 | 71 | } |
| 65 | 72 | |
| 66 | 73 | /** |
| 74 | + * 接続・認証・送信・切断を1回で行う(例外はResultにする) | |
| 75 | + * | |
| 76 | + * @param string $to 受信者のメールアドレス | |
| 77 | + * @param string $subject 件名 | |
| 78 | + * @param string $body 本文 | |
| 79 | + * @param string|null $toName 受信者の名前 | |
| 80 | + * @return Result | |
| 81 | + */ | |
| 82 | + public static function deliver(string $to, string $subject, string $body, ?string $toName = null): Result { | |
| 83 | + try { | |
| 84 | + $mail = new self(); | |
| 85 | + $mail->connect(); | |
| 86 | + try { | |
| 87 | + $mail->authenticate(); | |
| 88 | + $mail->send($to, $subject, $body, $toName); | |
| 89 | + } finally { | |
| 90 | + $mail->disconnect(); | |
| 91 | + } | |
| 92 | + return Result::Success('メールを送信しました。'); | |
| 93 | + } catch (\Throwable $e) { | |
| 94 | + logger(LogType::Mailer, "「{$to}」への送信に失敗:".$e->getMessage()); | |
| 95 | + return Result::Error('メールの送信に失敗しました。'); | |
| 96 | + } | |
| 97 | + } | |
| 98 | + | |
| 99 | + /** | |
| 67 | 100 | * ソケット接続を開く。 |
| 68 | 101 | * |
| 69 | 102 | * @return void |
| 70 | 103 | */ |
| 71 | 104 | public function connect(): void { |
| 72 | - $this->socket = fsockopen($this->host, $this->port, $errno, $err, 30); | |
| 105 | + $this->socket = @fsockopen($this->host, $this->port, $errno, $err, $this->timeout); | |
| 73 | 106 | if (!$this->socket) { |
| 74 | 107 | $msg = "接続に失敗: {$err} ({$errno})"; |
| 75 | 108 | logger(LogType::Mailer, $msg); |
| 76 | 109 | throw new \Exception($msg); |
| 77 | 110 | } |
| 111 | + stream_set_timeout($this->socket, $this->timeout); | |
| 78 | 112 | |
| 79 | - $this->readResponse(); | |
| 113 | + $this->expectGreeting(); | |
| 80 | 114 | } |
| 81 | 115 | |
| 82 | 116 | /** |
| 117 | + * 既に開いているストリームを使う(テストやプロキシ用) | |
| 118 | + * | |
| 119 | + * @param resource $stream SMTPサーバーに繋がったストリーム | |
| 120 | + * @return void | |
| 121 | + */ | |
| 122 | + public function useStream($stream): void { | |
| 123 | + $this->socket = $stream; | |
| 124 | + $this->expectGreeting(); | |
| 125 | + } | |
| 126 | + | |
| 127 | + private function expectGreeting(): void { | |
| 128 | + $res = $this->readResponse(); | |
| 129 | + if (substr($res, 0, 3) !== '220') { | |
| 130 | + $msg = "予期しない挨拶: {$res}"; | |
| 131 | + logger(LogType::Mailer, $msg); | |
| 132 | + throw new \Exception($msg); | |
| 133 | + } | |
| 134 | + } | |
| 135 | + | |
| 136 | + /** | |
| 83 | 137 | * ソケット接続を切断する。 |
| 84 | 138 | * |
| 85 | 139 | * @return void |
| 86 | 140 | */ |
| 87 | 141 | public function disconnect(): void { |
| 88 | - $this->sendCommand('QUIT', 221); | |
| 142 | + if (!is_resource($this->socket)) return; | |
| 143 | + try { | |
| 144 | + $this->sendCommand('QUIT', 221); | |
| 145 | + } catch (\Exception $e) { | |
| 146 | + // 切断の失敗は無視する | |
| 147 | + } | |
| 89 | 148 | fclose($this->socket); |
| 90 | 149 | } |
| 91 | 150 | |
| @@ -96,7 +155,9 @@ | ||
| 96 | 155 | * @return void |
| 97 | 156 | */ |
| 98 | 157 | public function authenticate(): void { |
| 99 | - $ehloRes = $this->sendCommand('EHLO '.$this->host, 250); | |
| 158 | + // EHLOには相手ではなく自分のホスト名を名乗る | |
| 159 | + $me = gethostname() ?: 'localhost'; | |
| 160 | + $ehloRes = $this->sendCommand('EHLO '.$me, 250); | |
| 100 | 161 | |
| 101 | 162 | // STARTTLSは対応するかどうか確認 |
| 102 | 163 | if (strpos($ehloRes, 'STARTTLS') !== false) { |
| @@ -110,18 +171,18 @@ | ||
| 110 | 171 | throw new \Exception($msg); |
| 111 | 172 | } |
| 112 | 173 | |
| 113 | - $this->sendCommand('EHLO '.gethostname(), 250); | |
| 174 | + $this->sendCommand('EHLO '.$me, 250); | |
| 114 | 175 | } |
| 115 | 176 | |
| 116 | 177 | if ($this->user && $this->pass) { |
| 117 | 178 | $this->sendCommand('AUTH LOGIN', 334); |
| 118 | - $this->sendCommand(base64_encode($this->user), 334); | |
| 119 | - $this->sendCommand(base64_encode($this->pass), 235); | |
| 179 | + $this->sendCommand(base64_encode($this->user), 334, true); | |
| 180 | + $this->sendCommand(base64_encode($this->pass), 235, true); | |
| 120 | 181 | } |
| 121 | 182 | } |
| 122 | 183 | |
| 123 | 184 | /** |
| 124 | - * TLSハンドシェイクに失敗する。 | |
| 185 | + * メールを送信する。 | |
| 125 | 186 | * |
| 126 | 187 | * @param string $to 受信者のメールアドレス。 |
| 127 | 188 | * @param string $subject メールの件名。 |
| @@ -132,6 +193,7 @@ | ||
| 132 | 193 | * @param bool $pgpSign PGPで署名するかどうか(現在は正常に動作していません)。 |
| 133 | 194 | * |
| 134 | 195 | * @return void |
| 196 | + * @throws \InvalidArgumentException アドレスや名前に改行等が含まれる場合(ヘッダー注入対策) | |
| 135 | 197 | */ |
| 136 | 198 | public function send( |
| 137 | 199 | string $to, |
| @@ -142,13 +204,22 @@ | ||
| 142 | 204 | ?string $replyToName = null, |
| 143 | 205 | bool $pgpSign = false |
| 144 | 206 | ): void { |
| 145 | - $from = MAILINFO['from']; | |
| 207 | + $from = $this->from; | |
| 208 | + foreach ([$from, $to, $replyTo] as $addr) { | |
| 209 | + if ($addr !== null && !self::isSafeAddress($addr)) throw new \InvalidArgumentException("不正なメールアドレス: {$addr}"); | |
| 210 | + } | |
| 211 | + foreach ([$toName, $replyToName, $subject] as $text) { | |
| 212 | + if ($text !== null && preg_match('/[\r\n\0]/', $text)) throw new \InvalidArgumentException('件名や名前に改行は使えません。'); | |
| 213 | + } | |
| 146 | 214 | |
| 147 | 215 | $this->sendCommand("MAIL FROM: <{$from}>", 250); |
| 148 | 216 | $this->sendCommand("RCPT TO: <{$to}>", 250); |
| 149 | 217 | $this->sendCommand('DATA', 354); |
| 150 | 218 | |
| 151 | 219 | $headers = "Date: ".date('r')."\r\n"; // RFC 2822 |
| 220 | + // Message-IDが無いと迷惑メール扱いされ易い | |
| 221 | + $domain = substr(strrchr($from, '@') ?: '@localhost', 1); | |
| 222 | + $headers .= "Message-ID: <".bin2hex(random_bytes(16))."@{$domain}>\r\n"; | |
| 152 | 223 | |
| 153 | 224 | $encSubject = mb_encode_mimeheader($subject, 'UTF-8', 'Q'); |
| 154 | 225 | $headers .= "Subject: {$encSubject}\r\n"; |
| @@ -181,7 +252,8 @@ | ||
| 181 | 252 | |
| 182 | 253 | $headers .= "X-Mailer: 076 Little Beast\r\n"; |
| 183 | 254 | |
| 184 | - $encBody = quoted_printable_encode($body); | |
| 255 | + // 改行をCRLFに揃えてからエンコードする | |
| 256 | + $encBody = quoted_printable_encode(preg_replace('/\r\n|\r|\n/', "\r\n", $body)); | |
| 185 | 257 | $data = $headers."\r\n".$encBody; |
| 186 | 258 | |
| 187 | 259 | if ($pgpSign) { |
| @@ -194,6 +266,8 @@ | ||
| 194 | 266 | ."--{$boundary}--\r\n"; |
| 195 | 267 | } |
| 196 | 268 | |
| 269 | + // ドットスタッフィング:「.」で始まる行は「..」にする(「.」だけの行で本文が途中で終わらない様に) | |
| 270 | + $data = preg_replace('/^\./m', '..', $data); | |
| 197 | 271 | $data .= "\r\n.\r\n"; |
| 198 | 272 | fwrite($this->socket, $data); |
| 199 | 273 | |
| @@ -232,11 +306,13 @@ | ||
| 232 | 306 | |
| 233 | 307 | // 機能性メソッド |
| 234 | 308 | |
| 235 | - private function sendCommand(string $command, int $retcode): string { | |
| 309 | + private function sendCommand(string $command, int $retcode, bool $secret = false): string { | |
| 236 | 310 | fwrite($this->socket, $command."\r\n"); |
| 237 | 311 | $res = $this->readResponse(); |
| 238 | 312 | if (substr($res, 0, 3) != $retcode) { |
| 239 | - $msg = "「{$command}」に対する予期しないレスポンス: {$res}"; | |
| 313 | + // 認証情報をログに残さない | |
| 314 | + $shown = $secret ? '(認証情報)' : $command; | |
| 315 | + $msg = "「{$shown}」に対する予期しないレスポンス: {$res}"; | |
| 240 | 316 | logger(LogType::Mailer, $msg); |
| 241 | 317 | throw new \Exception($msg); |
| 242 | 318 | } |
| @@ -247,12 +323,19 @@ | ||
| 247 | 323 | private function readResponse(): string { |
| 248 | 324 | $res = ''; |
| 249 | 325 | |
| 250 | - while ($line = fgets($this->socket, 515)) { | |
| 326 | + while (($line = fgets($this->socket, 515)) !== false) { | |
| 251 | 327 | $res .= $line; |
| 252 | 328 | if (substr($line, 3, 1) == ' ') break; // レスポンスの終了だ |
| 253 | 329 | } |
| 254 | 330 | |
| 255 | 331 | return $res; |
| 332 | + } | |
| 333 | + | |
| 334 | + /** | |
| 335 | + * SMTPのコマンドに入れても安全なメールアドレスか(改行や<>を含まない) | |
| 336 | + */ | |
| 337 | + private static function isSafeAddress(string $addr): bool { | |
| 338 | + return $addr !== '' && !preg_match('/[\r\n\0<>\s]/', $addr) && str_contains($addr, '@'); | |
| 256 | 339 | } |
| 257 | 340 | |
| 258 | 341 | private function signMessage(string $message): string { |
| @@ -53,8 +53,8 @@ | ||
| 53 | 53 | private int $resellerId = 0; |
| 54 | 54 | private int $lastAuth = 0; |
| 55 | 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'; | |
| 56 | + protected string $BASEURL = DEBUG_MODE ? 'http://api.sandbox.openprovider.nl:8480/v1beta' | |
| 57 | + : 'https://api.openprovider.eu/v1beta'; | |
| 58 | 58 | |
| 59 | 59 | public function __construct() { |
| 60 | 60 | if (!OPENPROVIDER_ENABLED) return; |
| @@ -86,36 +86,31 @@ | ||
| 86 | 86 | return Result::Success('', $cache); |
| 87 | 87 | } |
| 88 | 88 | |
| 89 | - $curl = new Curl("{$this->BASEURL}/auth/login"); | |
| 90 | 89 | $payload = [ |
| 91 | 90 | 'username' => $this->user, |
| 92 | 91 | 'password' => $this->pass, |
| 93 | 92 | 'ip' => $this->ip ?: '0.0.0.0', |
| 94 | 93 | ]; |
| 95 | 94 | |
| 96 | - $curl->setMethod("POST"); | |
| 97 | - $curl->setPostRaw(json_encode($payload)); | |
| 98 | - $curl->addHeader("Content-Type", "application/json"); | |
| 99 | - $curl->addHeader("Accept", "application/json"); | |
| 95 | + $curl = $this->setupCurl('/auth/login', 'POST', $payload); | |
| 100 | 96 | |
| 101 | 97 | $res = $curl->execute(); |
| 102 | 98 | if (!$res->isSuccess) { |
| 103 | - $err = "CURLの実行に失敗: ".$curl->message; | |
| 99 | + $err = "CURLの実行に失敗: ".$curl->getError(); | |
| 104 | 100 | assert_unless_success($res, $err); |
| 105 | 101 | return Result::Error($err); |
| 106 | 102 | } |
| 107 | 103 | |
| 108 | 104 | $body = $curl->getResponseBody(); |
| 109 | 105 | if ($curl->getResponseCode() != 200) { |
| 110 | - $err = json_decode($body, true); | |
| 111 | - assert_not_null($err, "エラーの受け取りに失敗。"); | |
| 112 | - assert($curl->getResponseCode() == 200, $err['desc']); | |
| 106 | + $err = self::errorDesc($body, $curl->getResponseCode()); | |
| 107 | + assert($curl->getResponseCode() == 200, $err); | |
| 113 | 108 | return Result::Error($err); |
| 114 | 109 | } |
| 115 | 110 | assert_not_null($body, "返事ボディーは空です。"); |
| 116 | 111 | |
| 117 | 112 | $res = json_decode($body, true); |
| 118 | - if (isset($res['data']['token'])) { | |
| 113 | + if (is_array($res) && isset($res['data']['token'])) { | |
| 119 | 114 | $this->token = $res['data']['token']; |
| 120 | 115 | $this->lastAuth = time(); |
| 121 | 116 | $res['last_auth'] = $this->lastAuth; |
| @@ -160,13 +155,14 @@ | ||
| 160 | 155 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 161 | 156 | $cacheName = 'listdomains'; |
| 162 | 157 | $cache = $this->cache->get($cacheName); |
| 163 | - if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 158 | + // キャッシュからも同じ形(results)で返す(以前はキャッシュの時だけ応答全体を返した) | |
| 159 | + if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache['data']['results'] ?? []); | |
| 164 | 160 | |
| 165 | 161 | $uri = "/domains?".http_build_query($query, '', '&', PHP_QUERY_RFC3986); |
| 166 | 162 | $curl = $this->setupCurl($uri); |
| 167 | 163 | $res = $this->curlResult($curl); |
| 168 | 164 | if (isset($res->data['data']['results'])) { |
| 169 | - $this->cache->set($cacheName, $res->data); | |
| 165 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 170 | 166 | return Result::Success('', $res->data['data']['results']); |
| 171 | 167 | } |
| 172 | 168 | |
| @@ -195,8 +191,7 @@ | ||
| 195 | 191 | // 新しい顧客の場合 |
| 196 | 192 | |
| 197 | 193 | $parts = explode('.', $name, 2); |
| 198 | - assert_exists($parts[0]); | |
| 199 | - assert_exists($parts[1]); | |
| 194 | + if (count($parts) !== 2 || $parts[0] === '' || $parts[1] === '') return Result::Error("不正なドメイン形: {$name}"); | |
| 200 | 195 | $domain = [ |
| 201 | 196 | 'name' => $parts[0], |
| 202 | 197 | 'extension' => $parts[1], |
| @@ -207,23 +202,26 @@ | ||
| 207 | 202 | $ns[] = ['name' => $n]; |
| 208 | 203 | } |
| 209 | 204 | |
| 205 | + // 以前は未定義の変数($owner_handle等)を使い、kys('TODO')で止まっていた | |
| 210 | 206 | $payload = [ |
| 211 | - 'name' => $domain, | |
| 207 | + 'domain' => $domain, | |
| 212 | 208 | 'period' => $period, |
| 213 | - 'owner_handle' => $owner_handle, | |
| 214 | - 'admin_handle' => $admin_handle, | |
| 215 | - 'tech_handle' => $tech_handle, | |
| 216 | - 'billing_handle' => $billing_handle, | |
| 217 | - 'reseller_handle' => $reseller_handle, | |
| 209 | + 'owner_handle' => $info['owner_handle'], | |
| 210 | + 'admin_handle' => $info['admin_handle'], | |
| 211 | + 'tech_handle' => $info['tech_handle'], | |
| 212 | + 'billing_handle' => $info['billing_handle'], | |
| 213 | + 'reseller_handle' => $info['reseller_handle'], | |
| 218 | 214 | 'name_servers' => $ns, |
| 219 | - 'autorenew' => $autorenew, | |
| 215 | + 'autorenew' => $info['autorenew'], | |
| 220 | 216 | ]; |
| 221 | 217 | |
| 222 | - $uri = '/domains/'; | |
| 218 | + $uri = '/domains'; | |
| 223 | 219 | $curl = $this->setupCurl($uri, 'POST', $payload); |
| 224 | - kys('TODO'); | |
| 225 | 220 | $res = $this->curlResult($curl); |
| 226 | - if (isset($res['data'])) return Result::Success('', $res); | |
| 221 | + if ($res->isSuccess && isset($res->data['data'])) { | |
| 222 | + $this->cache->murder('listdomains'); | |
| 223 | + return Result::Success('', $res->data['data']); | |
| 224 | + } | |
| 227 | 225 | |
| 228 | 226 | return Result::Error('ドメインの確認に失敗。'); |
| 229 | 227 | } |
| @@ -239,10 +237,9 @@ | ||
| 239 | 237 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 240 | 238 | $domainList = []; |
| 241 | 239 | foreach ($domains as $d) { |
| 242 | - $domain = explode('.', $d); | |
| 243 | - assert_exists($domain[0]); | |
| 244 | - assert_exists($domain[1]); | |
| 245 | - if (count($domain) !== 2) { | |
| 240 | + // 「076.co.jp」の様なTLDもあるので、最初の「.」で分ける | |
| 241 | + $domain = explode('.', (string)$d, 2); | |
| 242 | + if (count($domain) !== 2 || $domain[0] === '' || $domain[1] === '') { | |
| 246 | 243 | return Result::Error("不正なドメイン形: {$d}"); |
| 247 | 244 | } |
| 248 | 245 | |
| @@ -337,20 +334,24 @@ | ||
| 337 | 334 | */ |
| 338 | 335 | public function getDomainname(int $id, array $payload = []): Result { |
| 339 | 336 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 337 | + // 以前は未定義の$queryと$dataを使い、GETではなくPOSTで送っていた | |
| 340 | 338 | $cacheName = "getdomain-{$id}"; |
| 341 | 339 | $cache = $this->cache->get($cacheName); |
| 342 | - if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 340 | + if ($this->cache->resurrect($cache, $payload)) return Result::Success('', $cache['data'] ?? []); | |
| 343 | 341 | |
| 344 | - $curl = $this->setupCurl("/domains/{$id}", 'POST', $payload); | |
| 342 | + foreach ($payload as $k => $v) if (is_bool($v)) $payload[$k] = $v ? 'true' : 'false'; | |
| 343 | + $uri = "/domains/{$id}".(empty($payload) ? '' : '?'.http_build_query($payload, '', '&', PHP_QUERY_RFC3986)); | |
| 344 | + $curl = $this->setupCurl($uri); | |
| 345 | 345 | $res = $this->curlResult($curl); |
| 346 | - if (isset($res->data['data']['results'])) { | |
| 347 | - $this->cache->set($cacheName, $data->data); | |
| 348 | - return Result::Success('ドメインの受け取りに成功。', $res->data['data']['results']); | |
| 346 | + if ($res->isSuccess && isset($res->data['data'])) { | |
| 347 | + $this->cache->set($cacheName, $res->data + ['query' => $payload]); | |
| 348 | + return Result::Success('ドメインの受け取りに成功。', $res->data['data']); | |
| 349 | 349 | } |
| 350 | 350 | |
| 351 | 351 | return Result::Error('ドメインの受け取りに失敗。'); |
| 352 | 352 | } |
| 353 | 353 | |
| 354 | + | |
| 354 | 355 | /** |
| 355 | 356 | * ドメイン名の更新 |
| 356 | 357 | * @todo テスト |
| @@ -525,13 +526,13 @@ | ||
| 525 | 526 | $cacheName = $name && $ext ? "domainadditionaldata-{$name}.{$ext}" : 'domainadditionaldata'; |
| 526 | 527 | |
| 527 | 528 | $cache = $this->cache->get($cacheName); |
| 528 | - if ($this->cache->resurrect($cache)) return Result::Success('', $cache); | |
| 529 | + if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 529 | 530 | |
| 530 | 531 | $uri = "/domains/additional-data?".http_build_query($query, '', '&', PHP_QUERY_RFC3986); |
| 531 | 532 | $curl = $this->setupCurl($uri); |
| 532 | 533 | $res = $this->curlResult($curl); |
| 533 | 534 | if (isset($res->data['data'])) { |
| 534 | - $this->cache->set($cacheName, $res->data); | |
| 535 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 535 | 536 | return Result::Success('', $res->data); |
| 536 | 537 | } |
| 537 | 538 | |
| @@ -556,13 +557,13 @@ | ||
| 556 | 557 | $cacheName = $name && $ext ? "domainadditionaldata-{$name}.{$ext}" : 'domainadditionaldata'; |
| 557 | 558 | |
| 558 | 559 | $cache = $this->cache->get($cacheName); |
| 559 | - if ($this->cache->resurrect($cache)) return Result::Success('', $cache); | |
| 560 | + if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 560 | 561 | |
| 561 | 562 | $uri = "/domains/additional-data/customers?".http_build_query($query, '', '&', PHP_QUERY_RFC3986); |
| 562 | 563 | $curl = $this->setupCurl($uri); |
| 563 | 564 | $res = $this->curlResult($curl); |
| 564 | 565 | if (isset($res->data['data'])) { |
| 565 | - $this->cache->set($cacheName, $res->data); | |
| 566 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 566 | 567 | return Result::Success('', $res->data); |
| 567 | 568 | } |
| 568 | 569 | |
| @@ -587,13 +588,13 @@ | ||
| 587 | 588 | $cacheName = $name && $ext ? "domainprices-{$name}.{$ext}" : 'domainprices'; |
| 588 | 589 | |
| 589 | 590 | $cache = $this->cache->get($cacheName); |
| 590 | - if ($this->cache->resurrect($cache)) return Result::Success('', $cache); | |
| 591 | + if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 591 | 592 | |
| 592 | 593 | $uri = "/domains/prices?".http_build_query($query, '', '&', PHP_QUERY_RFC3986); |
| 593 | 594 | $curl = $this->setupCurl($uri); |
| 594 | 595 | $res = $this->curlResult($curl); |
| 595 | 596 | if (isset($res->data['data'])) { |
| 596 | - $this->cache->set($cacheName, $res->data); | |
| 597 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 597 | 598 | return Result::Success('', $res->data); |
| 598 | 599 | } |
| 599 | 600 | |
| @@ -635,10 +636,10 @@ | ||
| 635 | 636 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 636 | 637 | |
| 637 | 638 | $uri = "/domains/{$id}/authcode/reset"; |
| 638 | - $curl->setupCurl($uri, 'POST', $payload); | |
| 639 | + $curl = $this->setupCurl($uri, 'POST', $payload); | |
| 639 | 640 | $res = $this->curlResult($curl); |
| 640 | - kys($res); | |
| 641 | - if (isset($res)) { | |
| 641 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 642 | + if ($res->isSuccess) { | |
| 642 | 643 | return Result::Success('', $res->data); |
| 643 | 644 | } |
| 644 | 645 | |
| @@ -657,7 +658,7 @@ | ||
| 657 | 658 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 658 | 659 | $cacheName = 'listtlds'; |
| 659 | 660 | $cache = $this->cache->get($cacheName); |
| 660 | - if ($this->cache->resurrect($cache)) return Result::Success('', $cache); | |
| 661 | + if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 661 | 662 | |
| 662 | 663 | if (empty($query)) $query = [ 'limit' => 10, 'offset' => 0, 'order' => 'ASC' ]; |
| 663 | 664 | foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false'; |
| @@ -666,7 +667,7 @@ | ||
| 666 | 667 | $curl = $this->setupCurl($uri); |
| 667 | 668 | $res = $this->curlResult($curl); |
| 668 | 669 | if (isset($res->data['data'])) { |
| 669 | - $this->cache->set($cacheName, $res->data); | |
| 670 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 670 | 671 | return Result::Success('', $res->data); |
| 671 | 672 | } |
| 672 | 673 | |
| @@ -684,7 +685,7 @@ | ||
| 684 | 685 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 685 | 686 | $cacheName = "gettld-{$tld}"; |
| 686 | 687 | $cache = $this->cache->get($cacheName); |
| 687 | - if ($this->cache->resurrect($cache)) return Result::Success('', $cache); | |
| 688 | + if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 688 | 689 | |
| 689 | 690 | if (empty($query)) $query = [ 'limit' => 10, 'offset' => 0 ]; |
| 690 | 691 | foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false'; |
| @@ -693,7 +694,7 @@ | ||
| 693 | 694 | $curl = $this->setupCurl($uri); |
| 694 | 695 | $res = $this->curlResult($curl); |
| 695 | 696 | if (isset($res->data['data'])) { |
| 696 | - $this->cache->set($cacheName, $res->data); | |
| 697 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 697 | 698 | return Result::Success('', $res->data); |
| 698 | 699 | } |
| 699 | 700 | |
| @@ -713,7 +714,7 @@ | ||
| 713 | 714 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 714 | 715 | $cacheName = 'listinvoices'; |
| 715 | 716 | $cache = $this->cache->get($cacheName); |
| 716 | - if ($this->cache->resurrect($cache)) return Result::Success('', $cache); | |
| 717 | + if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 717 | 718 | |
| 718 | 719 | if (empty($query)) $query = [ 'limit' => 10, 'offset' => 0 ]; |
| 719 | 720 | foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false'; |
| @@ -722,7 +723,7 @@ | ||
| 722 | 723 | $curl = $this->setupCurl($uri); |
| 723 | 724 | $res = $this->curlResult($curl); |
| 724 | 725 | if (isset($res->data['data'])) { |
| 725 | - $this->cache->set($cacheName, $res->data); | |
| 726 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 726 | 727 | return Result::Success('', $res->data); |
| 727 | 728 | } |
| 728 | 729 | |
| @@ -741,7 +742,7 @@ | ||
| 741 | 742 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 742 | 743 | $cacheName = 'listpayments'; |
| 743 | 744 | $cache = $this->cache->get($cacheName); |
| 744 | - if ($this->cache->resurrect($cache)) return Result::Success('', $cache); | |
| 745 | + if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 745 | 746 | |
| 746 | 747 | if (empty($query)) $query = [ 'limit' => 10, 'offset' => 0 ]; |
| 747 | 748 | foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false'; |
| @@ -750,7 +751,7 @@ | ||
| 750 | 751 | $curl = $this->setupCurl($uri); |
| 751 | 752 | $res = $this->curlResult($curl); |
| 752 | 753 | if (isset($res->data['data'])) { |
| 753 | - $this->cache->set($cacheName, $res->data); | |
| 754 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 754 | 755 | return Result::Success('', $res->data); |
| 755 | 756 | } |
| 756 | 757 | |
| @@ -769,7 +770,7 @@ | ||
| 769 | 770 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 770 | 771 | $cacheName = 'listtransactions'; |
| 771 | 772 | $cache = $this->cache->get($cacheName); |
| 772 | - if ($this->cache->resurrect($cache)) return Result::Success('', $cache); | |
| 773 | + if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 773 | 774 | |
| 774 | 775 | if (empty($query)) $query = [ 'limit' => 10, 'offset' => 0 ]; |
| 775 | 776 | foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false'; |
| @@ -778,7 +779,7 @@ | ||
| 778 | 779 | $curl = $this->setupCurl($uri); |
| 779 | 780 | $res = $this->curlResult($curl); |
| 780 | 781 | if (isset($res->data['data'])) { |
| 781 | - $this->cache->set($cacheName, $res->data); | |
| 782 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 782 | 783 | return Result::Success('', $res->data); |
| 783 | 784 | } |
| 784 | 785 | |
| @@ -830,7 +831,7 @@ | ||
| 830 | 831 | $curl = $this->setupCurl($uri); |
| 831 | 832 | $res = $this->curlResult($curl); |
| 832 | 833 | if (isset($res->data['data'])) { |
| 833 | - $this->cache->set($cacheName, $res->data); | |
| 834 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 834 | 835 | return Result::Success('', $res->data); |
| 835 | 836 | } |
| 836 | 837 | |
| @@ -870,7 +871,7 @@ | ||
| 870 | 871 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 871 | 872 | $cacheName = "listnameserver-{$domain}"; |
| 872 | 873 | $cache = $this->cache->get($cacheName); |
| 873 | - if ($this->cache->resurrect($cache)) return Result::Success('', $cache); | |
| 874 | + if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 874 | 875 | |
| 875 | 876 | if (empty($query)) $query = [ 'limit' => 100, 'offset' => 0, 'order_by.name' => 'asc' ]; |
| 876 | 877 | foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false'; |
| @@ -879,7 +880,7 @@ | ||
| 879 | 880 | $curl = $this->setupCurl($uri); |
| 880 | 881 | $res = $this->curlResult($curl); |
| 881 | 882 | if (isset($res->data['data'])) { |
| 882 | - $this->cache->set($cacheName, $res->data); | |
| 883 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 883 | 884 | return Result::Success('', $res->data); |
| 884 | 885 | } |
| 885 | 886 | |
| @@ -953,7 +954,7 @@ | ||
| 953 | 954 | $curl = $this->setupCurl($uri); |
| 954 | 955 | $res = $this->curlResult($curl); |
| 955 | 956 | if (isset($res->data['data'])) { |
| 956 | - $this->cache->set($cacheName, $res->data); | |
| 957 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 957 | 958 | return Result::Success('', $res->data); |
| 958 | 959 | } |
| 959 | 960 | |
| @@ -993,7 +994,7 @@ | ||
| 993 | 994 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 994 | 995 | $cacheName = "listnsgroup-{$nsGroup}"; |
| 995 | 996 | $cache = $this->cache->get($cacheName); |
| 996 | - if ($this->cache->resurrect($cache)) return Result::Success('', $cache); | |
| 997 | + if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 997 | 998 | |
| 998 | 999 | if (empty($query)) $query = [ 'limit' => 100, 'offset' => 0, 'order_by.name' => 'asc' ]; |
| 999 | 1000 | foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false'; |
| @@ -1002,7 +1003,7 @@ | ||
| 1002 | 1003 | $curl = $this->setupCurl($uri); |
| 1003 | 1004 | $res = $this->curlResult($curl); |
| 1004 | 1005 | if (isset($res->data['data'])) { |
| 1005 | - $this->cache->set($cacheName, $res->data); | |
| 1006 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 1006 | 1007 | return Result::Success('', $res->data); |
| 1007 | 1008 | } |
| 1008 | 1009 | |
| @@ -1075,7 +1076,7 @@ | ||
| 1075 | 1076 | $res = $this->curlResult($curl); |
| 1076 | 1077 | if (isset($res->data['data'])) { |
| 1077 | 1078 | $res->data['query'] = $query; |
| 1078 | - $this->cache->set($cacheName, $res->data); | |
| 1079 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 1079 | 1080 | return Result::Success('', $res->data); |
| 1080 | 1081 | } |
| 1081 | 1082 | |
| @@ -1114,7 +1115,7 @@ | ||
| 1114 | 1115 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 1115 | 1116 | $cacheName = "getdnstemplate-{$id}"; |
| 1116 | 1117 | $cache = $this->cache->get($cacheName); |
| 1117 | - if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 1118 | + if ($this->cache->resurrect($cache)) return Result::Success('', $cache); | |
| 1118 | 1119 | |
| 1119 | 1120 | $uri = "/dns/templates/{$id}"; |
| 1120 | 1121 | $curl = $this->setupCurl($uri); |
| @@ -1170,7 +1171,7 @@ | ||
| 1170 | 1171 | $curl = $this->setupCurl($uri); |
| 1171 | 1172 | $res = $this->curlResult($curl); |
| 1172 | 1173 | if (isset($res->data['data'])) { |
| 1173 | - $this->cache->set($cacheName, $res->data); | |
| 1174 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 1174 | 1175 | return Result::Success('', $res->data); |
| 1175 | 1176 | } |
| 1176 | 1177 | |
| @@ -1209,7 +1210,7 @@ | ||
| 1209 | 1210 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 1210 | 1211 | $cacheName = "getdnszone-{$domain}"; |
| 1211 | 1212 | $cache = $this->cache->get($cacheName); |
| 1212 | - if ($this->cache->resurrect($cache)) return Result::Success('', $cache); | |
| 1213 | + if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 1213 | 1214 | |
| 1214 | 1215 | if (empty($query)) $query = [ 'limit' => 100, 'offset' => 0, 'order_by.name' => 'asc' ]; |
| 1215 | 1216 | foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false'; |
| @@ -1218,7 +1219,7 @@ | ||
| 1218 | 1219 | $curl = $this->setupCurl($uri); |
| 1219 | 1220 | $res = $this->curlResult($curl); |
| 1220 | 1221 | if (isset($res->data['data'])) { |
| 1221 | - $this->cache->set($cacheName, $res->data); | |
| 1222 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 1222 | 1223 | return Result::Success('', $res->data); |
| 1223 | 1224 | } |
| 1224 | 1225 | |
| @@ -1379,7 +1380,7 @@ | ||
| 1379 | 1380 | $curl = $this->setupCurl($uri); |
| 1380 | 1381 | $res = $this->curlResult($curl); |
| 1381 | 1382 | if (isset($res->data['data'])) { |
| 1382 | - $this->cache->set($cacheName, $res->data); | |
| 1383 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 1383 | 1384 | return Result::Success('', $res->data); |
| 1384 | 1385 | } |
| 1385 | 1386 | |
| @@ -1441,23 +1442,16 @@ | ||
| 1441 | 1442 | */ |
| 1442 | 1443 | public function getEasydmarcSso(int $orderId): Result { |
| 1443 | 1444 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 1444 | - $cacheName = "geteasydmarcssso-{$orderId}"; | |
| 1445 | - $cache = $this->cache->get($cacheName); | |
| 1446 | - if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 1447 | - | |
| 1448 | - $uri = "/dns/easydmarcs/{$domain}/sso"; | |
| 1445 | + // SSOのリンクは使い捨てなのでキャッシュしない(以前は未定義の$domainと$dataを使っていた) | |
| 1446 | + $uri = "/dns/easydmarcs/{$orderId}/sso"; | |
| 1449 | 1447 | $curl = $this->setupCurl($uri); |
| 1450 | 1448 | $res = $this->curlResult($curl); |
| 1451 | - if (isset($res->data['data'])) { | |
| 1452 | - $this->cache->set($cacheName, $data->data); | |
| 1453 | - $this->cache->murder('listeasydmarcs'); | |
| 1454 | - $this->cache->murder("listeasydmarc-{$domain}"); | |
| 1455 | - if ($res->data['data']['success']) return Result::Success('Easydmarcの削除に成功。', $res->data); | |
| 1456 | - } | |
| 1449 | + if ($res->isSuccess && isset($res->data['data'])) return Result::Success('', $res->data); | |
| 1457 | 1450 | |
| 1458 | - return Result::Error('Easydmarcの削除に失敗。'); | |
| 1451 | + return Result::Error('EasydmarcのSSOの受け取りに失敗。'); | |
| 1459 | 1452 | } |
| 1460 | 1453 | |
| 1454 | + | |
| 1461 | 1455 | // メールテンプレート |
| 1462 | 1456 | |
| 1463 | 1457 | //// メール |
| @@ -1481,7 +1475,7 @@ | ||
| 1481 | 1475 | $res = $this->curlResult($curl); |
| 1482 | 1476 | if (isset($res->data['data'])) { |
| 1483 | 1477 | $res->data['query'] = $query; |
| 1484 | - $this->cache->set($cacheName, $res->data); | |
| 1478 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 1485 | 1479 | return Result::Success('', $res->data); |
| 1486 | 1480 | } |
| 1487 | 1481 | |
| @@ -1573,7 +1567,7 @@ | ||
| 1573 | 1567 | $res = $this->curlResult($curl); |
| 1574 | 1568 | if (isset($res->data['data'])) { |
| 1575 | 1569 | $res->data['query'] = $query; |
| 1576 | - $this->cache->set($cacheName, $res->data); | |
| 1570 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 1577 | 1571 | return Result::Success('ライセンス一覧の受け取りに成功。', $res->data); |
| 1578 | 1572 | } |
| 1579 | 1573 | |
| @@ -1595,7 +1589,8 @@ | ||
| 1595 | 1589 | $uri = "/licenses/hwids/reset/{$product}/{$keyId}"; |
| 1596 | 1590 | $curl = $this->setupCurl($uri, 'POST', $payload); |
| 1597 | 1591 | $res = $this->curlResult($curl); |
| 1598 | - if (isset($res)) { | |
| 1592 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 1593 | + if ($res->isSuccess) { | |
| 1599 | 1594 | $this->cache->murder('listlicenses'); |
| 1600 | 1595 | return Result::Success('HWIDの再設定に成功。', $res->data); |
| 1601 | 1596 | } |
| @@ -1622,7 +1617,7 @@ | ||
| 1622 | 1617 | $res = $this->curlResult($curl); |
| 1623 | 1618 | if (isset($res->data['data'])) { |
| 1624 | 1619 | $res->data['query'] = $query; |
| 1625 | - $this->cache->set($cacheName, $res->data); | |
| 1620 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 1626 | 1621 | return Result::Success('商品一覧の受け取りに成功。', $res->data); |
| 1627 | 1622 | } |
| 1628 | 1623 | |
| @@ -1648,7 +1643,7 @@ | ||
| 1648 | 1643 | $res = $this->curlResult($curl); |
| 1649 | 1644 | if (isset($res->data['data'])) { |
| 1650 | 1645 | $res->data['query'] = $query; |
| 1651 | - $this->cache->set($cacheName, $res->data); | |
| 1646 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 1652 | 1647 | return Result::Success('Pleskライセンス一覧の受け取りに成功。', $res->data); |
| 1653 | 1648 | } |
| 1654 | 1649 | |
| @@ -1668,7 +1663,8 @@ | ||
| 1668 | 1663 | $uri = "/licenses/plesk"; |
| 1669 | 1664 | $curl = $this->setupCurl($uri, 'POST', $payload); |
| 1670 | 1665 | $res = $this->curlResult($curl); |
| 1671 | - if (isset($res)) { | |
| 1666 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 1667 | + if ($res->isSuccess) { | |
| 1672 | 1668 | $this->cache->murder('listlicenses'); |
| 1673 | 1669 | $this->cache->murder('listplesklicenses'); |
| 1674 | 1670 | return Result::Success('Pleskライセンスの作成に成功。', $res->data); |
| @@ -1799,7 +1795,7 @@ | ||
| 1799 | 1795 | $res = $this->curlResult($curl); |
| 1800 | 1796 | if (isset($res->data['data'])) { |
| 1801 | 1797 | $res->data['query'] = $query; |
| 1802 | - $this->cache->set($cacheName, $res->data); | |
| 1798 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 1803 | 1799 | return Result::Success('', $res->data); |
| 1804 | 1800 | } |
| 1805 | 1801 | |
| @@ -1819,7 +1815,8 @@ | ||
| 1819 | 1815 | $uri = '/contacts'; |
| 1820 | 1816 | $curl = $this->setupCurl($uri, 'POST', $payload); |
| 1821 | 1817 | $res = $this->curlResult($curl); |
| 1822 | - if (isset($res)) { | |
| 1818 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 1819 | + if ($res->isSuccess) { | |
| 1823 | 1820 | $this->cache->murder('listcontacts'); |
| 1824 | 1821 | return Result::Success('', $res->data); |
| 1825 | 1822 | } |
| @@ -1920,7 +1917,7 @@ | ||
| 1920 | 1917 | $res = $this->curlResult($curl); |
| 1921 | 1918 | if (isset($res->data['data'])) { |
| 1922 | 1919 | $res->data['query'] = $query; |
| 1923 | - $this->cache->set($cacheName, $res->data); | |
| 1920 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 1924 | 1921 | return Result::Success('', $res->data); |
| 1925 | 1922 | } |
| 1926 | 1923 | |
| @@ -1939,7 +1936,8 @@ | ||
| 1939 | 1936 | $uri = '/customers'; |
| 1940 | 1937 | $curl = $this->setupCurl($uri, 'POST', $payload); |
| 1941 | 1938 | $res = $this->curlResult($curl); |
| 1942 | - if (isset($res)) { | |
| 1939 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 1940 | + if ($res->isSuccess) { | |
| 1943 | 1941 | $this->cache->murder('listcustomers'); |
| 1944 | 1942 | return Result::Success('', $res->data); |
| 1945 | 1943 | } |
| @@ -2039,7 +2037,7 @@ | ||
| 2039 | 2037 | $res = $this->curlResult($curl); |
| 2040 | 2038 | if (isset($res->data['data'])) { |
| 2041 | 2039 | $res->data['query'] = $query; |
| 2042 | - $this->cache->set($cacheName, $res->data); | |
| 2040 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 2043 | 2041 | return Result::Success('', $res->data); |
| 2044 | 2042 | } |
| 2045 | 2043 | |
| @@ -2111,7 +2109,7 @@ | ||
| 2111 | 2109 | $res = $this->curlResult($curl); |
| 2112 | 2110 | if (isset($res->data['data'])) { |
| 2113 | 2111 | $res->data['query'] = $query; |
| 2114 | - $this->cache->set($cacheName, $res->data); | |
| 2112 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 2115 | 2113 | return Result::Success('', $res->data); |
| 2116 | 2114 | } |
| 2117 | 2115 | |
| @@ -2152,7 +2150,7 @@ | ||
| 2152 | 2150 | if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。'); |
| 2153 | 2151 | $cacheName = 'listsettings'; |
| 2154 | 2152 | $cache = $this->cache->get($cacheName); |
| 2155 | - if ($this->cache->resurrect($cache, $query)) return Result::Success('', $cache); | |
| 2153 | + if ($this->cache->resurrect($cache)) return Result::Success('', $cache); | |
| 2156 | 2154 | |
| 2157 | 2155 | $uri = '/resellers/settings'; |
| 2158 | 2156 | $curl = $this->setupCurl($uri); |
| @@ -2184,7 +2182,7 @@ | ||
| 2184 | 2182 | $res = $this->curlResult($curl); |
| 2185 | 2183 | if (isset($res->data['data'])) { |
| 2186 | 2184 | $res->data['query'] = $query; |
| 2187 | - $this->cache->set($cacheName, $res->data); | |
| 2185 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 2188 | 2186 | return Result::Success('', $res->data); |
| 2189 | 2187 | } |
| 2190 | 2188 | |
| @@ -2212,7 +2210,7 @@ | ||
| 2212 | 2210 | $res = $this->curlResult($curl); |
| 2213 | 2211 | if (isset($res->data['data'])) { |
| 2214 | 2212 | $res->data['query'] = $query; |
| 2215 | - $this->cache->set($cacheName, $res->data); | |
| 2213 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 2216 | 2214 | return Result::Success('', $res->data); |
| 2217 | 2215 | } |
| 2218 | 2216 | |
| @@ -2278,7 +2276,8 @@ | ||
| 2278 | 2276 | $uri = '/spam-expert/domains'; |
| 2279 | 2277 | $curl = $this->setupCurl($uri, 'POST', $payload); |
| 2280 | 2278 | $res = $this->curlResult($curl); |
| 2281 | - if (isset($res)) { | |
| 2279 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 2280 | + if ($res->isSuccess) { | |
| 2282 | 2281 | $this->cache->murder('listdomains'); |
| 2283 | 2282 | return Result::Success('', $res->data); |
| 2284 | 2283 | } |
| @@ -2306,7 +2305,7 @@ | ||
| 2306 | 2305 | $res = $this->curlResult($curl); |
| 2307 | 2306 | if (isset($res->data['data'])) { |
| 2308 | 2307 | $res->data['domain'] = $domain; |
| 2309 | - $this->cache->set($cacheName, $res->data); | |
| 2308 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 2310 | 2309 | return Result::Success('', $res->data); |
| 2311 | 2310 | } |
| 2312 | 2311 | |
| @@ -2381,7 +2380,7 @@ | ||
| 2381 | 2380 | $res = $this->curlResult($curl); |
| 2382 | 2381 | if (isset($res->data['data'])) { |
| 2383 | 2382 | $res->data['query'] = $query; |
| 2384 | - $this->cache->set($cacheName, $res->data); | |
| 2383 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 2385 | 2384 | return Result::Success('', $res->data); |
| 2386 | 2385 | } |
| 2387 | 2386 | |
| @@ -2403,7 +2402,8 @@ | ||
| 2403 | 2402 | $uri = '/ssl/csr'; |
| 2404 | 2403 | $curl = $this->setupCurl($uri, 'POST', $payload); |
| 2405 | 2404 | $res = $this->curlResult($curl); |
| 2406 | - if (isset($res)) { | |
| 2405 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 2406 | + if ($res->isSuccess) { | |
| 2407 | 2407 | return Result::Success('', $res->data); |
| 2408 | 2408 | } |
| 2409 | 2409 | |
| @@ -2423,7 +2423,8 @@ | ||
| 2423 | 2423 | $uri = '/ssl/csr/decode'; |
| 2424 | 2424 | $curl = $this->setupCurl($uri, 'POST', ['csr' => $csr]); |
| 2425 | 2425 | $res = $this->curlResult($curl); |
| 2426 | - if (isset($res)) { | |
| 2426 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 2427 | + if ($res->isSuccess) { | |
| 2427 | 2428 | return Result::Success('', $res->data); |
| 2428 | 2429 | } |
| 2429 | 2430 | |
| @@ -2451,7 +2452,7 @@ | ||
| 2451 | 2452 | $res = $this->curlResult($curl); |
| 2452 | 2453 | if (isset($res->data['data'])) { |
| 2453 | 2454 | $res->data['query'] = $query; |
| 2454 | - $this->cache->set($cacheName, $res->data); | |
| 2455 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 2455 | 2456 | return Result::Success('連絡先一覧の受け取りに成功。', $res->data); |
| 2456 | 2457 | } |
| 2457 | 2458 | |
| @@ -2471,7 +2472,8 @@ | ||
| 2471 | 2472 | $uri = '/ssl/orders'; |
| 2472 | 2473 | $curl = $this->setupCurl($uri, 'POST', $payload); |
| 2473 | 2474 | $res = $this->curlResult($curl); |
| 2474 | - if (isset($res)) { | |
| 2475 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 2476 | + if ($res->isSuccess) { | |
| 2475 | 2477 | $this->cache->murder('listsslorders'); |
| 2476 | 2478 | return Result::Success('SSL注文の作成に成功。', $res->data); |
| 2477 | 2479 | } |
| @@ -2542,7 +2544,8 @@ | ||
| 2542 | 2544 | $uri = "/ssl/orders/{$id}/cancel"; |
| 2543 | 2545 | $curl = $this->setupCurl($uri, 'POST', $payload); |
| 2544 | 2546 | $res = $this->curlResult($curl); |
| 2545 | - if (isset($res)) { | |
| 2547 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 2548 | + if ($res->isSuccess) { | |
| 2546 | 2549 | $this->cache->murder("getsslorder-{$id}"); |
| 2547 | 2550 | $this->cache->murder('listsslorders'); |
| 2548 | 2551 | return Result::Success('SSL注文のキャンセルに成功。', $res->data); |
| @@ -2565,7 +2568,8 @@ | ||
| 2565 | 2568 | $uri = "/ssl/orders/{$id}/reissue"; |
| 2566 | 2569 | $curl = $this->setupCurl($uri, 'POST', $payload); |
| 2567 | 2570 | $res = $this->curlResult($curl); |
| 2568 | - if (isset($res)) { | |
| 2571 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 2572 | + if ($res->isSuccess) { | |
| 2569 | 2573 | $this->cache->murder("getsslorder-{$id}"); |
| 2570 | 2574 | $this->cache->murder('listsslorders'); |
| 2571 | 2575 | return Result::Success('SSL注文の再発行に成功。', $res->data); |
| @@ -2588,7 +2592,8 @@ | ||
| 2588 | 2592 | $uri = "/ssl/orders/{$id}/renew"; |
| 2589 | 2593 | $curl = $this->setupCurl($uri, 'POST', $payload); |
| 2590 | 2594 | $res = $this->curlResult($curl); |
| 2591 | - if (isset($res)) { | |
| 2595 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 2596 | + if ($res->isSuccess) { | |
| 2592 | 2597 | $this->cache->murder("getsslorder-{$id}"); |
| 2593 | 2598 | $this->cache->murder('listsslorders'); |
| 2594 | 2599 | return Result::Success('SSL注文の更新に成功。', $res->data); |
| @@ -2613,7 +2618,8 @@ | ||
| 2613 | 2618 | $uri = "/ssl/orders/{$id}/approver-email"; |
| 2614 | 2619 | $curl = $this->setupCurl($uri, 'PUT', ['id' => $id, 'approver_email' => $approverEmail]); |
| 2615 | 2620 | $res = $this->curlResult($curl); |
| 2616 | - if (isset($res)) { | |
| 2621 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 2622 | + if ($res->isSuccess) { | |
| 2617 | 2623 | $this->cache->murder('listapproveremails'); |
| 2618 | 2624 | return Result::Success('', $res->data); |
| 2619 | 2625 | } |
| @@ -2634,7 +2640,8 @@ | ||
| 2634 | 2640 | $uri = "/ssl/orders/{$id}/approver-email/resend"; |
| 2635 | 2641 | $curl = $this->setupCurl($uri, 'POST', ['id' => $id]); |
| 2636 | 2642 | $res = $this->curlResult($curl); |
| 2637 | - if (isset($res)) { | |
| 2643 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 2644 | + if ($res->isSuccess) { | |
| 2638 | 2645 | $this->cache->murder('listapproveremails'); |
| 2639 | 2646 | return Result::Success('', $res->data); |
| 2640 | 2647 | } |
| @@ -2657,7 +2664,8 @@ | ||
| 2657 | 2664 | $uri = "/ssl/orders/{$id}/otp-tokens"; |
| 2658 | 2665 | $curl = $this->setupCurl($uri, 'POST'); |
| 2659 | 2666 | $res = $this->curlResult($curl); |
| 2660 | - if (isset($res)) { | |
| 2667 | + // 以前はisset($res)で、失敗しても成功を返していた | |
| 2668 | + if ($res->isSuccess) { | |
| 2661 | 2669 | return Result::Success('', $res->data); |
| 2662 | 2670 | } |
| 2663 | 2671 | |
| @@ -2685,7 +2693,7 @@ | ||
| 2685 | 2693 | $res = $this->curlResult($curl); |
| 2686 | 2694 | if (isset($res->data['data'])) { |
| 2687 | 2695 | $res->data['query'] = $query; |
| 2688 | - $this->cache->set($cacheName, $res->data); | |
| 2696 | + $this->cache->set($cacheName, $res->data + ['query' => $query]); | |
| 2689 | 2697 | return Result::Success('', $res->data); |
| 2690 | 2698 | } |
| 2691 | 2699 | |
| @@ -2740,12 +2748,12 @@ | ||
| 2740 | 2748 | |
| 2741 | 2749 | ////////// |
| 2742 | 2750 | |
| 2743 | - private function setupCurl(string $uri, string $resType = 'GET', array $payload = []): Curl { | |
| 2751 | + protected function setupCurl(string $uri, string $resType = 'GET', array $payload = []): Curl { | |
| 2744 | 2752 | $curl = new Curl("{$this->BASEURL}{$uri}"); |
| 2745 | 2753 | |
| 2746 | 2754 | $curl->setMethod($resType); |
| 2747 | 2755 | if (!empty($payload)) $curl->setPostRaw(json_encode($payload)); |
| 2748 | - $curl->setBearerAuth($this->token); | |
| 2756 | + if ($this->token !== '') $curl->setBearerAuth($this->token); | |
| 2749 | 2757 | $curl->addHeader('Content-Type', 'application/json'); |
| 2750 | 2758 | $curl->addHeader('Accept', 'application/json'); |
| 2751 | 2759 | |
| @@ -2755,20 +2763,30 @@ | ||
| 2755 | 2763 | private function curlResult(Curl &$curl): Result { |
| 2756 | 2764 | $res = $curl->execute(); |
| 2757 | 2765 | if (!$res->isSuccess) { |
| 2758 | - $err = "CURL実行に失敗: {$curl->message}"; | |
| 2766 | + $err = "CURL実行に失敗: {$curl->getError()}"; | |
| 2759 | 2767 | assert_unless_success($res, $err); |
| 2760 | 2768 | return Result::Error($err); |
| 2761 | 2769 | } |
| 2762 | 2770 | |
| 2763 | 2771 | $body = $curl->getResponseBody(); |
| 2764 | 2772 | if ($curl->getResponseCode() != 200) { |
| 2765 | - $err = json_decode($body, true); | |
| 2766 | - assert_not_null($err, "エラーの受け取りに失敗。"); | |
| 2767 | - assert($curl->getResponseCode() == 200, $err['desc']); | |
| 2768 | - return Result::Error($err['desc']); | |
| 2773 | + $err = self::errorDesc($body, $curl->getResponseCode()); | |
| 2774 | + assert($curl->getResponseCode() == 200, $err); | |
| 2775 | + return Result::Error($err); | |
| 2769 | 2776 | } |
| 2770 | 2777 | assert_not_null($body, "返事ボディーは空です。"); |
| 2771 | 2778 | |
| 2772 | - return Result::Success('', json_decode($body, true)); | |
| 2779 | + $data = json_decode($body, true); | |
| 2780 | + if (!is_array($data)) return Result::Error('レスポンスのJSONの解析に失敗。'); | |
| 2781 | + return Result::Success('', $data); | |
| 2782 | + } | |
| 2783 | + | |
| 2784 | + /** | |
| 2785 | + * エラーレスポンスの説明(JSONでない場合もある) | |
| 2786 | + */ | |
| 2787 | + private static function errorDesc(string $body, int $code): string { | |
| 2788 | + $err = json_decode($body, true); | |
| 2789 | + $desc = is_array($err) ? (string)($err['desc'] ?? '') : ''; | |
| 2790 | + return $desc !== '' ? $desc : "HTTP {$code}"; | |
| 2773 | 2791 | } |
| 2774 | 2792 | } |