*/ public function subject_types(): array { return [ SubjectType::Booking, SubjectType::Customer, SubjectType::Intent, SubjectType::Payment, ]; } /** * {@inheritDoc} */ public function is_send(): bool { return false; } /** * {@inheritDoc} */ public function is_destructive(): bool { return false; } /** * {@inheritDoc} */ public function requires_fresh_context(): bool { return false; } /** * {@inheritDoc} * * @param AutomationContext $ctx Context. * @param ActionStep $step Step. */ public function execute( AutomationContext $ctx, ActionStep $step ): ActionResult { $config = $step->config(); $method = strtoupper( isset( $config['method'] ) ? (string) $config['method'] : 'POST' ); $url = $this->interpolate( isset( $config['url'] ) ? (string) $config['url'] : '', $ctx ); $body = $this->interpolate( isset( $config['body'] ) ? (string) $config['body'] : '', $ctx ); $headers = $this->parse_headers( isset( $config['headers'] ) ? (string) $config['headers'] : '' ); if ( '' === $url ) { return ActionResult::failed( 'Webhook URL is empty.' ); } if ( ! in_array( $method, [ 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' ], true ) ) { return ActionResult::failed( 'Unsupported HTTP method.' ); } if ( '' === $body && 'GET' !== $method ) { $encoded = wp_json_encode( $ctx->fields() ); $body = is_string( $encoded ) ? $encoded : '{}'; } $rendered = $this->render_preview( $method, $url, $headers, $body ); if ( ! OutboundGuard::allows_outbound() ) { return ActionResult::drafted( $rendered ); } if ( null === $this->http ) { return ActionResult::drafted( $rendered ); } try { $result = $this->http->send( $method, $url, $headers, $body, 10 ); } catch ( DomainException $e ) { return ActionResult::failed( $e->getMessage() ); } return ActionResult::completed( $rendered . "\n→ HTTP " . (string) $result['status'] ); } /** * {@inheritDoc} * * @return array */ public function schema(): array { return [ 'send' => false, 'destructive' => false, 'category' => 'integration', 'fields' => [ [ 'name' => 'url', 'type' => 'url', 'label' => 'URL', 'required' => true, 'hint' => 'Supports {{field}} tokens from the trigger context.', ], [ 'name' => 'method', 'type' => 'select', 'label' => 'Method', 'options' => [ [ 'value' => 'POST', 'label' => 'POST', ], [ 'value' => 'PUT', 'label' => 'PUT', ], [ 'value' => 'PATCH', 'label' => 'PATCH', ], [ 'value' => 'GET', 'label' => 'GET', ], [ 'value' => 'DELETE', 'label' => 'DELETE', ], ], ], [ 'name' => 'headers', 'type' => 'textarea', 'label' => 'Headers', 'hint' => 'One header per line, e.g. Authorization: Bearer …', ], [ 'name' => 'body', 'type' => 'textarea', 'label' => 'Body', 'hint' => 'JSON by default. Empty body sends the context field bag.', ], ], ]; } /** * Replace {{field}} tokens from context. * * @param string $template Template. * @param AutomationContext $ctx Context. */ private function interpolate( string $template, AutomationContext $ctx ): string { $fields = $ctx->fields(); return (string) preg_replace_callback( '/\{\{\s*([a-z0-9_.]+)\s*\}\}/i', static function ( array $m ) use ( $fields ): string { $key = $m[1]; if ( ! array_key_exists( $key, $fields ) ) { return ''; } $value = $fields[ $key ]; return is_scalar( $value ) ? (string) $value : ''; }, $template ); } /** * Parse header lines. Secret header values are not stored in the preview. * * @param string $raw Raw. * @return array */ private function parse_headers( string $raw ): array { $out = []; $lines = preg_split( '/\r\n|\r|\n/', $raw ); if ( ! is_array( $lines ) ) { $lines = []; } foreach ( $lines as $line ) { $line = trim( $line ); if ( '' === $line || ! str_contains( $line, ':' ) ) { continue; } [ $name, $value ] = explode( ':', $line, 2 ); $name = trim( $name ); if ( '' === $name ) { continue; } $out[ $name ] = trim( $value ); } return $out; } /** * Preview text with secret headers redacted. * * @param string $method Method. * @param string $url URL. * @param array $headers Headers. * @param string $body Body. */ private function render_preview( string $method, string $url, array $headers, string $body ): string { $lines = [ $method . ' ' . $url ]; foreach ( $headers as $name => $value ) { $lower = strtolower( $name ); $redact = str_contains( $lower, 'authorization' ) || str_contains( $lower, 'api-key' ) || str_contains( $lower, 'apikey' ) || str_contains( $lower, 'secret' ) || str_contains( $lower, 'token' ); $lines[] = $name . ': ' . ( $redact ? '[redacted]' : $value ); } $lines[] = $body; return implode( "\n", $lines ); } }