色々の修正

守矢諏訪子 <suwako@076.moe>、2026-11-10 20:56
コミット 2e4f443c83d25314dc4506fd6b91d5806320022b ・親 7208562 ・ファイルを見る

10個のファイル、+529 -239

変更 README-JP.md
@@ -203,7 +203,10 @@
203203 php cron.php help
204204 ```
205205
206-タスクは `cron.php` で登録します。
206+タスクは `cron.php` で登録します。組み込みのタスク:
207+* `apfederate`:新しいブログ投稿を ActivityPub のフォロワーに送る
208+* `apupdate`:プロフィール(アクター)の更新を ActivityPub のフォロワーに送る
209+* `authpurge`:7日以内にメールアドレスを確認しなかったアカウントを削除する(`AUTH_VERIFY_EMAIL` が有効の場合)
207210
208211 ## 必要な PHP モジュール
209212 * php_gmp
変更 README.md
@@ -203,7 +203,10 @@
203203 php cron.php help
204204 ```
205205
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)
207210
208211 ## Required PHP module
209212 * php_gmp
変更 route.php
@@ -73,6 +73,7 @@
7373 $routes[] = Route::add('GET', 'ap/outbox', Fediverse::class.'@apoutbox');
7474 $routes[] = Route::add('GET', 'ap/activities/create/{uuid}',
7575 Fediverse::class.'@apactivity');
76+ $routes[] = Route::add('GET', 'ap/objects/{uuid}', Fediverse::class.'@apobject');
7677 $routes[] = Route::add('POST', 'ap/inbox', Fediverse::class.'@apinbox');
7778 $routes[] = Route::add('GET', 'ap/actor', Fediverse::class.'@apactor');
7879 }
@@ -110,6 +110,12 @@
110110 $ap = new Activitypub();
111111
112112 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+ }
113119 header('HTTP/1.1 401 Unauthorized');
114120 echo json_encode(['error' => '署名の検証に失敗']);
115121 exit;
@@ -137,7 +143,36 @@
137143 header('Content-Type: application/activity+json');
138144 $posts = $this->getPosts('/blog/', null);
139145 $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;
141176 exit;
142177 } catch (\Exception $e) {
143178 header('Content-Type: text/plain; charset=utf-8');
@@ -91,7 +91,7 @@
9191 */
9292 public function getActor(): string {
9393 if (!ACTIVITYPUB_ENABLED) return '';
94- $pubkey = file_get_contents(FEDIINFO['pubkey']);
94+ $pubkey = @file_get_contents(FEDIINFO['pubkey']);
9595 if ($pubkey === false) {
9696 throw new \Exception('公開鍵の受取に失敗。パス:'.FEDIINFO['pubkey']);
9797 }
@@ -142,19 +142,31 @@
142142 */
143143 public function getActivity(string $uuid): string {
144144 if (!ACTIVITYPUB_ENABLED) return '';
145- $items = [];
145+ $post = $this->findPublicPost($uuid);
146+ if ($post === null) return '';
146147
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+ }
152150
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);
154166 }
155167
156168 /**
157- * アウトボックスデータを取得する
169+ * アウトボックスデータを取得する(公開投稿のみ、新しい順)
158170 *
159171 * @return string JSONエンコードされたアウトボックスデータ
160172 */
@@ -162,7 +174,7 @@
162174 if (!ACTIVITYPUB_ENABLED) return '';
163175 $items = [];
164176
165- foreach ($this->posts as $post) $items[] = $this->buildCreate($post);
177+ foreach (array_reverse($this->federatablePosts()) as $post) $items[] = $this->buildCreate($post);
166178
167179 $outbox = [
168180 '@context' => [
@@ -207,20 +219,7 @@
207219 */
208220 public function getFollowers(): string {
209221 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());
224223 }
225224
226225 /**
@@ -230,20 +229,29 @@
230229 */
231230 public function getFollowing(): string {
232231 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+ }
234234
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 = [
236244 '@context' => [
237245 'https://www.w3.org/ns/activitystreams',
238246 'https://w3id.org/security/v1',
239247 ],
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,
244252 ];
245253
246- return json_encode($following, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
254+ return json_encode($collection, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
247255 }
248256
249257 /**
@@ -254,24 +262,57 @@
254262 */
255263 public function postInbox(array $activity): void {
256264 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);
267266
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}");
269269 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);
271271 exit;
272272 }
273273
274274 /**
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+ /**
275316 * アクタープロフィールの更新アクティビティを作成する
276317 *
277318 * @return string JSONエンコードされた更新アクティビティ
@@ -539,15 +580,20 @@
539580 * @param string $url 取得するURL
540581 * @return array|null デコード済みのJSON、失敗した場合はnull
541582 */
542- private function fetchRemoteJson(string $url): ?array {
583+ protected function fetchRemoteJson(string $url): ?array {
543584 if (!$this->isSafeRemoteUrl($url)) {
544585 logger(\LogType::ActivityPub, "安全ではないURLを拒否: {$url}");
545586 return null;
546587 }
547588
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+
548594 $curl = new Curl($url);
549595 // リダイレクト先は検証されないので追わない / redirect targets aren't re-checked, so don't follow them
550- $curl->setHeaders(['Accept: application/activity+json'])
596+ $curl->setHeaders($headers)
551597 ->setFollowRedirects(false)
552598 ->setTimeout(10)
553599 ->setCaInfo('/etc/ssl/cert.pem');
@@ -606,40 +652,45 @@
606652 }
607653
608654 /**
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
611658 *
612659 * @param string $url 送信先のURL
613- * @param string $body 送信する本文
660+ * @param string $body 送信する本文(GETは空)
614661 * @param \OpenSSLAsymmetricKey $priv 秘密鍵
662+ * @param string $method post又はget
615663 * @return array|null 「名前: 値」のヘッダー、署名に失敗した場合はnull
616664 */
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 {
618666 $p = parse_url($url);
619667 if (!$p || empty($p['host'])) return null;
668+ $method = strtolower($method);
620669 $target = ($p['path'] ?? '') === '' ? '/' : $p['path'];
621670 if (isset($p['query'])) $target .= '?'.$p['query'];
622671
623672 $headers = [
624673 'Host' => $p['host'].(isset($p['port']) && (int)$p['port'] !== 443 ? ':'.$p['port'] : ''),
625674 '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',
628675 ];
676+ $signed = ['(request-target)', 'host', 'date'];
677+ $lines = ["(request-target): {$method} {$target}", "host: {$headers['Host']}", "date: {$headers['Date']}"];
629678
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+ }
634685
635- if (!openssl_sign($stringToSign, $signature, $priv, OPENSSL_ALGO_SHA256)) {
686+ if (!openssl_sign(implode("\n", $lines), $signature, $priv, OPENSSL_ALGO_SHA256)) {
636687 logger(LogType::ActivityPub, 'エラー:署名に失敗: '.openssl_error_string());
637688 return null;
638689 }
639690
640691 $headers['Signature'] = "keyId=\"{$this->domain}/ap/actor#main-key\","
641692 .'algorithm="rsa-sha256",'
642- .'headers="(request-target) host date digest",'
693+ .'headers="'.implode(' ', $signed).'",'
643694 .'signature="'.base64_encode($signature).'"';
644695
645696 return array_map(fn($k, $v) => "{$k}: {$v}", array_keys($headers), $headers);
@@ -648,16 +699,17 @@
648699 /**
649700 * 秘密鍵を読み込む(1回だけ)
650701 *
702+ * @param bool $log 失敗をログに書くか
651703 * @return \OpenSSLAsymmetricKey|null 失敗した場合はnull
652704 */
653- private function privateKey(): ?\OpenSSLAsymmetricKey {
705+ private function privateKey(bool $log = true): ?\OpenSSLAsymmetricKey {
654706 if ($this->privKey !== null) return $this->privKey;
655707
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;
658710 $key = $pem === false ? false : openssl_pkey_get_private($pem);
659711 if ($key === false) {
660- logger(LogType::ActivityPub, "エラー:秘密鍵「{$file}」の読込に失敗");
712+ if ($log) logger(LogType::ActivityPub, "エラー:秘密鍵「{$file}」の読込に失敗");
661713 return null;
662714 }
663715
@@ -704,7 +756,7 @@
704756 $note['attachment'] = [
705757 [
706758 'digestMultibase' => 'z'.base58btc_encode(hash_file('sha256', $imgpath, true)),
707- 'mediaType' => mime_content_type($imgpath),
759+ 'mediaType' => self::imageType($imgpath),
708760 'type' => 'Image',
709761 'url' => "{$this->domain}/static/article/".rawurlencode($thumb),
710762 ],
@@ -737,6 +789,41 @@
737789 }
738790
739791 /**
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+ /**
740827 * 連合できる投稿(UUIDあり、公開、日付が過去)を古い順に返す
741828 *
742829 * @return array 投稿データの配列
@@ -801,32 +888,23 @@
801888 * フォロワーを受け入れる
802889 *
803890 * @param array $activity フォローアクティビティデータ
804- * @return void
891+ * @return array [HTTPステータス, レスポンスの本文]
805892 */
806- private function acceptFollower(array $activity): void {
893+ private function acceptFollower(array $activity): array {
807894 $followerActor = $activity['actor'] ?? null;
808895 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' => 'アクターがない']];
813897 }
814898
815899 $object = $activity['object'] ?? null;
816900 if (is_array($object)) $object = $object['id'] ?? null;
817901 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' => '不正なフォロー対象']];
822903 }
823904
824905 $inbox = $this->getInboxFromActor($followerActor);
825906 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' => 'フォロワーの受付ボックスの受取に失敗']];
830908 }
831909
832910 $this->storeFollower($followerActor);
@@ -836,7 +914,7 @@
836914 'https://www.w3.org/ns/activitystreams',
837915 'https://w3id.org/security/v1',
838916 ],
839- 'id' => "{$this->domain}/ap/activities/".uniqid(),
917+ 'id' => "{$this->domain}/ap/activities/accept/".uuid(),
840918 'type' => 'Accept',
841919 'actor' => "{$this->domain}/ap/actor",
842920 'object' => $activity,
@@ -845,6 +923,7 @@
845923 if (!$this->sendActivity($inbox, $accept)) {
846924 logger(LogType::ActivityPub, "「{$followerActor}」へのAcceptの送信に失敗");
847925 }
926+ return [200, ['status' => 'OK']];
848927 }
849928
850929 /**
@@ -854,35 +933,89 @@
854933 * @return void
855934 */
856935 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;
861964 }
862965
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);
865978 }
866979
867980 /**
981+ * フォロワーを保存するファイル(テストで変えられる様に)
982+ *
983+ * @return string
984+ */
985+ protected function followersFile(): string {
986+ return ROOT.'/data/followers.txt';
987+ }
988+
989+ /**
868990 * フォロワーのリストを配列として取得する
869991 *
870992 * @return array フォロワーのURLの配列
871993 */
872994 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());
877996 }
878997
879998 /**
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+ /**
8801013 * アクターのインボックスURLを取得する
8811014 *
8821015 * @param string $actor アクターのURL
8831016 * @return string|null インボックスURL、取得に失敗した場合はnull
8841017 */
885- private function getInboxFromActor(string $actor): ?string {
1018+ protected function getInboxFromActor(string $actor): ?string {
8861019 $data = $this->fetchRemoteJson($actor);
8871020 if (!$data) return null;
8881021
@@ -46,8 +46,14 @@
4646 private string $dataDir;
4747 protected int $dataDuration = 1800; // 30分
4848
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.'/';
5157 }
5258
5359 /**
@@ -59,14 +65,14 @@
5965 */
6066 public function set(string $type, array $data): Result {
6167 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('エラー:キャッシュディレクトリの作成に失敗。');
6671
6772 $data['lifespan'] = time();
6873 $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('エラー:キャッシュデータの保存に失敗。');
7076
7177 return Result::Success();
7278 }
@@ -399,9 +399,10 @@
399399 // 名前解決は1回だけ行い、確認したIPに直接接続する(DNSリバインディング対策)
400400 $connectHost = $host;
401401 if ($this->blockPrivate) {
402- $ip = $this->resolvePublicIp($host);
402+ $ip = $this->resolvePublicIp($host, $unresolved);
403403 if ($ip === null) {
404- $this->responseError = "非公開アドレスへの接続を拒否: {$host}";
404+ // 以前は名前解決の失敗も「非公開アドレス」と表示していた
405+ $this->responseError = $unresolved ? "名前解決に失敗: {$host}" : "非公開アドレスへの接続を拒否: {$host}";
405406 $this->method = $originalMethod;
406407 return Result::Error($this->responseError);
407408 }
@@ -735,9 +736,11 @@
735736 * ホスト名を解決し、全てのIPが公開アドレスの場合だけ接続先のIPを返す
736737 *
737738 * @param string $host ホスト名又はIPアドレス
739+ * @param bool|null $unresolved 名前解決に失敗した場合true
738740 * @return string|null 接続先のIP(IPv6は[]付き)、拒否する場合はnull
739741 */
740- private function resolvePublicIp(string $host): ?string {
742+ private function resolvePublicIp(string $host, ?bool &$unresolved = null): ?string {
743+ $unresolved = false;
741744 $host = trim($host, '[]');
742745 if (filter_var($host, FILTER_VALIDATE_IP)) {
743746 $ips = [$host];
@@ -747,7 +750,10 @@
747750 if (isset($r['ipv6'])) $ips[] = $r['ipv6'];
748751 }
749752 }
750- if (empty($ips)) return null;
753+ if (empty($ips)) {
754+ $unresolved = true;
755+ return null;
756+ }
751757
752758 foreach ($ips as $ip) {
753759 if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) return null;
@@ -73,6 +73,8 @@
7373 ];
7474
7575 public function __construct(string $file) {
76+ // ファイルが無くてもfullInfoを読める様に(以前は未初期化のままで、記事のサムネイルが無いとページが落ちた)
77+ $this->fullInfo = (object)['type' => $this->type, 'size' => $this->size, 'bytes' => 0, 'width' => 0, 'height' => 0];
7678 $file = ROOT.$file;
7779 // $file = ROOT.'/public/static/article/mock-screenshot.tga';
7880 if (!file_exists($file)) return;
@@ -38,6 +38,7 @@
3838 namespace Std\Lib;
3939
4040 use LogType;
41+use Result;
4142
4243 class Mailer {
4344 private $socket;
@@ -46,46 +47,104 @@
4647 private ?string $user;
4748 private ?string $pass;
4849
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;
5154
5255 /**
53- * コンストラクタ
56+ * コンストラクタ
57+ *
58+ * @param array|null $info MAILINFOの代わりの設定(テスト用。指定した場合はMAILER_ENABLEDを確認しない)
5459 */
55- public function __construct() {
56- if (!MAILER_ENABLED) {
60+ public function __construct(?array $info = null) {
61+ if ($info === null && !MAILER_ENABLED) {
5762 throw new \Exception("メーラーは無効です。");
5863 }
5964
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'] ?? '');
6471 }
6572
6673 /**
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+ /**
67100 * ソケット接続を開く。
68101 *
69102 * @return void
70103 */
71104 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);
73106 if (!$this->socket) {
74107 $msg = "接続に失敗: {$err} ({$errno})";
75108 logger(LogType::Mailer, $msg);
76109 throw new \Exception($msg);
77110 }
111+ stream_set_timeout($this->socket, $this->timeout);
78112
79- $this->readResponse();
113+ $this->expectGreeting();
80114 }
81115
82116 /**
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+ /**
83137 * ソケット接続を切断する。
84138 *
85139 * @return void
86140 */
87141 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+ }
89148 fclose($this->socket);
90149 }
91150
@@ -96,7 +155,9 @@
96155 * @return void
97156 */
98157 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);
100161
101162 // STARTTLSは対応するかどうか確認
102163 if (strpos($ehloRes, 'STARTTLS') !== false) {
@@ -110,18 +171,18 @@
110171 throw new \Exception($msg);
111172 }
112173
113- $this->sendCommand('EHLO '.gethostname(), 250);
174+ $this->sendCommand('EHLO '.$me, 250);
114175 }
115176
116177 if ($this->user && $this->pass) {
117178 $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);
120181 }
121182 }
122183
123184 /**
124- * TLSハンドシェイクに失敗する。
185+ * メールを送信する。
125186 *
126187 * @param string $to 受信者のメールアドレス。
127188 * @param string $subject メールの件名。
@@ -132,6 +193,7 @@
132193 * @param bool $pgpSign PGPで署名するかどうか(現在は正常に動作していません)。
133194 *
134195 * @return void
196+ * @throws \InvalidArgumentException アドレスや名前に改行等が含まれる場合(ヘッダー注入対策)
135197 */
136198 public function send(
137199 string $to,
@@ -142,13 +204,22 @@
142204 ?string $replyToName = null,
143205 bool $pgpSign = false
144206 ): 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+ }
146214
147215 $this->sendCommand("MAIL FROM: <{$from}>", 250);
148216 $this->sendCommand("RCPT TO: <{$to}>", 250);
149217 $this->sendCommand('DATA', 354);
150218
151219 $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";
152223
153224 $encSubject = mb_encode_mimeheader($subject, 'UTF-8', 'Q');
154225 $headers .= "Subject: {$encSubject}\r\n";
@@ -181,7 +252,8 @@
181252
182253 $headers .= "X-Mailer: 076 Little Beast\r\n";
183254
184- $encBody = quoted_printable_encode($body);
255+ // 改行をCRLFに揃えてからエンコードする
256+ $encBody = quoted_printable_encode(preg_replace('/\r\n|\r|\n/', "\r\n", $body));
185257 $data = $headers."\r\n".$encBody;
186258
187259 if ($pgpSign) {
@@ -194,6 +266,8 @@
194266 ."--{$boundary}--\r\n";
195267 }
196268
269+ // ドットスタッフィング:「.」で始まる行は「..」にする(「.」だけの行で本文が途中で終わらない様に)
270+ $data = preg_replace('/^\./m', '..', $data);
197271 $data .= "\r\n.\r\n";
198272 fwrite($this->socket, $data);
199273
@@ -232,11 +306,13 @@
232306
233307 // 機能性メソッド
234308
235- private function sendCommand(string $command, int $retcode): string {
309+ private function sendCommand(string $command, int $retcode, bool $secret = false): string {
236310 fwrite($this->socket, $command."\r\n");
237311 $res = $this->readResponse();
238312 if (substr($res, 0, 3) != $retcode) {
239- $msg = "「{$command}」に対する予期しないレスポンス: {$res}";
313+ // 認証情報をログに残さない
314+ $shown = $secret ? '(認証情報)' : $command;
315+ $msg = "「{$shown}」に対する予期しないレスポンス: {$res}";
240316 logger(LogType::Mailer, $msg);
241317 throw new \Exception($msg);
242318 }
@@ -247,12 +323,19 @@
247323 private function readResponse(): string {
248324 $res = '';
249325
250- while ($line = fgets($this->socket, 515)) {
326+ while (($line = fgets($this->socket, 515)) !== false) {
251327 $res .= $line;
252328 if (substr($line, 3, 1) == ' ') break; // レスポンスの終了だ
253329 }
254330
255331 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, '@');
256339 }
257340
258341 private function signMessage(string $message): string {
@@ -53,8 +53,8 @@
5353 private int $resellerId = 0;
5454 private int $lastAuth = 0;
5555 // 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';
5858
5959 public function __construct() {
6060 if (!OPENPROVIDER_ENABLED) return;
@@ -86,36 +86,31 @@
8686 return Result::Success('', $cache);
8787 }
8888
89- $curl = new Curl("{$this->BASEURL}/auth/login");
9089 $payload = [
9190 'username' => $this->user,
9291 'password' => $this->pass,
9392 'ip' => $this->ip ?: '0.0.0.0',
9493 ];
9594
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);
10096
10197 $res = $curl->execute();
10298 if (!$res->isSuccess) {
103- $err = "CURLの実行に失敗: ".$curl->message;
99+ $err = "CURLの実行に失敗: ".$curl->getError();
104100 assert_unless_success($res, $err);
105101 return Result::Error($err);
106102 }
107103
108104 $body = $curl->getResponseBody();
109105 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);
113108 return Result::Error($err);
114109 }
115110 assert_not_null($body, "返事ボディーは空です。");
116111
117112 $res = json_decode($body, true);
118- if (isset($res['data']['token'])) {
113+ if (is_array($res) && isset($res['data']['token'])) {
119114 $this->token = $res['data']['token'];
120115 $this->lastAuth = time();
121116 $res['last_auth'] = $this->lastAuth;
@@ -160,13 +155,14 @@
160155 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
161156 $cacheName = 'listdomains';
162157 $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'] ?? []);
164160
165161 $uri = "/domains?".http_build_query($query, '', '&', PHP_QUERY_RFC3986);
166162 $curl = $this->setupCurl($uri);
167163 $res = $this->curlResult($curl);
168164 if (isset($res->data['data']['results'])) {
169- $this->cache->set($cacheName, $res->data);
165+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
170166 return Result::Success('', $res->data['data']['results']);
171167 }
172168
@@ -195,8 +191,7 @@
195191 // 新しい顧客の場合
196192
197193 $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}");
200195 $domain = [
201196 'name' => $parts[0],
202197 'extension' => $parts[1],
@@ -207,23 +202,26 @@
207202 $ns[] = ['name' => $n];
208203 }
209204
205+ // 以前は未定義の変数($owner_handle等)を使い、kys('TODO')で止まっていた
210206 $payload = [
211- 'name' => $domain,
207+ 'domain' => $domain,
212208 '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'],
218214 'name_servers' => $ns,
219- 'autorenew' => $autorenew,
215+ 'autorenew' => $info['autorenew'],
220216 ];
221217
222- $uri = '/domains/';
218+ $uri = '/domains';
223219 $curl = $this->setupCurl($uri, 'POST', $payload);
224- kys('TODO');
225220 $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+ }
227225
228226 return Result::Error('ドメインの確認に失敗。');
229227 }
@@ -239,10 +237,9 @@
239237 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
240238 $domainList = [];
241239 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] === '') {
246243 return Result::Error("不正なドメイン形: {$d}");
247244 }
248245
@@ -337,20 +334,24 @@
337334 */
338335 public function getDomainname(int $id, array $payload = []): Result {
339336 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
337+ // 以前は未定義の$queryと$dataを使い、GETではなくPOSTで送っていた
340338 $cacheName = "getdomain-{$id}";
341339 $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'] ?? []);
343341
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);
345345 $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']);
349349 }
350350
351351 return Result::Error('ドメインの受け取りに失敗。');
352352 }
353353
354+
354355 /**
355356 * ドメイン名の更新
356357 * @todo テスト
@@ -525,13 +526,13 @@
525526 $cacheName = $name && $ext ? "domainadditionaldata-{$name}.{$ext}" : 'domainadditionaldata';
526527
527528 $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);
529530
530531 $uri = "/domains/additional-data?".http_build_query($query, '', '&', PHP_QUERY_RFC3986);
531532 $curl = $this->setupCurl($uri);
532533 $res = $this->curlResult($curl);
533534 if (isset($res->data['data'])) {
534- $this->cache->set($cacheName, $res->data);
535+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
535536 return Result::Success('', $res->data);
536537 }
537538
@@ -556,13 +557,13 @@
556557 $cacheName = $name && $ext ? "domainadditionaldata-{$name}.{$ext}" : 'domainadditionaldata';
557558
558559 $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);
560561
561562 $uri = "/domains/additional-data/customers?".http_build_query($query, '', '&', PHP_QUERY_RFC3986);
562563 $curl = $this->setupCurl($uri);
563564 $res = $this->curlResult($curl);
564565 if (isset($res->data['data'])) {
565- $this->cache->set($cacheName, $res->data);
566+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
566567 return Result::Success('', $res->data);
567568 }
568569
@@ -587,13 +588,13 @@
587588 $cacheName = $name && $ext ? "domainprices-{$name}.{$ext}" : 'domainprices';
588589
589590 $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);
591592
592593 $uri = "/domains/prices?".http_build_query($query, '', '&', PHP_QUERY_RFC3986);
593594 $curl = $this->setupCurl($uri);
594595 $res = $this->curlResult($curl);
595596 if (isset($res->data['data'])) {
596- $this->cache->set($cacheName, $res->data);
597+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
597598 return Result::Success('', $res->data);
598599 }
599600
@@ -635,10 +636,10 @@
635636 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
636637
637638 $uri = "/domains/{$id}/authcode/reset";
638- $curl->setupCurl($uri, 'POST', $payload);
639+ $curl = $this->setupCurl($uri, 'POST', $payload);
639640 $res = $this->curlResult($curl);
640- kys($res);
641- if (isset($res)) {
641+ // 以前はisset($res)で、失敗しても成功を返していた
642+ if ($res->isSuccess) {
642643 return Result::Success('', $res->data);
643644 }
644645
@@ -657,7 +658,7 @@
657658 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
658659 $cacheName = 'listtlds';
659660 $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);
661662
662663 if (empty($query)) $query = [ 'limit' => 10, 'offset' => 0, 'order' => 'ASC' ];
663664 foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false';
@@ -666,7 +667,7 @@
666667 $curl = $this->setupCurl($uri);
667668 $res = $this->curlResult($curl);
668669 if (isset($res->data['data'])) {
669- $this->cache->set($cacheName, $res->data);
670+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
670671 return Result::Success('', $res->data);
671672 }
672673
@@ -684,7 +685,7 @@
684685 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
685686 $cacheName = "gettld-{$tld}";
686687 $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);
688689
689690 if (empty($query)) $query = [ 'limit' => 10, 'offset' => 0 ];
690691 foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false';
@@ -693,7 +694,7 @@
693694 $curl = $this->setupCurl($uri);
694695 $res = $this->curlResult($curl);
695696 if (isset($res->data['data'])) {
696- $this->cache->set($cacheName, $res->data);
697+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
697698 return Result::Success('', $res->data);
698699 }
699700
@@ -713,7 +714,7 @@
713714 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
714715 $cacheName = 'listinvoices';
715716 $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);
717718
718719 if (empty($query)) $query = [ 'limit' => 10, 'offset' => 0 ];
719720 foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false';
@@ -722,7 +723,7 @@
722723 $curl = $this->setupCurl($uri);
723724 $res = $this->curlResult($curl);
724725 if (isset($res->data['data'])) {
725- $this->cache->set($cacheName, $res->data);
726+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
726727 return Result::Success('', $res->data);
727728 }
728729
@@ -741,7 +742,7 @@
741742 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
742743 $cacheName = 'listpayments';
743744 $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);
745746
746747 if (empty($query)) $query = [ 'limit' => 10, 'offset' => 0 ];
747748 foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false';
@@ -750,7 +751,7 @@
750751 $curl = $this->setupCurl($uri);
751752 $res = $this->curlResult($curl);
752753 if (isset($res->data['data'])) {
753- $this->cache->set($cacheName, $res->data);
754+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
754755 return Result::Success('', $res->data);
755756 }
756757
@@ -769,7 +770,7 @@
769770 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
770771 $cacheName = 'listtransactions';
771772 $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);
773774
774775 if (empty($query)) $query = [ 'limit' => 10, 'offset' => 0 ];
775776 foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false';
@@ -778,7 +779,7 @@
778779 $curl = $this->setupCurl($uri);
779780 $res = $this->curlResult($curl);
780781 if (isset($res->data['data'])) {
781- $this->cache->set($cacheName, $res->data);
782+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
782783 return Result::Success('', $res->data);
783784 }
784785
@@ -830,7 +831,7 @@
830831 $curl = $this->setupCurl($uri);
831832 $res = $this->curlResult($curl);
832833 if (isset($res->data['data'])) {
833- $this->cache->set($cacheName, $res->data);
834+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
834835 return Result::Success('', $res->data);
835836 }
836837
@@ -870,7 +871,7 @@
870871 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
871872 $cacheName = "listnameserver-{$domain}";
872873 $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);
874875
875876 if (empty($query)) $query = [ 'limit' => 100, 'offset' => 0, 'order_by.name' => 'asc' ];
876877 foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false';
@@ -879,7 +880,7 @@
879880 $curl = $this->setupCurl($uri);
880881 $res = $this->curlResult($curl);
881882 if (isset($res->data['data'])) {
882- $this->cache->set($cacheName, $res->data);
883+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
883884 return Result::Success('', $res->data);
884885 }
885886
@@ -953,7 +954,7 @@
953954 $curl = $this->setupCurl($uri);
954955 $res = $this->curlResult($curl);
955956 if (isset($res->data['data'])) {
956- $this->cache->set($cacheName, $res->data);
957+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
957958 return Result::Success('', $res->data);
958959 }
959960
@@ -993,7 +994,7 @@
993994 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
994995 $cacheName = "listnsgroup-{$nsGroup}";
995996 $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);
997998
998999 if (empty($query)) $query = [ 'limit' => 100, 'offset' => 0, 'order_by.name' => 'asc' ];
9991000 foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false';
@@ -1002,7 +1003,7 @@
10021003 $curl = $this->setupCurl($uri);
10031004 $res = $this->curlResult($curl);
10041005 if (isset($res->data['data'])) {
1005- $this->cache->set($cacheName, $res->data);
1006+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
10061007 return Result::Success('', $res->data);
10071008 }
10081009
@@ -1075,7 +1076,7 @@
10751076 $res = $this->curlResult($curl);
10761077 if (isset($res->data['data'])) {
10771078 $res->data['query'] = $query;
1078- $this->cache->set($cacheName, $res->data);
1079+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
10791080 return Result::Success('', $res->data);
10801081 }
10811082
@@ -1114,7 +1115,7 @@
11141115 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
11151116 $cacheName = "getdnstemplate-{$id}";
11161117 $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);
11181119
11191120 $uri = "/dns/templates/{$id}";
11201121 $curl = $this->setupCurl($uri);
@@ -1170,7 +1171,7 @@
11701171 $curl = $this->setupCurl($uri);
11711172 $res = $this->curlResult($curl);
11721173 if (isset($res->data['data'])) {
1173- $this->cache->set($cacheName, $res->data);
1174+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
11741175 return Result::Success('', $res->data);
11751176 }
11761177
@@ -1209,7 +1210,7 @@
12091210 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
12101211 $cacheName = "getdnszone-{$domain}";
12111212 $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);
12131214
12141215 if (empty($query)) $query = [ 'limit' => 100, 'offset' => 0, 'order_by.name' => 'asc' ];
12151216 foreach ($query as $k => $v) if (is_bool($v)) $query[$k] = $v ? 'true' : 'false';
@@ -1218,7 +1219,7 @@
12181219 $curl = $this->setupCurl($uri);
12191220 $res = $this->curlResult($curl);
12201221 if (isset($res->data['data'])) {
1221- $this->cache->set($cacheName, $res->data);
1222+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
12221223 return Result::Success('', $res->data);
12231224 }
12241225
@@ -1379,7 +1380,7 @@
13791380 $curl = $this->setupCurl($uri);
13801381 $res = $this->curlResult($curl);
13811382 if (isset($res->data['data'])) {
1382- $this->cache->set($cacheName, $res->data);
1383+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
13831384 return Result::Success('', $res->data);
13841385 }
13851386
@@ -1441,23 +1442,16 @@
14411442 */
14421443 public function getEasydmarcSso(int $orderId): Result {
14431444 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";
14491447 $curl = $this->setupCurl($uri);
14501448 $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);
14571450
1458- return Result::Error('Easydmarcの削除に失敗。');
1451+ return Result::Error('EasydmarcのSSOの受け取りに失敗。');
14591452 }
14601453
1454+
14611455 // メールテンプレート
14621456
14631457 //// メール
@@ -1481,7 +1475,7 @@
14811475 $res = $this->curlResult($curl);
14821476 if (isset($res->data['data'])) {
14831477 $res->data['query'] = $query;
1484- $this->cache->set($cacheName, $res->data);
1478+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
14851479 return Result::Success('', $res->data);
14861480 }
14871481
@@ -1573,7 +1567,7 @@
15731567 $res = $this->curlResult($curl);
15741568 if (isset($res->data['data'])) {
15751569 $res->data['query'] = $query;
1576- $this->cache->set($cacheName, $res->data);
1570+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
15771571 return Result::Success('ライセンス一覧の受け取りに成功。', $res->data);
15781572 }
15791573
@@ -1595,7 +1589,8 @@
15951589 $uri = "/licenses/hwids/reset/{$product}/{$keyId}";
15961590 $curl = $this->setupCurl($uri, 'POST', $payload);
15971591 $res = $this->curlResult($curl);
1598- if (isset($res)) {
1592+ // 以前はisset($res)で、失敗しても成功を返していた
1593+ if ($res->isSuccess) {
15991594 $this->cache->murder('listlicenses');
16001595 return Result::Success('HWIDの再設定に成功。', $res->data);
16011596 }
@@ -1622,7 +1617,7 @@
16221617 $res = $this->curlResult($curl);
16231618 if (isset($res->data['data'])) {
16241619 $res->data['query'] = $query;
1625- $this->cache->set($cacheName, $res->data);
1620+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
16261621 return Result::Success('商品一覧の受け取りに成功。', $res->data);
16271622 }
16281623
@@ -1648,7 +1643,7 @@
16481643 $res = $this->curlResult($curl);
16491644 if (isset($res->data['data'])) {
16501645 $res->data['query'] = $query;
1651- $this->cache->set($cacheName, $res->data);
1646+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
16521647 return Result::Success('Pleskライセンス一覧の受け取りに成功。', $res->data);
16531648 }
16541649
@@ -1668,7 +1663,8 @@
16681663 $uri = "/licenses/plesk";
16691664 $curl = $this->setupCurl($uri, 'POST', $payload);
16701665 $res = $this->curlResult($curl);
1671- if (isset($res)) {
1666+ // 以前はisset($res)で、失敗しても成功を返していた
1667+ if ($res->isSuccess) {
16721668 $this->cache->murder('listlicenses');
16731669 $this->cache->murder('listplesklicenses');
16741670 return Result::Success('Pleskライセンスの作成に成功。', $res->data);
@@ -1799,7 +1795,7 @@
17991795 $res = $this->curlResult($curl);
18001796 if (isset($res->data['data'])) {
18011797 $res->data['query'] = $query;
1802- $this->cache->set($cacheName, $res->data);
1798+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
18031799 return Result::Success('', $res->data);
18041800 }
18051801
@@ -1819,7 +1815,8 @@
18191815 $uri = '/contacts';
18201816 $curl = $this->setupCurl($uri, 'POST', $payload);
18211817 $res = $this->curlResult($curl);
1822- if (isset($res)) {
1818+ // 以前はisset($res)で、失敗しても成功を返していた
1819+ if ($res->isSuccess) {
18231820 $this->cache->murder('listcontacts');
18241821 return Result::Success('', $res->data);
18251822 }
@@ -1920,7 +1917,7 @@
19201917 $res = $this->curlResult($curl);
19211918 if (isset($res->data['data'])) {
19221919 $res->data['query'] = $query;
1923- $this->cache->set($cacheName, $res->data);
1920+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
19241921 return Result::Success('', $res->data);
19251922 }
19261923
@@ -1939,7 +1936,8 @@
19391936 $uri = '/customers';
19401937 $curl = $this->setupCurl($uri, 'POST', $payload);
19411938 $res = $this->curlResult($curl);
1942- if (isset($res)) {
1939+ // 以前はisset($res)で、失敗しても成功を返していた
1940+ if ($res->isSuccess) {
19431941 $this->cache->murder('listcustomers');
19441942 return Result::Success('', $res->data);
19451943 }
@@ -2039,7 +2037,7 @@
20392037 $res = $this->curlResult($curl);
20402038 if (isset($res->data['data'])) {
20412039 $res->data['query'] = $query;
2042- $this->cache->set($cacheName, $res->data);
2040+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
20432041 return Result::Success('', $res->data);
20442042 }
20452043
@@ -2111,7 +2109,7 @@
21112109 $res = $this->curlResult($curl);
21122110 if (isset($res->data['data'])) {
21132111 $res->data['query'] = $query;
2114- $this->cache->set($cacheName, $res->data);
2112+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
21152113 return Result::Success('', $res->data);
21162114 }
21172115
@@ -2152,7 +2150,7 @@
21522150 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
21532151 $cacheName = 'listsettings';
21542152 $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);
21562154
21572155 $uri = '/resellers/settings';
21582156 $curl = $this->setupCurl($uri);
@@ -2184,7 +2182,7 @@
21842182 $res = $this->curlResult($curl);
21852183 if (isset($res->data['data'])) {
21862184 $res->data['query'] = $query;
2187- $this->cache->set($cacheName, $res->data);
2185+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
21882186 return Result::Success('', $res->data);
21892187 }
21902188
@@ -2212,7 +2210,7 @@
22122210 $res = $this->curlResult($curl);
22132211 if (isset($res->data['data'])) {
22142212 $res->data['query'] = $query;
2215- $this->cache->set($cacheName, $res->data);
2213+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
22162214 return Result::Success('', $res->data);
22172215 }
22182216
@@ -2278,7 +2276,8 @@
22782276 $uri = '/spam-expert/domains';
22792277 $curl = $this->setupCurl($uri, 'POST', $payload);
22802278 $res = $this->curlResult($curl);
2281- if (isset($res)) {
2279+ // 以前はisset($res)で、失敗しても成功を返していた
2280+ if ($res->isSuccess) {
22822281 $this->cache->murder('listdomains');
22832282 return Result::Success('', $res->data);
22842283 }
@@ -2306,7 +2305,7 @@
23062305 $res = $this->curlResult($curl);
23072306 if (isset($res->data['data'])) {
23082307 $res->data['domain'] = $domain;
2309- $this->cache->set($cacheName, $res->data);
2308+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
23102309 return Result::Success('', $res->data);
23112310 }
23122311
@@ -2381,7 +2380,7 @@
23812380 $res = $this->curlResult($curl);
23822381 if (isset($res->data['data'])) {
23832382 $res->data['query'] = $query;
2384- $this->cache->set($cacheName, $res->data);
2383+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
23852384 return Result::Success('', $res->data);
23862385 }
23872386
@@ -2403,7 +2402,8 @@
24032402 $uri = '/ssl/csr';
24042403 $curl = $this->setupCurl($uri, 'POST', $payload);
24052404 $res = $this->curlResult($curl);
2406- if (isset($res)) {
2405+ // 以前はisset($res)で、失敗しても成功を返していた
2406+ if ($res->isSuccess) {
24072407 return Result::Success('', $res->data);
24082408 }
24092409
@@ -2423,7 +2423,8 @@
24232423 $uri = '/ssl/csr/decode';
24242424 $curl = $this->setupCurl($uri, 'POST', ['csr' => $csr]);
24252425 $res = $this->curlResult($curl);
2426- if (isset($res)) {
2426+ // 以前はisset($res)で、失敗しても成功を返していた
2427+ if ($res->isSuccess) {
24272428 return Result::Success('', $res->data);
24282429 }
24292430
@@ -2451,7 +2452,7 @@
24512452 $res = $this->curlResult($curl);
24522453 if (isset($res->data['data'])) {
24532454 $res->data['query'] = $query;
2454- $this->cache->set($cacheName, $res->data);
2455+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
24552456 return Result::Success('連絡先一覧の受け取りに成功。', $res->data);
24562457 }
24572458
@@ -2471,7 +2472,8 @@
24712472 $uri = '/ssl/orders';
24722473 $curl = $this->setupCurl($uri, 'POST', $payload);
24732474 $res = $this->curlResult($curl);
2474- if (isset($res)) {
2475+ // 以前はisset($res)で、失敗しても成功を返していた
2476+ if ($res->isSuccess) {
24752477 $this->cache->murder('listsslorders');
24762478 return Result::Success('SSL注文の作成に成功。', $res->data);
24772479 }
@@ -2542,7 +2544,8 @@
25422544 $uri = "/ssl/orders/{$id}/cancel";
25432545 $curl = $this->setupCurl($uri, 'POST', $payload);
25442546 $res = $this->curlResult($curl);
2545- if (isset($res)) {
2547+ // 以前はisset($res)で、失敗しても成功を返していた
2548+ if ($res->isSuccess) {
25462549 $this->cache->murder("getsslorder-{$id}");
25472550 $this->cache->murder('listsslorders');
25482551 return Result::Success('SSL注文のキャンセルに成功。', $res->data);
@@ -2565,7 +2568,8 @@
25652568 $uri = "/ssl/orders/{$id}/reissue";
25662569 $curl = $this->setupCurl($uri, 'POST', $payload);
25672570 $res = $this->curlResult($curl);
2568- if (isset($res)) {
2571+ // 以前はisset($res)で、失敗しても成功を返していた
2572+ if ($res->isSuccess) {
25692573 $this->cache->murder("getsslorder-{$id}");
25702574 $this->cache->murder('listsslorders');
25712575 return Result::Success('SSL注文の再発行に成功。', $res->data);
@@ -2588,7 +2592,8 @@
25882592 $uri = "/ssl/orders/{$id}/renew";
25892593 $curl = $this->setupCurl($uri, 'POST', $payload);
25902594 $res = $this->curlResult($curl);
2591- if (isset($res)) {
2595+ // 以前はisset($res)で、失敗しても成功を返していた
2596+ if ($res->isSuccess) {
25922597 $this->cache->murder("getsslorder-{$id}");
25932598 $this->cache->murder('listsslorders');
25942599 return Result::Success('SSL注文の更新に成功。', $res->data);
@@ -2613,7 +2618,8 @@
26132618 $uri = "/ssl/orders/{$id}/approver-email";
26142619 $curl = $this->setupCurl($uri, 'PUT', ['id' => $id, 'approver_email' => $approverEmail]);
26152620 $res = $this->curlResult($curl);
2616- if (isset($res)) {
2621+ // 以前はisset($res)で、失敗しても成功を返していた
2622+ if ($res->isSuccess) {
26172623 $this->cache->murder('listapproveremails');
26182624 return Result::Success('', $res->data);
26192625 }
@@ -2634,7 +2640,8 @@
26342640 $uri = "/ssl/orders/{$id}/approver-email/resend";
26352641 $curl = $this->setupCurl($uri, 'POST', ['id' => $id]);
26362642 $res = $this->curlResult($curl);
2637- if (isset($res)) {
2643+ // 以前はisset($res)で、失敗しても成功を返していた
2644+ if ($res->isSuccess) {
26382645 $this->cache->murder('listapproveremails');
26392646 return Result::Success('', $res->data);
26402647 }
@@ -2657,7 +2664,8 @@
26572664 $uri = "/ssl/orders/{$id}/otp-tokens";
26582665 $curl = $this->setupCurl($uri, 'POST');
26592666 $res = $this->curlResult($curl);
2660- if (isset($res)) {
2667+ // 以前はisset($res)で、失敗しても成功を返していた
2668+ if ($res->isSuccess) {
26612669 return Result::Success('', $res->data);
26622670 }
26632671
@@ -2685,7 +2693,7 @@
26852693 $res = $this->curlResult($curl);
26862694 if (isset($res->data['data'])) {
26872695 $res->data['query'] = $query;
2688- $this->cache->set($cacheName, $res->data);
2696+ $this->cache->set($cacheName, $res->data + ['query' => $query]);
26892697 return Result::Success('', $res->data);
26902698 }
26912699
@@ -2740,12 +2748,12 @@
27402748
27412749 //////////
27422750
2743- private function setupCurl(string $uri, string $resType = 'GET', array $payload = []): Curl {
2751+ protected function setupCurl(string $uri, string $resType = 'GET', array $payload = []): Curl {
27442752 $curl = new Curl("{$this->BASEURL}{$uri}");
27452753
27462754 $curl->setMethod($resType);
27472755 if (!empty($payload)) $curl->setPostRaw(json_encode($payload));
2748- $curl->setBearerAuth($this->token);
2756+ if ($this->token !== '') $curl->setBearerAuth($this->token);
27492757 $curl->addHeader('Content-Type', 'application/json');
27502758 $curl->addHeader('Accept', 'application/json');
27512759
@@ -2755,20 +2763,30 @@
27552763 private function curlResult(Curl &$curl): Result {
27562764 $res = $curl->execute();
27572765 if (!$res->isSuccess) {
2758- $err = "CURL実行に失敗: {$curl->message}";
2766+ $err = "CURL実行に失敗: {$curl->getError()}";
27592767 assert_unless_success($res, $err);
27602768 return Result::Error($err);
27612769 }
27622770
27632771 $body = $curl->getResponseBody();
27642772 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);
27692776 }
27702777 assert_not_null($body, "返事ボディーは空です。");
27712778
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}";
27732791 }
27742792 }