Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
95.00% |
228 / 240 |
|
71.43% |
5 / 7 |
CRAP | |
0.00% |
0 / 1 |
| Command | |
95.00% |
228 / 240 |
|
71.43% |
5 / 7 |
141 | |
0.00% |
0 / 1 |
| clientToCommand | |
89.04% |
65 / 73 |
|
0.00% |
0 / 1 |
54.42 | |||
| commandToClient | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
6 | |||
| parseCommandOptions | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
| extractCommandOptionValues | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
15 | |||
| convertCommandOptions | |
96.04% |
97 / 101 |
|
0.00% |
0 / 1 |
54 | |||
| trimQuotes | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
5 | |||
| addQuotes | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
5 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Pop PHP Framework (https://www.popphp.org/) |
| 4 | * |
| 5 | * @link https://github.com/popphp/popphp-framework |
| 6 | * @author Nick Sagona, III <dev@noladev.com> |
| 7 | * @copyright Copyright (c) 2009-2026 NOLA Interactive, LLC. |
| 8 | * @license https://www.popphp.org/license New BSD License |
| 9 | */ |
| 10 | |
| 11 | /** |
| 12 | * @namespace |
| 13 | */ |
| 14 | namespace Pop\Http\Client\Handler\Curl; |
| 15 | |
| 16 | use Pop\Http\Auth; |
| 17 | use Pop\Http\Client; |
| 18 | use Pop\Http\Client\Request; |
| 19 | use Pop\Http\Client\Handler\Curl; |
| 20 | use Pop\Http\Promise; |
| 21 | |
| 22 | /** |
| 23 | * HTTP client curl command class |
| 24 | * |
| 25 | * @category Pop |
| 26 | * @package Pop\Http |
| 27 | * @author Nick Sagona, III <dev@noladev.com> |
| 28 | * @copyright Copyright (c) 2009-2026 NOLA Interactive, LLC. |
| 29 | * @license https://www.popphp.org/license New BSD License |
| 30 | * @version 5.3.8 |
| 31 | */ |
| 32 | class Command |
| 33 | { |
| 34 | |
| 35 | /** |
| 36 | * Create a compatible command string to execute with the curl CLI application |
| 37 | * |
| 38 | * @param Client $client |
| 39 | * @return string |
| 40 | */ |
| 41 | public static function clientToCommand(Client $client): string |
| 42 | { |
| 43 | $command = 'curl'; |
| 44 | $currentOptions = []; |
| 45 | |
| 46 | // If client has a Curl handler, get current options before reset |
| 47 | if (($client->hasHandler()) && ($client->getHandler() instanceof Curl) && ($client->getHandler()->hasOptions())) { |
| 48 | $currentOptions = $client->getHandler()->getOptions(); |
| 49 | } |
| 50 | |
| 51 | $client->prepare(); |
| 52 | |
| 53 | if (!($client->getHandler() instanceof Curl)) { |
| 54 | throw new Exception('Error: The client object must use a Curl handler.'); |
| 55 | } |
| 56 | |
| 57 | // Set return header |
| 58 | if ($client->getHandler()->isReturnHeader()) { |
| 59 | $command .= ' -i'; |
| 60 | } |
| 61 | |
| 62 | // Set method |
| 63 | $method = $client->getRequest()->getMethod(); |
| 64 | $command .= ' -X ' . $method; |
| 65 | |
| 66 | // Handle insecure settings |
| 67 | if (($client->hasOption('verify_peer') && ($client->getOption('verify_peer'))) || |
| 68 | ($client->hasOption('allow_self_signed') && ($client->getOption('allow_self_signed'))) || |
| 69 | ($client->getHandler()->hasOption(CURLOPT_SSL_VERIFYHOST) && (!$client->getHandler()->getOption(CURLOPT_SSL_VERIFYHOST))) || |
| 70 | ($client->getHandler()->hasOption(CURLOPT_SSL_VERIFYPEER) && (!$client->getHandler()->getOption(CURLOPT_SSL_VERIFYPEER)))) { |
| 71 | $command .= ' --insecure'; |
| 72 | } |
| 73 | |
| 74 | // Handle basic auth |
| 75 | if (($client->hasAuth()) && ($client->getAuth()->isBasic())) { |
| 76 | $command .= ' --basic -u "' . $client->getAuth()->getUsername() . ':' . $client->getAuth()->getPassword() . '"'; |
| 77 | if ($client->getRequest()->hasHeader('Authorization')) { |
| 78 | $client->getRequest()->removeHeader('Authorization'); |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | // Handle headers |
| 83 | if ($client->getRequest()->hasHeaders()) { |
| 84 | foreach ($client->getRequest()->getHeaders() as $header) { |
| 85 | if ((!str_contains($header->getValueAsString(), 'multipart/form-data')) && |
| 86 | (!str_contains($header->getValueAsString(), 'x-www-form-urlencoded')) && ($header->getName() != 'Content-Length')) { |
| 87 | $command .= ' --header "' . $header . '"'; |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | // Handle data |
| 93 | if ($client->getRequest()->hasData()) { |
| 94 | // Multipart form data |
| 95 | if ($client->getRequest()->isMultipart()) { |
| 96 | $data = $client->getRequest()->getData()->toArray(); |
| 97 | foreach ($data as $key => $value) { |
| 98 | $command .= (isset($value['filename']) && file_exists($value['filename'])) ? |
| 99 | ' -F "' . $key . '=@' . $value['filename'] . '"' : |
| 100 | ' -F "' . http_build_query([$key => $value]) . '"'; |
| 101 | } |
| 102 | // JSON data |
| 103 | } else if ($client->getRequest()->isJson()) { |
| 104 | $data = $client->getRequest()->getData()->toArray(); |
| 105 | foreach ($data as $key => $datum) { |
| 106 | if (isset($datum['filename']) && file_exists($datum['filename'])) { |
| 107 | $command .= ' --data @' . $datum['filename']; |
| 108 | unset($data[$key]); |
| 109 | } |
| 110 | } |
| 111 | if (!empty($data)) { |
| 112 | $json = json_encode($data); |
| 113 | if (str_contains($json, "'")) { |
| 114 | $json = str_replace("'", "\\'", $json); |
| 115 | } |
| 116 | $command .= " --data '" . $json . "'"; |
| 117 | } |
| 118 | // XML data |
| 119 | } else if ($client->getRequest()->isXml()) { |
| 120 | $data = $client->getRequest()->getData()->toArray(); |
| 121 | foreach ($data as $key => $datum) { |
| 122 | if (isset($datum['filename']) && file_exists($datum['filename'])) { |
| 123 | $command .= ' --data @' . $datum['filename']; |
| 124 | unset($data[$key]); |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | if (!empty($data)) { |
| 129 | foreach ($data as $datum) { |
| 130 | if (str_contains($datum, "'")) { |
| 131 | $datum = str_replace("'", "\\'", $datum); |
| 132 | } |
| 133 | $command .= " --data '" . $datum . "'"; |
| 134 | } |
| 135 | } |
| 136 | // URL-encoded data |
| 137 | } else if (($client->getRequest()->getMethod() == 'GET') || ($client->getRequest()->isUrlEncoded()) || |
| 138 | !($client->getRequest()->hasRequestType())) { |
| 139 | $command .= ' --data "' . $client->getRequest()->getData()->prepare()->getDataContent() . '"'; |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | // Add body content as data |
| 144 | if ($client->getRequest()->hasBody()) { |
| 145 | $body = $client->getRequest()->getBodyContent(); |
| 146 | if (str_contains($body, "'")) { |
| 147 | $body = str_replace("'", "\\'", $body); |
| 148 | } |
| 149 | $command .= " --data '" . $body . "'"; |
| 150 | } |
| 151 | |
| 152 | // Handle all other options |
| 153 | $curlOptions = $client->getHandler()->getOptions() + $currentOptions; |
| 154 | foreach ($curlOptions as $curlOption => $curlOptionValue) { |
| 155 | $curlOptionName = Options::getOptionNameByValue($curlOption); |
| 156 | if (!Options::isOmitOption($curlOptionName)) { |
| 157 | $commandOption = Options::getPhpOption($curlOptionName); |
| 158 | $command .= (is_array($commandOption) && isset($commandOption[0])) ? |
| 159 | ' ' . $commandOption[0] : ' ' . $commandOption; |
| 160 | if (Options::isValueOption($curlOptionName) && !empty($curlOptionValue)) { |
| 161 | $command .= ' ' . self::addQuotes($curlOptionValue); |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | $command .= ' ' . self::addQuotes($client->getRequest()->getUriAsString()); |
| 167 | |
| 168 | return $command; |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * Create a client object from a command string from the Curl CLI application |
| 173 | * |
| 174 | * @param string $command |
| 175 | * @return Client |
| 176 | */ |
| 177 | public static function commandToClient(string $command): Client |
| 178 | { |
| 179 | $command = trim($command); |
| 180 | |
| 181 | if (!str_starts_with($command, 'curl')) { |
| 182 | throw new Exception("Error: The command isn't a valid cURL command."); |
| 183 | } |
| 184 | |
| 185 | $command = substr($command, 4); |
| 186 | $options = []; |
| 187 | |
| 188 | // No options |
| 189 | if (!str_contains($command, '-')) { |
| 190 | $requestUri = trim($command); |
| 191 | // Else, parse options |
| 192 | } else { |
| 193 | $optionString = substr($command, 0, strrpos($command, ' ')); |
| 194 | $requestUri = substr($command, (strrpos($command, ' ') + 1)); |
| 195 | $options = self::parseCommandOptions($optionString); |
| 196 | } |
| 197 | |
| 198 | $request = new Request(self::trimQuotes($requestUri)); |
| 199 | $curl = new Curl(); |
| 200 | $files = null; |
| 201 | |
| 202 | if (!empty($options)) { |
| 203 | [$auth, $files] = self::convertCommandOptions($options, $curl, $request); |
| 204 | } |
| 205 | |
| 206 | $client = new Client($request, $curl); |
| 207 | |
| 208 | if (!empty($auth)) { |
| 209 | $client->setAuth($auth); |
| 210 | } |
| 211 | if (!empty($files)) { |
| 212 | $client->setFiles($files, false); |
| 213 | } |
| 214 | |
| 215 | return $client; |
| 216 | } |
| 217 | |
| 218 | /** |
| 219 | * Parse the CLI command options string |
| 220 | * |
| 221 | * @param string $optionString |
| 222 | * @return array |
| 223 | */ |
| 224 | public static function parseCommandOptions(string $optionString): array |
| 225 | { |
| 226 | $options = []; |
| 227 | $matches = []; |
| 228 | |
| 229 | preg_match_all('/\s[\-]{1,2}/', $optionString, $matches, PREG_OFFSET_CAPTURE); |
| 230 | |
| 231 | if (isset($matches[0]) && isset($matches[0][0])) { |
| 232 | foreach ($matches[0] as $i => $match) { |
| 233 | if (isset($matches[0][$i + 1])) { |
| 234 | $length = ($matches[0][$i + 1][1]) - $match[1] - 1; |
| 235 | $options[] = substr($optionString, $match[1] + 1, $length); |
| 236 | } else { |
| 237 | $options[] = substr($optionString, $match[1] + 1); |
| 238 | } |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | return $options; |
| 243 | } |
| 244 | |
| 245 | /** |
| 246 | * Extract command option values |
| 247 | * |
| 248 | * @param array $options |
| 249 | * @return array |
| 250 | */ |
| 251 | public static function extractCommandOptionValues(array $options): array |
| 252 | { |
| 253 | $optionValues = []; |
| 254 | |
| 255 | foreach ($options as $option) { |
| 256 | $opt = null; |
| 257 | $val = null; |
| 258 | if (str_starts_with($option, '--')) { |
| 259 | if (str_contains($option, ' ')) { |
| 260 | $opt = substr($option, 0, strpos($option, ' ')); |
| 261 | $val = substr($option, (strpos($option, ' ') + 1)); |
| 262 | } else { |
| 263 | $opt = $option; |
| 264 | } |
| 265 | } else { |
| 266 | if (strlen($option) > 2) { |
| 267 | if (substr($option, 2, 1) == ' ') { |
| 268 | $opt = substr($option, 0, 2); |
| 269 | $val = substr($option, 3); |
| 270 | } else { |
| 271 | $opt = substr($option, 0, 2); |
| 272 | $val = substr($option, 2); |
| 273 | } |
| 274 | } else { |
| 275 | $opt = $option; |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | if (($opt == '-d') || ($opt == '--data') || ($opt == '-F') || ($opt == '--form')) { |
| 280 | $val = self::trimQuotes($val); |
| 281 | if (str_contains($val, '=') && !str_contains($val, '<?xml')) { |
| 282 | parse_str(self::trimQuotes($val), $val); |
| 283 | } |
| 284 | } |
| 285 | |
| 286 | if (isset($optionValues[$opt])) { |
| 287 | if (!is_array($optionValues[$opt])) { |
| 288 | $optionValues[$opt] = [$optionValues[$opt]]; |
| 289 | } |
| 290 | if (is_array($val)) { |
| 291 | $optionValues[$opt] = array_merge($optionValues[$opt], $val); |
| 292 | } else { |
| 293 | $optionValues[$opt][] = $val; |
| 294 | } |
| 295 | } else { |
| 296 | $optionValues[$opt] = $val; |
| 297 | } |
| 298 | } |
| 299 | |
| 300 | return $optionValues; |
| 301 | } |
| 302 | |
| 303 | /** |
| 304 | * Convert CLI options to usable values for the Curl handler and request |
| 305 | * |
| 306 | * @param array $options |
| 307 | * @param Curl $curl |
| 308 | * @param Request $request |
| 309 | * @return array |
| 310 | */ |
| 311 | public static function convertCommandOptions(array $options, Curl $curl, Request $request): array |
| 312 | { |
| 313 | $optionValues = self::extractCommandOptionValues($options); |
| 314 | $auth = null; |
| 315 | $files = []; |
| 316 | |
| 317 | // Handle method |
| 318 | // If forced GET method |
| 319 | if (array_key_exists('-G', $optionValues) || array_key_exists('--get', $optionValues)) { |
| 320 | $request->setMethod('GET'); |
| 321 | if (array_key_exists('-G', $optionValues)) { |
| 322 | unset($optionValues['-G']); |
| 323 | } else { |
| 324 | unset($optionValues['--get']); |
| 325 | } |
| 326 | // If HEAD method |
| 327 | } else if (array_key_exists('-I', $optionValues) || array_key_exists('--head', $optionValues)) { |
| 328 | $request->setMethod('HEAD'); |
| 329 | if (array_key_exists('-I', $optionValues)) { |
| 330 | unset($optionValues['-I']); |
| 331 | } else { |
| 332 | unset($optionValues['--head']); |
| 333 | } |
| 334 | // All other methods |
| 335 | } else if (isset($optionValues['-X']) || isset($optionValues['--request'])) { |
| 336 | $request->setMethod(($optionValues['-X'] ?? $optionValues['--request'])); |
| 337 | if (isset($optionValues['-X'])) { |
| 338 | unset($optionValues['-X']); |
| 339 | } else { |
| 340 | unset($optionValues['--request']); |
| 341 | } |
| 342 | } |
| 343 | |
| 344 | // Handle insecure settings |
| 345 | if (array_key_exists('-k', $optionValues) || array_key_exists('--insecure', $optionValues)) { |
| 346 | $curl->setOption(CURLOPT_SSL_VERIFYHOST, 0); |
| 347 | $curl->setOption(CURLOPT_SSL_VERIFYPEER, 0); |
| 348 | } |
| 349 | |
| 350 | // Handle headers |
| 351 | if (isset($optionValues['-H']) || isset($optionValues['--header'])) { |
| 352 | $headerOpts = ($optionValues['-H'] ?? $optionValues['--header']); |
| 353 | if (is_array($headerOpts)) { |
| 354 | $headers = array_map(function ($value) { |
| 355 | return Command::trimQuotes($value); |
| 356 | }, $headerOpts); |
| 357 | } else { |
| 358 | $headers = [Command::trimQuotes($headerOpts)]; |
| 359 | } |
| 360 | |
| 361 | $request->addHeaders($headers); |
| 362 | |
| 363 | if (isset($optionValues['-H'])) { |
| 364 | unset($optionValues['-H']); |
| 365 | } else { |
| 366 | unset($optionValues['--header']); |
| 367 | } |
| 368 | } |
| 369 | |
| 370 | // Handle basic auth |
| 371 | if ((!array_key_exists('--digest', $optionValues) || array_key_exists('--basic', $optionValues) || array_key_exists('--anyauth', $optionValues)) && |
| 372 | (isset($optionValues['-u']) || isset($optionValues['--user']))) { |
| 373 | $userData = ($optionValues['-u'] ?? $optionValues['--user']); |
| 374 | if (str_contains($userData, ':')) { |
| 375 | [$username, $password] = explode(':', self::trimQuotes($userData), 2); |
| 376 | $auth = Auth::createBasic($username, $password); |
| 377 | if (isset($optionValues['-u'])) { |
| 378 | unset($optionValues['-u']); |
| 379 | } else { |
| 380 | unset($optionValues['--user']); |
| 381 | } |
| 382 | if (array_key_exists('--basic', $optionValues)) { |
| 383 | unset($optionValues['--basic']); |
| 384 | } |
| 385 | } |
| 386 | } |
| 387 | |
| 388 | // Handle JSON request |
| 389 | if (array_key_exists('--json', $optionValues)) { |
| 390 | $request->addHeaders([ |
| 391 | 'Content-Type: application/json', |
| 392 | 'Accept: application/json' |
| 393 | ]); |
| 394 | unset($optionValues['--json']); |
| 395 | } |
| 396 | |
| 397 | // Handle data |
| 398 | if (isset($optionValues['-d']) || isset($optionValues['--data'])) { |
| 399 | $data = ($optionValues['-d'] ?? $optionValues['--data']); |
| 400 | |
| 401 | if ($request->hasHeader('Content-Type') && is_string($data)) { |
| 402 | if (str_starts_with($data, '@')) { |
| 403 | //&& file_exists(getcwd() . DIRECTORY_SEPARATOR . substr($data, 1))) { |
| 404 | $file = substr($data, 1); |
| 405 | if (!str_starts_with($file, '/')) { |
| 406 | $file = getcwd() . DIRECTORY_SEPARATOR . substr($data, 1); |
| 407 | } |
| 408 | if (file_exists($file)) { |
| 409 | $files[] = $file; |
| 410 | } |
| 411 | } else { |
| 412 | $contentType = $request->getHeaderValueAsString('Content-Type'); |
| 413 | if ($contentType == Request::JSON) { |
| 414 | $data = json_decode($data, true); |
| 415 | } else if ($contentType == Request::URLENCODED) { |
| 416 | parse_str($data, $data); |
| 417 | } |
| 418 | } |
| 419 | } |
| 420 | $request->setData($data); |
| 421 | if (isset($optionValues['-d'])) { |
| 422 | unset($optionValues['-d']); |
| 423 | } else { |
| 424 | unset($optionValues['--data']); |
| 425 | } |
| 426 | } |
| 427 | |
| 428 | // Handle form data |
| 429 | if (isset($optionValues['-F']) || isset($optionValues['--form'])) { |
| 430 | $data = []; |
| 431 | $formData = ($optionValues['-F'] ?? $optionValues['--form']); |
| 432 | if (is_array($formData)) { |
| 433 | foreach ($formData as $key => $formDatum) { |
| 434 | if (str_starts_with($formDatum, '@')) { |
| 435 | $data[$key] = [ |
| 436 | 'filename' => getcwd() . DIRECTORY_SEPARATOR . substr($formDatum, 1), |
| 437 | 'contentType' => Client\Data::getMimeTypeFromFilename(substr($formDatum, 1)) |
| 438 | ]; |
| 439 | } else { |
| 440 | $data[$key] = $formDatum; |
| 441 | } |
| 442 | } |
| 443 | } |
| 444 | |
| 445 | $request->setData($data) |
| 446 | ->setRequestType(Request::MULTIPART); |
| 447 | |
| 448 | if (isset($optionValues['-F'])) { |
| 449 | unset($optionValues['-F']); |
| 450 | } else { |
| 451 | unset($optionValues['--form']); |
| 452 | } |
| 453 | } |
| 454 | |
| 455 | // Handle all other options |
| 456 | foreach ($optionValues as $option => $value) { |
| 457 | if (!Options::isOmitOption($option)) { |
| 458 | foreach (Options::getPhpOptions() as $phpOption => $curlOption) { |
| 459 | if (is_array($curlOption)) { |
| 460 | foreach ($curlOption as $cOpt) { |
| 461 | if ((str_starts_with($option, '--') && str_contains($cOpt, $option)) || str_starts_with($cOpt, $option)) { |
| 462 | if (Options::isValueOption($option)) { |
| 463 | $optionValue = Options::getValueOption($option) ?? self::trimQuotes($value); |
| 464 | } else { |
| 465 | $optionValue = true; |
| 466 | } |
| 467 | $curl->setOption(constant($phpOption), $optionValue); |
| 468 | break; |
| 469 | } |
| 470 | } |
| 471 | } else if ((str_starts_with($option, '--') && str_contains($curlOption, $option)) || str_starts_with($curlOption, $option)) { |
| 472 | if (Options::isValueOption($option)) { |
| 473 | $optionValue = Options::getValueOption($option) ?? self::trimQuotes($value); |
| 474 | } else { |
| 475 | $optionValue = true; |
| 476 | } |
| 477 | $curl->setOption(constant($phpOption), $optionValue); |
| 478 | break; |
| 479 | } |
| 480 | } |
| 481 | } |
| 482 | } |
| 483 | |
| 484 | return [$auth, $files]; |
| 485 | } |
| 486 | |
| 487 | /** |
| 488 | * Trim quotes from value |
| 489 | * |
| 490 | * @param string $value |
| 491 | * @return string |
| 492 | */ |
| 493 | public static function trimQuotes(string $value): string |
| 494 | { |
| 495 | if ((str_starts_with($value, '"') && str_ends_with($value, '"')) || (str_starts_with($value, "'") && str_ends_with($value, "'"))) { |
| 496 | $value = substr($value, 1); |
| 497 | $value = substr($value, 0, -1); |
| 498 | } |
| 499 | |
| 500 | return $value; |
| 501 | } |
| 502 | |
| 503 | /** |
| 504 | * Trim quotes from value |
| 505 | * |
| 506 | * @param string $value |
| 507 | * @param string $quote |
| 508 | * @return string |
| 509 | */ |
| 510 | public static function addQuotes(string $value, string $quote = '"'): string |
| 511 | { |
| 512 | if (!str_starts_with($value, '"') && !str_ends_with($value, '"') && !str_starts_with($value, "'") && !str_ends_with($value, "'")) { |
| 513 | $value = $quote . $value . $quote; |
| 514 | } |
| 515 | |
| 516 | return $value; |
| 517 | } |
| 518 | |
| 519 | } |