Jake Vanderwerf
10 days ago de317675a8069b747cb253ba3e2b5dc394ca36ef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<?php
namespace JVBase\integrations;
 
use Exception;
use WP_REST_Request;
use WP_REST_Response;
 
if (!defined('ABSPATH')) {
    exit;
}
 
trait Webhooks {
    use _Base;
    protected bool $hasWebhooks = true;
 
    abstract protected function validateWebhook(array $payload):bool;
    abstract protected function processWebhook(array $payload):bool;
    abstract protected function extractWebhookId(array $payload):string;
 
    /**
     * Register webhook endpoint
     */
    protected function registerWebhookEndpoint(): void
    {
        // Register REST API endpoint for webhooks
        add_action('rest_api_init', function() {
            register_rest_route('jvb/v1', '/webhooks/' . $this->service_name, [
                'methods' => 'POST',
                'callback' => [$this, 'handleWebhookRequest'],
                'permission_callback' => '__return_true', // Webhooks come from external services
            ]);
        });
    }
 
    /**
     * Handle webhook REST API request
     */
    public function handleWebhookRequest(WP_REST_Request $request): WP_REST_Response
    {
        $payload = $request->get_params();
        $headers = $request->get_headers();
 
        // Include headers in payload for signature validation
        $payload['_headers'] = $headers;
 
        $success = $this->handleWebhook($payload);
 
        return new WP_REST_Response([
            'success' => $success
        ], $success ? 200 : 400);
    }
 
    public function handleWebhook(array $payload):bool
    {
        error_log('[Integrations]::handleWebhook for '.$this->service_name.': '.print_r($payload, true));
 
        if (!$this->validateWebhook($payload)) {
            $this->logError('handleWebhook', 'Webhook validation failed.');
            return false;
        }
 
        //Check for duplicate processing
        if ($this->isWebhookProcessed($payload)) {
            return true;
        }
 
        try {
            $result = $this->processWebhook($payload);
 
            if ($result) {
                $this->markWebhookProcessed($payload);
            } else {
                $this->logError('handleWebhook', 'Webhook processing returned false.');
            }
            return $result;
        } catch (Exception $e) {
            $this->logError('handleWebhook', 'Webhook processing failed', [
                'error' => $e->getMessage(),
                'webhook_id'    => $this->extractWebhookId($payload),
                'trace' => $e->getTraceAsString()
            ]);
 
            Auth::getInstance()->markUnhealthy($this->service_name, 'oauth', $this->userID);
            return false;
        }
    }
 
 
    protected function renderWebhookUrl(): string
    {
        if (!$this->hasWebhooks || !$this->isSetUp()) {
            return '';
        }
 
        $webhook_url = rest_url('jvb/v1/webhooks/' . $this->service_name);
 
        return sprintf(
            '<div class="webhook-info">
            <h4>Webhook URL</h4>
            <code id="webhook-url-%s">%s</code>
            <p class="hint">Add this URL to your %s webhook settings</p>
        </div>',
            esc_attr($this->service_name),
            esc_html($webhook_url),
            esc_html($this->title)
        );
    }
 
    protected function isWebhookProcessed(array $payload):bool
    {
        $id = $this->extractWebhookId($payload);
        $key = BASE."webhook_processed_{$this->service_name}_{$id}";
        return get_transient($key)!==false;
    }
    protected function markWebhookProcessed(array $payload):void
    {
        $id = $this->extractWebhookId($payload);
        if (!empty($id)) {
            $key = BASE."webhook_processed_{$this->service_name}_{$id}";
            set_transient($key, true, DAY_IN_SECONDS);
        }
 
 
    }
}