PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.4.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.4.0
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / api / onboarding-api.php

onboarding-api.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.4.0, at inc/api/onboarding-api.php

345 lines 9.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Onboarding REST endpoints.
4 *
5 * Routes (under `suredonation/v1`):
6 * - GET /onboarding/get-status — return { completed: 'yes'|'no' }
7 * - POST /onboarding/set-status — write completion + optional analytics
8 * - POST /onboarding/create-campaign — create a published suredonation_cmpgn
9 * - POST /onboarding/user-details — persist lead capture (free-only step)
10 *
11 * @package SureDonation
12 */
13
14 namespace SureDonation\Inc\API;
15
16 use SureDonation\Inc\Campaigns\Campaign_Cpt;
17 use SureDonation\Inc\Helper;
18 use SureDonation\Inc\Onboarding;
19 use WP_Error;
20 use WP_REST_Request;
21 use WP_REST_Response;
22 use WP_REST_Server;
23
24 if ( ! defined( 'ABSPATH' ) ) {
25 exit;
26 }
27
28 /**
29 * Onboarding REST API endpoints.
30 *
31 * @since 1.0.0
32 */
33 class Onboarding_API {
34 /**
35 * Allowed goal types for the create-campaign endpoint.
36 *
37 * @since 1.0.0
38 * @var array<int,string>
39 */
40 private const GOAL_TYPES = [ 'raised_amount', 'donation_count' ];
41
42 /**
43 * Return endpoint definitions for Rest_Api to register.
44 *
45 * @return array<string,mixed>
46 * @since 1.0.0
47 */
48 public function get_endpoints() {
49 return [
50 '/onboarding/get-status' => [
51 'methods' => WP_REST_Server::READABLE,
52 'callback' => [ $this, 'get_status' ],
53 'permission_callback' => [ $this, 'check_permissions' ],
54 ],
55 '/onboarding/set-status' => [
56 'methods' => WP_REST_Server::EDITABLE,
57 'callback' => [ $this, 'set_status' ],
58 'permission_callback' => [ $this, 'check_permissions' ],
59 ],
60 '/onboarding/create-campaign' => [
61 'methods' => WP_REST_Server::EDITABLE,
62 'callback' => [ $this, 'create_campaign' ],
63 'permission_callback' => [ $this, 'check_permissions' ],
64 ],
65 '/onboarding/user-details' => [
66 'methods' => WP_REST_Server::EDITABLE,
67 'callback' => [ $this, 'save_user_details' ],
68 'permission_callback' => [ $this, 'check_permissions' ],
69 ],
70 ];
71 }
72
73 /**
74 * Permission gate. Write requests (POST/PUT/PATCH/DELETE) additionally
75 * require a valid wp_rest nonce, matching Donors_API — onboarding forwards a
76 * lead to the BSF CRM, so the write boundary is pinned explicitly.
77 *
78 * @param \WP_REST_Request<array<string,mixed>>|null $request Current request.
79 * @return bool|\WP_Error
80 * @since 1.0.0
81 */
82 public function check_permissions( $request = null ) {
83 if ( ! current_user_can( 'manage_options' ) ) {
84 return false;
85 }
86
87 if ( $request instanceof \WP_REST_Request ) {
88 $method = strtoupper( $request->get_method() );
89 if ( in_array( $method, [ 'POST', 'PUT', 'PATCH', 'DELETE' ], true ) ) {
90 $nonce = $request->get_header( 'X-WP-Nonce' );
91 if ( empty( $nonce ) ) {
92 $nonce_param = $request->get_param( '_wpnonce' );
93 $nonce = is_string( $nonce_param ) ? $nonce_param : '';
94 }
95 if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
96 return new \WP_Error(
97 'rest_forbidden',
98 __( 'Invalid or missing nonce.', 'suredonation' ),
99 [ 'status' => 403 ]
100 );
101 }
102 }
103 }
104
105 return true;
106 }
107
108 /**
109 * GET /onboarding/get-status.
110 *
111 * @return WP_REST_Response
112 * @since 1.0.0
113 */
114 public function get_status() {
115 return new WP_REST_Response(
116 [
117 'completed' => Onboarding::get_instance()->is_completed() ? 'yes' : 'no',
118 ]
119 );
120 }
121
122 /**
123 * POST /onboarding/set-status.
124 *
125 * @param WP_REST_Request $request Request.
126 * @return WP_REST_Response
127 * @since 1.0.0
128 */
129 public function set_status( $request ) {
130 $completed = $request->get_param( 'completed' );
131 Onboarding::get_instance()->set_completed( 'yes' === $completed ? 'yes' : 'no' );
132
133 return new WP_REST_Response( [ 'success' => true ] );
134 }
135
136 /**
137 * POST /onboarding/create-campaign.
138 *
139 * Creates a published campaign post + writes its meta. Returns the new
140 * campaign id + edit URL so the JS can persist it in onboarding state.
141 *
142 * @param WP_REST_Request $request Request.
143 * @return WP_REST_Response|WP_Error
144 * @since 1.0.0
145 */
146 public function create_campaign( $request ) {
147 $name = sanitize_text_field( (string) $request->get_param( 'name' ) );
148 $goal_type = (string) $request->get_param( 'goal_type' );
149 $description = wp_kses_post( (string) $request->get_param( 'description' ) );
150
151 // Clamp to a non-negative, finite, sane range. The JS already
152 // validates this, but the endpoint is callable directly by any
153 // manage_options user and shouldn't trust client-side bounds.
154 $goal_amount = (float) $request->get_param( 'goal_amount' );
155 if ( ! is_finite( $goal_amount ) || $goal_amount < 0 ) {
156 $goal_amount = 0.0;
157 }
158 // Cap at 1e9 so a stray "1e308" can't poison campaign meta.
159 $goal_amount = min( $goal_amount, 1000000000.0 );
160
161 if ( '' === trim( $name ) ) {
162 return new WP_Error(
163 'suredonation_campaign_name_required',
164 __( 'Campaign name is required.', 'suredonation' ),
165 [ 'status' => 400 ]
166 );
167 }
168
169 if ( ! in_array( $goal_type, self::GOAL_TYPES, true ) ) {
170 $goal_type = 'raised_amount';
171 }
172
173 // Publish the campaign so it behaves like one created via the normal
174 // flow: the save_post_suredonation_cmpgn hook auto-creates its default
175 // donation form, and the campaign becomes selectable in the Donation
176 // Form block (whose query is limited to published campaigns).
177 $result = wp_insert_post(
178 [
179 'post_type' => Campaign_Cpt::POST_TYPE,
180 'post_status' => 'publish',
181 'post_title' => $name,
182 'post_excerpt' => $description,
183 'post_author' => get_current_user_id(),
184 ],
185 true
186 );
187
188 if ( is_wp_error( $result ) ) {
189 return new WP_Error(
190 'suredonation_campaign_create_failed',
191 $result->get_error_message(),
192 [ 'status' => 500 ]
193 );
194 }
195
196 $campaign_id = (int) $result;
197
198 if ( $campaign_id <= 0 ) {
199 return new WP_Error(
200 'suredonation_campaign_create_failed',
201 __( 'Could not create the campaign.', 'suredonation' ),
202 [ 'status' => 500 ]
203 );
204 }
205
206 Helper::update_campaign_meta(
207 $campaign_id,
208 [
209 'goal_type' => $goal_type,
210 'goal_amount' => $goal_amount,
211 ]
212 );
213
214 return new WP_REST_Response(
215 [
216 'success' => true,
217 'campaign_id' => $campaign_id,
218 'edit_url' => admin_url( 'admin.php?page=suredonation#/campaigns/' . $campaign_id ),
219 ]
220 );
221 }
222
223 /**
224 * POST /onboarding/user-details.
225 *
226 * Stores the lead-capture payload under suredonation_options so we
227 * don't re-prompt on subsequent setup passes.
228 *
229 * @param WP_REST_Request $request Request.
230 * @return WP_REST_Response
231 * @since 1.0.0
232 */
233 public function save_user_details( $request ) {
234 $onboarding = Onboarding::get_instance();
235
236 $payload = [
237 'first_name' => sanitize_text_field( (string) $request->get_param( 'first_name' ) ),
238 'last_name' => sanitize_text_field( (string) $request->get_param( 'last_name' ) ),
239 'email' => sanitize_email( (string) $request->get_param( 'email' ) ),
240 'opted_in' => (bool) $request->get_param( 'opted_in' ),
241 ];
242
243 $onboarding->set_user_details( $payload );
244
245 update_site_option(
246 'suredonation_usage_optin',
247 $payload['opted_in'] ? 'yes' : 'no'
248 );
249
250 if ( ! $onboarding->is_lead_sent() && $this->forward_lead_to_crm( $payload ) ) {
251 $onboarding->mark_lead_sent();
252 }
253
254 /**
255 * Fires after onboarding lead-capture details are persisted.
256 *
257 * @since 1.0.0
258 *
259 * @param array<string,mixed> $payload Sanitised payload.
260 */
261 do_action( 'suredonation_onboarding_user_details_saved', $payload );
262
263 return new WP_REST_Response( [ 'success' => true ] );
264 }
265
266 /**
267 * Generate lead.
268 *
269 * @param array<string,mixed> $payload Sanitised lead-capture payload.
270 * @return bool True when the CRM accepted the lead, false otherwise.
271 * @since 1.1.2
272 */
273 private function forward_lead_to_crm( array $payload ) {
274 $email_raw = $payload['email'] ?? '';
275 $email = is_string( $email_raw ) ? sanitize_email( $email_raw ) : '';
276 if ( empty( $email ) || ! is_email( $email ) ) {
277 return false;
278 }
279
280 $url = 'https://metrics.brainstormforce.com/wp-json/bsf-metrics-server/v1/subscribe';
281
282 if ( defined( 'SUREDONATION_METRICS_ENDPOINT' ) && is_string( SUREDONATION_METRICS_ENDPOINT ) ) {
283 $url = SUREDONATION_METRICS_ENDPOINT;
284 }
285
286 /**
287 * Filters the endpoint.
288 *
289 * @since 1.1.2
290 *
291 * @param string $url Endpoint URL.
292 * @param array<string,mixed> $payload Lead payload being sent.
293 */
294 $filtered = apply_filters( 'suredonation_metrics_subscribe_url', $url, $payload );
295 $url = is_string( $filtered ) ? $filtered : $url;
296
297 if ( '' === $url ) {
298 return false;
299 }
300
301 $first_name = isset( $payload['first_name'] ) && is_string( $payload['first_name'] ) ? $payload['first_name'] : '';
302 $last_name = isset( $payload['last_name'] ) && is_string( $payload['last_name'] ) ? $payload['last_name'] : '';
303 $domain = wp_parse_url( home_url(), PHP_URL_HOST );
304 $domain = is_string( $domain ) ? $domain : '';
305
306 $body = wp_json_encode(
307 [
308 // Lowercase keys satisfy the current BSF Metrics REST args.
309 'email' => $email,
310 'first_name' => $first_name,
311 'last_name' => $last_name,
312 'domain' => $domain,
313 'source' => 'suredonation',
314 // Legacy uppercase keys kept for backward compatibility.
315 'EMAIL' => $email,
316 'FIRSTNAME' => $first_name,
317 'LASTNAME' => $last_name,
318 'DOMAIN' => $domain,
319 ]
320 );
321
322 if ( false === $body ) {
323 return false;
324 }
325
326 // `source` identifies the originating plugin on the shared CRM server.
327 // wp_safe_remote_post with WP's default 5s timeout keeps a slow or
328 // hung endpoint from stalling onboarding completion.
329 $response = wp_safe_remote_post(
330 $url,
331 [
332 'headers' => [ 'Content-Type' => 'application/json' ],
333 'body' => $body,
334 ]
335 );
336
337 if ( is_wp_error( $response ) ) {
338 return false;
339 }
340
341 $code = (int) wp_remote_retrieve_response_code( $response );
342 return in_array( $code, [ 200, 201, 204 ], true );
343 }
344 }
345