FreshRSS-Extensions/xExtension-Webhook/request.php

296 lines
8.7 KiB
PHP

<?php
declare(strict_types=1);
/**
* Optimized HTTP request handler for Webhook extension
*
* This function sends HTTP requests with proper validation, error handling,
* and logging capabilities for the FreshRSS Webhook extension.
*
* @param string $url The target URL for the HTTP request
* @param string $method HTTP method (GET, POST, PUT, DELETE, etc.)
* @param string $bodyType Content type for the request body ('json' or 'form')
* @param string $body Request body content as JSON string
* @param string[] $headers Array of HTTP headers
* @param bool $logEnabled Whether logging is enabled
* @param string $additionalLog Additional context for logging
*
* @throws InvalidArgumentException When invalid parameters are provided
* @throws JsonException When JSON encoding/decoding fails
* @throws RuntimeException When cURL operations fail
*
* @return void
*/
function sendReq(
string $url,
string $method,
string $bodyType,
string $body,
array $headers = [],
bool $logEnabled = true,
string $additionalLog = "",
): void {
// Validate inputs
if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
throw new InvalidArgumentException("Invalid URL provided: {$url}");
}
$allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'];
if (!in_array(strtoupper($method), $allowedMethods, true)) {
throw new InvalidArgumentException("Invalid HTTP method: {$method}");
}
$allowedBodyTypes = ['json', 'form'];
if (!in_array($bodyType, $allowedBodyTypes, true)) {
throw new InvalidArgumentException("Invalid body type: {$bodyType}");
}
$ch = curl_init($url);
if ($ch === false) {
throw new RuntimeException("Failed to initialize cURL session");
}
try {
// Configure HTTP method
configureHttpMethod($ch, strtoupper($method));
// Process and set HTTP body
$processedBody = processHttpBody($body, $bodyType, $method, $logEnabled);
if ($processedBody !== null && $method !== 'GET') {
curl_setopt($ch, CURLOPT_POSTFIELDS, $processedBody);
}
// Configure headers
$finalHeaders = configureHeaders($headers, $bodyType);
curl_setopt($ch, CURLOPT_HTTPHEADER, $finalHeaders);
// Log the request
logRequest($logEnabled, $additionalLog, $method, $url, $bodyType, $processedBody, $finalHeaders);
// Execute request
executeRequest($ch, $logEnabled);
} catch (Throwable $err) {
logError($logEnabled, "Error in sendReq: {$err->getMessage()} | URL: {$url} | Body: {$body}");
throw $err;
} finally {
curl_close($ch);
}
}
/**
* Configure cURL HTTP method settings
*
* Sets the appropriate cURL options based on the HTTP method.
*
* @param CurlHandle $ch The cURL handle
* @param string $method HTTP method in uppercase
*
* @return void
*/
function configureHttpMethod(CurlHandle $ch, string $method): void {
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
switch ($method) {
case 'POST':
curl_setopt($ch, CURLOPT_POST, true);
break;
case 'PUT':
curl_setopt($ch, CURLOPT_PUT, true);
break;
case 'GET':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
break;
case 'DELETE':
case 'PATCH':
case 'OPTIONS':
case 'HEAD':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
break;
}
}
/**
* Process HTTP body based on content type
*
* Converts the request body to the appropriate format based on the body type.
* Supports JSON and form-encoded data.
*
* @param string $body Raw body content as JSON string
* @param string $bodyType Content type ('json' or 'form')
* @param string $method HTTP method
* @param bool $logEnabled Whether logging is enabled
*
* @throws JsonException When JSON processing fails
* @throws InvalidArgumentException When unsupported body type is provided
*
* @return string|null Processed body content or null if no body needed
*/
function processHttpBody(string $body, string $bodyType, string $method, bool $logEnabled): ?string {
if (empty($body) || $method === 'GET') {
return null;
}
try {
$bodyObject = json_decode($body, true, 256, JSON_THROW_ON_ERROR);
return match ($bodyType) {
'json' => json_encode($bodyObject, JSON_THROW_ON_ERROR),
'form' => http_build_query($bodyObject ?? []),
default => throw new InvalidArgumentException("Unsupported body type: {$bodyType}")
};
} catch (JsonException $err) {
logError($logEnabled, "JSON processing error: {$err->getMessage()} | Body: {$body}");
throw $err;
}
}
/**
* Configure HTTP headers for the request
*
* Sets appropriate Content-Type headers if none are provided,
* based on the body type.
*
* @param string[] $headers Array of custom headers
* @param string $bodyType Content type ('json' or 'form')
*
* @return string[] Final array of headers to use
*/
function configureHeaders(array $headers, string $bodyType): array {
if (empty($headers)) {
return match ($bodyType) {
'form' => ['Content-Type: application/x-www-form-urlencoded'],
'json' => ['Content-Type: application/json'],
default => []
};
}
return $headers;
}
/**
* Log the outgoing HTTP request details
*
* Logs comprehensive information about the request being sent,
* including URL, method, body, and headers.
*
* @param bool $logEnabled Whether logging is enabled
* @param string $additionalLog Additional context information
* @param string $method HTTP method
* @param string $url Target URL
* @param string $bodyType Content type
* @param string|null $body Processed request body
* @param string[] $headers Array of HTTP headers
*
* @return void
*/
function logRequest(
bool $logEnabled,
string $additionalLog,
string $method,
string $url,
string $bodyType,
?string $body,
array $headers
): void {
if (!$logEnabled) {
return;
}
$cleanUrl = urldecode($url);
$cleanBody = $body ? str_replace('\/', '/', $body) : '';
$headersJson = json_encode($headers);
$logMessage = trim("{$additionalLog} ♦♦ sendReq ⏩ {$method}: {$cleanUrl} ♦♦ {$bodyType} ♦♦ {$cleanBody} ♦♦ {$headersJson}");
logWarning($logEnabled, $logMessage);
}
/**
* Execute cURL request and handle response
*
* Executes the configured cURL request and handles both success
* and error responses with appropriate logging.
*
* @param CurlHandle $ch The configured cURL handle
* @param bool $logEnabled Whether logging is enabled
*
* @throws RuntimeException When cURL execution fails
*
* @return void
*/
function executeRequest(CurlHandle $ch, bool $logEnabled): void {
$response = curl_exec($ch);
if (curl_errno($ch)) {
$error = curl_error($ch);
logError($logEnabled, "cURL error: {$error}");
throw new RuntimeException("cURL error: {$error}");
}
$info = curl_getinfo($ch);
$httpCode = $info['http_code'] ?? 'unknown';
logWarning($logEnabled, "Response ✅ ({$httpCode}) {$response}");
}
/**
* Log warning message using FreshRSS logging system
*
* Safely logs warning messages through the FreshRSS Minz_Log system
* with proper class existence checking.
*
* @param bool $logEnabled Whether logging is enabled
* @param mixed $data Data to log (will be converted to string)
*
* @return void
*/
function logWarning(bool $logEnabled, $data): void {
if ($logEnabled && class_exists('Minz_Log')) {
Minz_Log::warning("[WEBHOOK] " . $data);
}
}
/**
* Log error message using FreshRSS logging system
*
* Safely logs error messages through the FreshRSS Minz_Log system
* with proper class existence checking.
*
* @param bool $logEnabled Whether logging is enabled
* @param mixed $data Data to log (will be converted to string)
*
* @return void
*/
function logError(bool $logEnabled, $data): void {
if ($logEnabled && class_exists('Minz_Log')) {
Minz_Log::error("[WEBHOOK]❌ " . $data);
}
}
/**
* Backward compatibility alias for logWarning function
*
* @deprecated Use logWarning() instead
* @param bool $logEnabled Whether logging is enabled
* @param mixed $data Data to log
*
* @return void
*/
function LOG_WARN(bool $logEnabled, $data): void {
logWarning($logEnabled, $data);
}
/**
* Backward compatibility alias for logError function
*
* @deprecated Use logError() instead
* @param bool $logEnabled Whether logging is enabled
* @param mixed $data Data to log
*
* @return void
*/
function LOG_ERR(bool $logEnabled, $data): void {
logError($logEnabled, $data);
}