url = $url; } } /** * リクエスト先のURLを設定する * * @param string $url リクエスト先のURL * @return Curl このインスタンス(メソッドチェーン用) */ public function setUrl(string $url): Curl { $this->url = $url; return $this; } /** * リクエストメソッドを設定する * * @param string $method GE又はPOST等のHTTPメソッド * @return Curl このインスタンス(メソッドチェーン用) */ public function setMethod(string $method): Curl { $this->method = strtoupper($method); return $this; } /** * リクエストのタイムアウト秒数を設定する * * @param int $seconds タイムアウト秒数 * @return Curl このインスタンス(メソッドチェーン用) */ public function setTimeout(int $seconds): Curl { $this->timeout = (int)$seconds; return $this; } /** * リクエストヘッダーを設定する * * @param array $headers リクエストヘッダーの配列 * @return Curl このインスタンス(メソッドチェーン用) */ public function setHeaders(array $headers): Curl { $this->headers = $headers; return $this; } /** * 単一のヘッダーを追加する * * @param string $name ヘッダー名 * @param mixed $value ヘッダー値 * @return Curl このインスタンス(メソッドチェーン用) */ public function addHeader(string $name, mixed $value): Curl { $this->headers[$name] = $value; return $this; } /** * リクエストのクッキーを設定する * * @param array $cookies クッキーの配列 * @return Curl このインスタンス(メソッドチェーン用) */ public function setCookies(array $cookies): Curl { $this->cookies = $cookies; return $this; } /** * 単一のクッキーを追加する * * @param string $name クッキー名 * @param mixed $value クッキー値 * @return Curl このインスタンス(メソッドチェーン用) */ public function addCookie(string $name, mixed $value): Curl { $this->cookies[$name] = $value; return $this; } /** * POSTフィールドを設定する * * @param array $fields POSTデータの配列 * @return Curl このインスタンス(メソッドチェーン用) */ public function setPostFields(array $fields): Curl { $this->postFields = $fields; return $this; } /** * 生のPOSTデータを設定する * * @param string $data 生のPOSTデータ * @return Curl このインスタンス(メソッドチェーン用) */ public function setPostRaw(string $data): Curl { $this->postRaw = $data; return $this; } /** * ユーザーエージェントを設定する * * @param string $userAgent カスタムユーザーエージェント * @return Curl このインスタンス(メソッドチェーン用) */ public function setUserAgent(string $userAgent): Curl { $this->userAgent = $userAgent; return $this; } /** * リダイレクトを追跡するかどうかを設定する * * @param bool $follow 追跡するかどうか(デフォルトはtrue) * @return Curl このインスタンス(メソッドチェーン用) */ public function setFollowRedirects(bool $follow): Curl { $this->followRedirects = (bool)$follow; return $this; } /** * 追跡するリダイレクトの最大数を設定する * * @param int $max リダイレクトの最大数 * @return Curl このインスタンス(メソッドチェーン用) */ public function setMaxRedirects(int $max): Curl { $this->maxRedirects = (int)$max; return $this; } /** * SSL証明書を検証するかどうかを設定する * * @param bool $verify SSL検証を行うかどうか(デフォルトはtrue) * @return Curl このインスタンス(メソッドチェーン用) */ public function setVerifySSL(bool $verify): Curl { $this->verifySSL = (bool)$verify; return $this; } /** * 基本認証の資格情報を設定する * * @param string $username ユーザー名 * @param string $password パスワード * @return Curl このインスタンス(メソッドチェーン用) */ public function setBasicAuth(string $username, string $password): Curl { $this->username = $username; $this->password = $password; return $this; } /** * リファラーURLを設定する * * @param string $referer リファラーURL * @return Curl このインスタンス(メソッドチェーン用) */ public function setReferer(string $referer): Curl { $this->referer = $referer; return $this; } /** * 詳細ログを有効にする * * @param bool $verbose 詳細ログを有効にするかどうか * @return Curl このインスタンス(メソッドチェーン用) */ public function setVerbose(bool $verbose): Curl { $this->verbose = (bool)$verbose; return $this; } /** * エラー出力先を設定する * * @param resource $handle エラー出力先のファイルハンドル * @return Curl このインスタンス(メソッドチェーン用) */ public function setStderr($handle): Curl { $this->stderr = $handle; return $this; } /** * SSL証明書のファイルパスを設定する * * @param string $path 証明書ファイルのパス * @return Curl このインスタンス(メソッドチェーン用) */ public function setCaInfo(string $path): Curl { $this->caInfoPath = $path; return $this; } /** * リクエストを実行する * * @return Result 成功または失敗 */ public function execute(): \Result { if (!CURL_ENABLED) return \Result::Error('エラー:認証システムは無効です。'); if (empty($this->url)) { $this->responseError = 'URLがありません'; return \Result::Error($this->responseError); } // レスポンスデータのリセット $this->responseHeaders = []; $this->responseBody = ''; $this->responseCode = 0; $this->responseError = ''; $this->info = [ 'url' => $this->url, 'content_type' => '', 'http_code' => 0, 'header_size' => 0, 'request_size' => 0, 'total_time' => 0, 'redirect_count' => 0, 'redirect_url' => '', ]; $startTime = microtime(true); // ソケットベースの実装を使用する $redirectCount = 0; $currentUrl = $this->url; $originalMethod = $this->method; do { if ($this->verbose && $this->stderr) { fwrite($this->stderr, "* 接続中: {$currentUrl}\n"); } $parsed = parse_url($currentUrl); if (!$parsed) { $this->responseError = "無効なURL: {$currentUrl}"; return \Result::Error($this->responseError); } $scheme = isset($parsed['scheme']) ? strtolower($parsed['scheme']) : 'http'; $host = $parsed['host']; $port = isset($parsed['port']) ? $parsed['port'] : ($scheme === 'https' ? 443 : 80); $path = isset($parsed['path']) ? $parsed['path'] : '/'; if (isset($parsed['query'])) { $path .= '?'.$parsed['query']; } // Basic認証 $authHeader = ''; if (!empty($this->username) && !empty($this->password)) { $authHeader = "Authorization: Basic " .base64_encode($this->username.':'.$this->password)."\r\n"; } elseif (isset($parsed['user']) && isset($parsed['pass'])) { $authHeader = "Authorization: Basic " .base64_encode($parsed['user'].':'.$parsed['pass'])."\r\n"; } // 送信するHTTPリクエストを構築 $method = $this->method; $httpData = ''; if ($method === 'POST' || $method === 'PUT') { if (!empty($this->postRaw)) { $httpData = $this->postRaw; } elseif (!empty($this->postFields)) { $httpData = http_build_query($this->postFields); if (!isset($this->headers['Content-Type'])) { $this->headers['Content-Type'] = 'application/x-www-form-urlencoded'; } } $this->headers['Content-Length'] = strlen($httpData); } // HTTPリクエストを構築 $accept = 'Accept: */*'; foreach ($this->headers as $h) { if (str_contains($h, 'Accept:')) $accept = $h; } $request = "{$method} {$path} HTTP/1.1\r\n"; $request .= "Host: {$host}\r\n"; $request .= "User-Agent: {$this->userAgent}\r\n"; $request .= "{$accept}\r\n"; $request .= "Connection: close\r\n"; if (!empty($authHeader)) { $request .= $authHeader; } // ヘッダーを追加 foreach ($this->headers as $name => $value) { $request .= "{$name}: {$value}\r\n"; } // リファラーが設定されていれば追加 if (!empty($this->referer) && !isset($this->headers['Referer'])) { $request .= "Referer: {$this->referer}\r\n"; } // クッキーヘッダーを追加 if (!empty($this->cookies) && !isset($this->headers['Cookie'])) { $cookieStrings = []; foreach ($this->cookies as $name => $value) { $cookieStrings[] = $name.'='.urlencode($value); } $request .= 'Cookie: '.implode('; ', $cookieStrings)."\r\n"; } $request .= "\r\n"; // POSTデータを追加 if ($method === 'POST' || $method === 'PUT') { $request .= $httpData; } if ($this->verbose && $this->stderr) { fwrite($this->stderr, "* リクエストヘッダー:\n{$request}\n"); } // ソケット接続を確立 $errno = 0; $errstr = ''; if ($scheme === 'https') { $sslOptions = [ 'verify_peer' => $this->verifySSL, 'verify_peer_name' => $this->verifySSL ]; if (!empty($this->caInfoPath) && file_exists($this->caInfoPath)) { $sslOptions['cafile'] = $this->caInfoPath; } $context = stream_context_create(['ssl' => $sslOptions]); $socket = @stream_socket_client( "tls://{$host}:{$port}", $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT, $context ); } else { $socket = @fsockopen($host, $port, $errno, $errstr, $this->timeout); } if (!$socket) { $this->responseError = "接続出来ません: {$errstr} ({$errno})"; if ($this->verbose && $this->stderr) { fwrite($this->stderr, "* エラー: {$this->responseError}\n"); } return \Result::Error($this->responseError); } // タイムアウトを設定 stream_set_timeout($socket, $this->timeout); // リクエストを送信 fwrite($socket, $request); // レスポンスを読み込む $rawResponse = ''; $headers = ''; $body = ''; $headersComplete = false; // ヘッダーとボディを分けて読み込む while (!feof($socket)) { $line = fgets($socket); if ($line === false) { break; } $rawResponse .= $line; if (!$headersComplete) { if (trim($line) === '') { $headersComplete = true; } else { $headers .= $line; } } else { $body .= $line; } } fclose($socket); // レスポンスヘッダーを解析 $headerLines = explode("\r\n", $headers); // ステータスコードを取得 $statusLine = isset($headerLines[0]) ? $headerLines[0] : ''; $statusParts = explode(' ', $statusLine, 3); $this->responseCode = isset($statusParts[1]) ? (int)$statusParts[1] : 0; $this->info['http_code'] = $this->responseCode; // ヘッダーを解析 $this->responseHeaders = []; $redirectUrl = ''; foreach ($headerLines as $index => $header) { if ($index === 0) continue; if (strpos($header, ':') !== false) { list($name, $value) = explode(':', $header, 2); $name = trim($name); $value = trim($value); $this->responseHeaders[$name] = $value; if (strtolower($name) === 'content-type') { $this->info['content_type'] = $value; } // リダイレクトをチェック if ($this->followRedirects && strtolower($name) === 'location' && $this->responseCode >= 300 && $this->responseCode < 400) { $redirectUrl = $value; // 相対URLを絶対URLに変換 if (strpos($redirectUrl, 'http') !== 0) { if ($redirectUrl[0] === '/') { $redirectUrl = "{$scheme}://{$host}" .($port != 80 && $port != 443 ? ":{$port}" : '').$redirectUrl; } else { $redirectUrl = "{$scheme}://{$host}" .($port != 80 && $port != 443 ? ":{$port}" : '') .dirname($path).'/'.$redirectUrl; } } $this->info['redirect_url'] = $redirectUrl; } } } $this->info['header_size'] += strlen($headers); $this->responseBody .= $body; if ($this->verbose && $this->stderr) { fwrite($this->stderr, "* レスポンスコード: {$this->responseCode}\n"); fwrite($this->stderr, "* レスポンスヘッダー:\n{$headers}\n"); if (!empty($redirectUrl)) { fwrite($this->stderr, "* リダイレクト先: {$redirectUrl}\n"); } } // リダイレクトが必要な場合 if (!empty($redirectUrl) && $redirectCount < $this->maxRedirects) { $currentUrl = $redirectUrl; $redirectCount++; $this->info['redirect_count'] = $redirectCount; // 302や303リダイレクトはGETにメソッドを変更 if ($this->responseCode == 302 || $this->responseCode == 303) { $this->method = 'GET'; $this->postRaw = ''; $this->postFields = []; } } else { break; } } while (true); // リクエスト完了後、元のメソッドに戻す $this->method = $originalMethod; $this->info['total_time'] = microtime(true) - $startTime; return \Result::Success(); } /** * レスポンスボディを取得する * * @return string レスポンスボディ */ public function getResponseBody(): string { return $this->responseBody; } /** * レスポンスヘッダーを取得する * * @return array レスポンスヘッダーの配列 */ public function getResponseHeaders(): array { return $this->responseHeaders; } /** * HTTPレスポンスコードを取得する * * @return int HTTPレスポンスコード */ public function getResponseCode(): int { return $this->responseCode; } /** * エラーメッセージがあれば取得する * * @return string エラーメッセージ */ public function getError(): string { return $this->responseError; } /** * リクエスト/レスポンス情報を取得する * * @return array 情報の配列 */ public function getInfo(): array { return $this->info; } // 機能性メソッド /** * リダイレクトURLを確認する * * @param string $name ヘッダー名 * @param string $value ヘッダー値 * @param string $currentUrl 現在のURL * @return string リダイレクトURL、リダイレクトがない場合は空文字 */ private function checkReds(string $name, string $value, string $currentUrl): string { $redirectUrl = ''; if ($this->followRedirects && (strtolower($name) === 'location' && $this->responseCode >= 300 && $this->responseCode < 400)) { $redirectUrl = $value; if (strpos($redirectUrl, 'http') !== 0) { if ($redirectUrl[0] === '/') { $parsed = parse_url($currentUrl); $redirectUrl = $parsed['scheme'].'://'.$parsed['host'] .(isset($parsed['port']) ? ':'.$parsed['port'] : '') .$redirectUrl; } else { $redirectUrl = dirname($currentUrl).'/'.$redirectUrl; } } } return $redirectUrl; } /** * ヘッダー文字列を構築する * * @return string 構築されたヘッダー文字列 */ private function buildHeaderString(): string { $headers = []; // ユーザー指定のヘッダーを追加 foreach ($this->headers as $name => $value) { $headers[] = "{$name}: {$value}"; } // リファラーが設定されていれば追加 if (!empty($this->referer) && !isset($this->headers['Referer'])) { $headers[] = "Referer: {$this->referer}"; } // 必要に応じてクッキーヘッダーを追加 if (!empty($this->cookies) && !isset($this->headers['Cookie'])) { $cookieStrings = []; foreach ($this->cookies as $name => $value) { $cookieStrings[] = $name.'='.urlencode($value); } $headers[] = 'Cookie: '.implode('; ', $cookieStrings); } return implode("\r\n", $headers)."\r\n"; } /** * レスポンスを解析してヘッダーとボディに分割する * * @param string $response 完全なHTTPレスポンス * @return array [ヘッダー配列, ボディ文字列] */ private function parseResponse(string $response): array { $parts = explode("\r\n\r\n", $response, 2); if (count($parts) < 2) { return [[], '']; } $headers = explode("\r\n", $parts[0]); $body = $parts[1]; // チャンク転送エンコーディングを処理 if (isset($this->responseHeaders['Transfer-Encoding']) && strtolower($this->responseHeaders['Transfer-Encoding']) === 'chunked') { $body = $this->decodeChunkedBody($body); } return [$headers, $body]; } /** * チャンク転送エンコーディングされたボディをデコードする * * @param string $body チャンクエンコードされたボディ * @return string デコードされたボディ */ private function decodeChunkedBody(string $body): string { $decodedBody = ''; $position = 0; while ($position < strlen($body)) { $lineEnd = strpos($body, "\r\n", $position); if ($lineEnd === false) { break; } $chunkSize = hexdec(substr($body, $position, $lineEnd - $position)); if ($chunkSize === 0) { break; } $position = $lineEnd + 2; $decodedBody .= substr($body, $position, $chunkSize); $position += $chunkSize + 2; // チャンクサイズ + CRLF } return $decodedBody; } }