PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.16.3
GiveWP – Donation Plugin and Fundraising Platform v4.16.3
4.18.0 4.17.0 4.16.9 4.16.8.1 4.16.8 4.16.7.2 4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 All 257 releases
give / src / API / REST / V3 / Routes / Donors / DonorController.php

DonorController.php in GiveWP – Donation Plugin and Fundraising Platform 4.16.3, at src/API/REST/V3/Routes/Donors/DonorController.php

668 lines 26.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Give\API\REST\V3\Routes\Donors;
4
5 use Give\API\REST\V3\Routes\Donations\ValueObjects\DonationRoute;
6 use Give\API\REST\V3\Routes\Donors\Permissions\DonorPermissions;
7 use Give\API\REST\V3\Routes\Donors\ValueObjects\DonorAnonymousMode;
8 use Give\API\REST\V3\Routes\Donors\ValueObjects\DonorRoute;
9 use Give\API\REST\V3\Routes\Donors\ViewModels\DonorViewModel;
10 use Give\API\REST\V3\Routes\Subscriptions\ValueObjects\SubscriptionRoute;
11 use Give\API\REST\V3\Support\CURIE;
12 use Give\API\REST\V3\Support\Headers;
13 use Give\API\REST\V3\Support\Item;
14 use Give\Donors\DonorsQuery;
15 use Give\Donors\Models\Donor;
16 use Give\Donors\ValueObjects\DonorAddress;
17 use WP_Error;
18 use WP_REST_Controller;
19 use WP_REST_Request;
20 use WP_REST_Response;
21 use WP_REST_Server;
22
23 /**
24 * The methods using snake case like register_routes() are present in the base class,
25 * and the methods using camel case like getSortColumn() are available only on this class.
26 *
27 * @since 4.14.0 Extract permissions logic to separate classes
28 * @since 4.4.0 Extends WP_REST_Controller class and rename methods
29 * @since 4.0.0
30 */
31 class DonorController extends WP_REST_Controller
32 {
33 /**
34 * @since 4.0.0
35 */
36 public function __construct()
37 {
38 $this->namespace = DonorRoute::NAMESPACE;
39 $this->rest_base = DonorRoute::BASE;
40 }
41
42 /**
43 * @since 4.9.0 Move schema key to the route level instead of defining it for each endpoint (which is incorrect)
44 * @since 4.0.0
45 */
46 public function register_routes()
47 {
48 register_rest_route($this->namespace, '/' . $this->rest_base, [
49 [
50 'methods' => WP_REST_Server::READABLE,
51 'callback' => [$this, 'get_items'],
52 'permission_callback' => [$this, 'get_items_permissions_check'],
53 'args' => array_merge($this->get_collection_params(), $this->getSharedParamsForGetMethods()),
54 ],
55 'schema' => [$this, 'get_public_item_schema'],
56 ]);
57
58 register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', [
59 [
60 'methods' => WP_REST_Server::READABLE,
61 'callback' => [$this, 'get_item'],
62 'permission_callback' => [$this, 'get_item_permissions_check'],
63 'args' => array_merge([
64 'id' => [
65 'description' => __(
66 'The donor ID.',
67 'give'
68 ),
69 'type' => 'integer',
70 'required' => true,
71 ],
72 '_embed' => [
73 'description' => __(
74 'Whether to embed related resources in the response. It can be true when we want to embed all available resources, or a string like "givewp:statistics" when we wish to embed only a specific one. Available embeddable resources: givewp:statistics | givewp:donations | givewp:subscriptions. IMPORTANT: Use with caution when setting to true, as donations and subscriptions return 30 items by default, which can result in a large payload.',
75 'give'
76 ),
77 'type' => ['string', 'boolean'],
78 'default' => false,
79 ],
80 'mode' => [
81 'description' => __(
82 'The mode of donations to filter by "live" or "test" (it only gets applied when "_embed" is set).',
83 'give'
84 ),
85 'type' => 'string',
86 'default' => 'live',
87 'enum' => ['live', 'test'],
88 ],
89 'campaignId' => [
90 'description' => __(
91 'The ID of the campaign to filter donors by - zero or empty mean "all campaigns" (it only gets applied when "_embed" is set).',
92 'give'
93 ),
94 'type' => 'integer',
95 'default' => 0,
96 ],
97 ], $this->getSharedParamsForGetMethods()),
98 ],
99 [
100 'methods' => WP_REST_Server::EDITABLE,
101 'callback' => [$this, 'update_item'],
102 'permission_callback' => [$this, 'update_item_permissions_check'],
103 'args' => rest_get_endpoint_args_for_schema($this->get_public_item_schema(), WP_REST_Server::EDITABLE),
104 ],
105 'schema' => [$this, 'get_public_item_schema'],
106 ]);
107 }
108
109 /**
110 * Get list of donors.
111 *
112 * @since 4.14.0 Use Headers::addPagination() helper for pagination headers
113 * @since 4.8.0 Add support for search parameter
114 * @since 4.0.0
115 *
116 * @param WP_REST_Request $request Full details about the request.
117 *
118 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
119 */
120 public function get_items($request)
121 {
122 $page = $request->get_param('page');
123 $perPage = $request->get_param('per_page');
124 $sortColumn = $this->getSortColumn($request->get_param('sort'));
125 $sortDirection = $request->get_param('direction');
126 $includeSensitiveData = $request->get_param('includeSensitiveData');
127 $donorAnonymousMode = new DonorAnonymousMode($request->get_param('anonymousDonors'));
128
129 $query = new DonorsQuery();
130
131 if ($request->get_param('search')) {
132 $query->whereLike('name', '%%' . $request->get_param('search') . '%%');
133 $query->orWhereLike('email', '%%' . $request->get_param('search') . '%%');
134 }
135
136 // Donors only can be donors if they have donations associated with them
137 if ($request->get_param('onlyWithDonations')) {
138 $mode = $request->get_param('mode');
139 $campaignId = $request->get_param('campaignId');
140 $query->whereDonorsHaveDonations($mode, $campaignId, $donorAnonymousMode->isExcluded());
141 }
142
143 $totalQuery = $query->clone();
144 $query
145 ->limit($perPage)
146 ->offset(($page - 1) * $perPage)
147 ->orderBy($sortColumn, $sortDirection);
148
149 $donors = $query->getAll() ?? [];
150 $donors = array_map(function ($donor) use ($includeSensitiveData, $donorAnonymousMode, $request) {
151 $item = (new DonorViewModel($donor))->anonymousMode($donorAnonymousMode)->includeSensitiveData($includeSensitiveData)->exports();
152 $response = $this->prepare_item_for_response($item, $request);
153
154 return $this->prepare_response_for_collection($response);
155 }, $donors);
156
157 $totalDonors = empty($donors) ? 0 : $totalQuery->count();
158 $response = rest_ensure_response($donors);
159 $response = Headers::addPagination($response, $request, $totalDonors, $perPage, $this->rest_base);
160
161 return $response;
162 }
163
164 /**
165 * Get a single donor.
166 *
167 * @since 4.0.0
168 *
169 * @param WP_REST_Request $request Full data about the request.
170 *
171 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
172 */
173 public function get_item($request)
174 {
175 $donor = Donor::find($request->get_param('id'));
176 $includeSensitiveData = $request->get_param('includeSensitiveData');
177 $donorAnonymousMode = new DonorAnonymousMode($request->get_param('anonymousDonors'));
178
179 if (!$donor || ($donor->isAnonymous() && $donorAnonymousMode->isExcluded())) {
180 return new WP_Error('donor_not_found', __('Donor not found', 'give'), ['status' => 404]);
181 }
182
183 $item = (new DonorViewModel($donor))->anonymousMode($donorAnonymousMode)->includeSensitiveData($includeSensitiveData)->exports();
184 $response = $this->prepare_item_for_response($item, $request);
185
186 return rest_ensure_response($response);
187 }
188
189 /**
190 * Update a single donor.
191 *
192 * @since 4.8.0 Update donor name when firstName or lastName is updated
193 * @since 4.7.0 Add support for updating custom fields
194 * @since 4.4.0
195 *
196 * @return WP_REST_Response|WP_Error
197 */
198 public function update_item($request)
199 {
200 $donor = Donor::find($request->get_param('id'));
201
202 if (!$donor) {
203 return new WP_REST_Response(__('Donor not found', 'give'), 404);
204 }
205
206 $nonEditableFields = [
207 'id',
208 'userId',
209 'createdAt',
210 ];
211
212 foreach ($request->get_params() as $key => $value) {
213 if (!in_array($key, $nonEditableFields)) {
214 if ($donor->hasProperty($key)) {
215 if ($key === 'addresses') {
216 $donor->addresses = array_map(function ($address) {
217 return DonorAddress::fromArray($address);
218 }, $value);
219 continue;
220 }
221
222 if (!$donor->isPropertyTypeValid($key, $value)) {
223 $value = null;
224 }
225
226 $donor->$key = $value;
227 }
228 }
229 }
230
231 if ($request->get_param('firstName') || $request->get_param('lastName')) {
232 $donor->name = trim($donor->firstName . ' ' . $donor->lastName);
233 }
234
235 if ($donor->isDirty()) {
236 $donor->save();
237 }
238
239 $item = (new DonorViewModel($donor))->includeSensitiveData(true)->anonymousMode(DonorAnonymousMode::INCLUDED())->exports();
240 $fieldsUpdate = $this->update_additional_fields_for_object($item, $request);
241
242 if (is_wp_error($fieldsUpdate)) {
243 return $fieldsUpdate;
244 }
245
246 $response = $this->prepare_item_for_response($item, $request);
247
248 return rest_ensure_response($response);
249 }
250
251 /**
252 * @since 4.14.0 Use DonorPermissions class
253 * @since 4.0.0
254 *
255 * @param WP_REST_Request $request
256 *
257 * @return true|WP_Error
258 */
259 public function get_items_permissions_check($request)
260 {
261 return DonorPermissions::validationForGetMethods($request);
262 }
263
264 /**
265 * @since 4.14.0 Use DonorPermissions class
266 * @since 4.0.0
267 *
268 * @param WP_REST_Request $request
269 *
270 * @return true|WP_Error
271 */
272 public function get_item_permissions_check($request)
273 {
274 return DonorPermissions::validationForGetMethods($request);
275 }
276
277 /**
278 * @since 4.14.0 Use DonorPermissions class
279 * @since 4.4.0
280 *
281 * @param WP_REST_Request $request
282 *
283 * @return true|WP_Error
284 */
285 public function update_item_permissions_check($request)
286 {
287 return DonorPermissions::validationForUpdateMethod($request);
288 }
289
290 /**
291 * @since 4.14.0 Add links to donations and subscriptions and format dates as strings using Item::formatDatesForResponse
292 * @since 4.7.0 Add support for adding custom fields to the response
293 * @since 4.4.0
294 */
295 public function prepare_item_for_response($item, $request): WP_REST_Response
296 {
297 $donorId = $request->get_param('id');
298 $mode = $request->get_param('mode');
299 $campaignId = $request->get_param('campaignId');
300 $includeSensitiveData = $request->get_param('includeSensitiveData') ? '1' : '0';
301 $anonymousDonors = $request->get_param('anonymousDonors');
302 $anonymousDonations = $anonymousDonors;
303
304 $self_url = rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $donorId));
305
306 $statistics_url = add_query_arg([
307 'mode' => $mode,
308 'campaignId' => $campaignId,
309 ], $self_url . '/statistics');
310
311 $donations_url = rest_url(sprintf('%s/%s', DonationRoute::NAMESPACE, DonationRoute::BASE));
312 $donations_url = add_query_arg([
313 'donorId' => $donorId,
314 'mode' => $mode,
315 'campaignId' => $campaignId,
316 'includeSensitiveData' => $includeSensitiveData,
317 'anonymousDonations' => $anonymousDonations,
318 'page' => 1,
319 'per_page' => 30,
320 ], $donations_url);
321
322 $subscriptions_url = rest_url(sprintf('%s/%s', SubscriptionRoute::NAMESPACE, SubscriptionRoute::BASE));
323 $subscriptions_url = add_query_arg([
324 'donorId' => $donorId,
325 'mode' => $mode,
326 'campaignId' => $campaignId,
327 'includeSensitiveData' => $includeSensitiveData,
328 'anonymousDonors' => $anonymousDonors,
329 'page' => 1,
330 'per_page' => 30,
331 ], $subscriptions_url);
332
333 $links = [
334 'self' => ['href' => $self_url],
335 CURIE::relationUrl('statistics') => [
336 'href' => $statistics_url,
337 'embeddable' => true,
338 ],
339 CURIE::relationUrl('donations') => [
340 'href' => $donations_url,
341 'embeddable' => true,
342 ],
343 CURIE::relationUrl('subscriptions') => [
344 'href' => $subscriptions_url,
345 'embeddable' => true,
346 ],
347 ];
348
349 $response = new WP_REST_Response(Item::formatDatesForResponse($item, ['createdAt']));
350 $response->add_links($links);
351 $response->data = $this->add_additional_fields_to_object($response->data, $request);
352
353 return $response;
354 }
355
356 /**
357 * @since 4.14.0 Add missing properties to the schema
358 * @since 4.13.0 add schema description
359 * @since 4.9.0 Set proper JSON Schema version
360 * @since 4.7.0 Change title to givewp/donor and add custom fields schema
361 * @since 4.4.0
362 */
363 public function get_item_schema(): array
364 {
365 $schema = [
366 '$schema' => 'http://json-schema.org/draft-04/schema#',
367 'title' => 'givewp/donor',
368 'description' => esc_html__('Donor routes for CRUD operations', 'give'),
369 'type' => 'object',
370 'properties' => [
371 'id' => [
372 'type' => 'integer',
373 'description' => esc_html__('Donor ID', 'give'),
374 'readonly' => true,
375 ],
376 'prefix' => [
377 'type' => ['string', 'null'],
378 'description' => esc_html__('Donor prefix', 'give'),
379 'format' => 'text-field',
380 ],
381 'firstName' => [
382 'type' => 'string',
383 'description' => esc_html__('Donor first name', 'give'),
384 'minLength' => 1,
385 'maxLength' => 128,
386 'errorMessage' => esc_html__('First name is required', 'give'),
387 'format' => 'text-field',
388 'required' => true,
389 ],
390 'lastName' => [
391 'type' => 'string',
392 'description' => esc_html__('Donor last name', 'give'),
393 'minLength' => 1,
394 'maxLength' => 128,
395 'errorMessage' => esc_html__('Last name is required', 'give'),
396 'format' => 'text-field',
397 'required' => true,
398 ],
399 'email' => [
400 'type' => 'string',
401 'description' => esc_html__('Donor email', 'give'),
402 'format' => 'email',
403 'required' => true,
404 ],
405 'additionalEmails' => [
406 'type' => 'array',
407 'description' => esc_html__('Donor additional emails', 'give'),
408 'items' => [
409 'type' => 'string',
410 'format' => 'email',
411 ],
412 ],
413 'phone' => [
414 'type' => ['string', 'null'],
415 'description' => esc_html__('Donor phone', 'give'),
416 'pattern' => '^$|^[\+]?[1-9][\d\s\-\(\)]{7,20}$',
417 ],
418 'company' => [
419 'type' => ['string', 'null'],
420 'description' => esc_html__('Donor company', 'give'),
421 'format' => 'text-field',
422 ],
423 'avatarId' => [
424 'type' => ['integer', 'string', 'null'],
425 'description' => esc_html__('Donor avatar ID', 'give'),
426 'pattern' => '^$|^[0-9]+$',
427 'errorMessage' => esc_html__('Invalid avatar ID', 'give'),
428 ],
429 'addresses' => [
430 'type' => 'array',
431 'description' => esc_html__('Donor addresses', 'give'),
432 'items' => [
433 'type' => 'object',
434 'description' => esc_html__('Donor address', 'give'),
435 'properties' => [
436 'address1' => [
437 'type' => 'string',
438 'description' => esc_html__('Donor address line 1', 'give'),
439 'format' => 'text-field',
440 ],
441 'address2' => [
442 'type' => 'string',
443 'description' => esc_html__('Donor address line 2', 'give'),
444 'format' => 'text-field',
445 ],
446 'city' => [
447 'type' => 'string',
448 'description' => esc_html__('Donor address city', 'give'),
449 'format' => 'text-field',
450 ],
451 'state' => [
452 'type' => 'string',
453 'description' => esc_html__('Donor address state', 'give'),
454 'format' => 'text-field',
455 ],
456 'country' => [
457 'type' => 'string',
458 'description' => esc_html__('Donor address country', 'give'),
459 'format' => 'text-field',
460 ],
461 'zip' => [
462 'type' => 'string',
463 'description' => esc_html__('Donor address zip', 'give'),
464 'format' => 'text-field',
465 ],
466 ],
467 ],
468 ],
469 'customFields' => [
470 'type' => 'array',
471 'readonly' => true,
472 'description' => esc_html__('Custom fields (sensitive data)', 'give'),
473 'items' => [
474 'type' => 'object',
475 'properties' => [
476 'label' => [
477 'type' => 'string',
478 'description' => esc_html__('Field label', 'give'),
479 'format' => 'text-field',
480 ],
481 'value' => [
482 'type' => 'string',
483 'description' => esc_html__('Field value', 'give'),
484 'format' => 'text-field',
485 ],
486 ],
487 ],
488 ],
489 'createdAt' => [
490 'type' => ['string', 'null'],
491 'description' => sprintf(
492 /* translators: %s: WordPress documentation URL */
493 esc_html__('Donor creation date in ISO 8601 format. Follows WordPress REST API date format standards. See %s for more information.', 'give'),
494 '<a href="https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#format" target="_blank">WordPress REST API Date and Time</a>'
495 ),
496 'format' => 'date-time',
497 'example' => '2025-09-02T20:27:02',
498 'readonly' => true,
499 ],
500 'userId' => [
501 'type' => ['integer', 'null'],
502 'description' => esc_html__('WordPress user ID associated with the donor', 'give'),
503 'readonly' => true,
504 ],
505 'name' => [
506 'type' => 'string',
507 'description' => esc_html__('Donor full name (calculated from firstName and lastName)', 'give'),
508 'readonly' => true,
509 ],
510 'avatarUrl' => [
511 'type' => ['string', 'null'],
512 'description' => esc_html__('URL of the donor avatar image', 'give'),
513 'format' => 'uri',
514 'readonly' => true,
515 ],
516 'wpUserPermalink' => [
517 'type' => ['string', 'null'],
518 'description' => esc_html__('Link to edit the WordPress user associated with the donor', 'give'),
519 'format' => 'uri',
520 'readonly' => true,
521 ],
522 'totalAmountDonated' => [
523 'type' => 'object',
524 'properties' => [
525 'value' => [
526 'type' => 'number',
527 'description' => esc_html__('Total amount donated in decimal format', 'give'),
528 ],
529 'valueInMinorUnits' => [
530 'type' => 'integer',
531 'description' => esc_html__('Total amount donated in minor units (cents)', 'give'),
532 ],
533 'currency' => [
534 'type' => 'string',
535 'format' => 'text-field',
536 'description' => esc_html__('Currency code (e.g., USD, EUR)', 'give'),
537 ],
538 ],
539 'description' => esc_html__('Total amount donated by the donor', 'give'),
540 'readonly' => true,
541 ],
542 'totalNumberOfDonations' => [
543 'type' => 'integer',
544 'description' => esc_html__('Total number of donations made by the donor', 'give'),
545 'readonly' => true,
546 ],
547 ],
548 ];
549
550 return $this->add_additional_fields_schema($schema);
551 }
552
553 /**
554 * @since 4.8.0 Re-add search parameter
555 * @since 4.4.0
556 */
557 public function get_collection_params(): array
558 {
559 $params = parent::get_collection_params();
560
561 $params['page']['default'] = 1;
562 $params['per_page']['default'] = 30;
563
564 // Remove default parameters not being used
565 unset($params['context']);
566
567 $params += [
568 'sort' => [
569 'description' => __('The field by which to sort the donors.', 'give'),
570 'type' => 'string',
571 'default' => 'id',
572 'enum' => [
573 'id',
574 'createdAt',
575 'name',
576 'firstName',
577 'lastName',
578 'totalAmountDonated',
579 'totalNumberOfDonations',
580 ],
581 ],
582 'direction' => [
583 'description' => __('The direction of sorting: ascending (ASC) or descending (DESC).', 'give'),
584 'type' => 'string',
585 'default' => 'DESC',
586 'enum' => ['ASC', 'DESC'],
587 ],
588 'onlyWithDonations' => [
589 'description' => __('Whether to include only donors who have made donations.', 'give'),
590 'type' => 'boolean',
591 'default' => true,
592 ],
593 'mode' => [
594 'description' => __(
595 'The mode of donations to filter by "live" or "test" (it only gets applied when "onlyWithDonations" is set to true).',
596 'give'
597 ),
598 'type' => 'string',
599 'default' => 'live',
600 'enum' => ['live', 'test'],
601 ],
602 'campaignId' => [
603 'description' => __(
604 'The ID of the campaign to filter donors by - zero or empty mean "all campaigns" (it only gets applied when "onlyWithDonations" is set to true).',
605 'give'
606 ),
607 'type' => 'integer',
608 'default' => 0,
609 ],
610 'search' => [
611 'description' => __('Search donors by name or email.', 'give'),
612 'type' => 'string',
613 ],
614 ];
615
616 return $params;
617 }
618
619 /**
620 * @since 4.13.1 cast totalAmountDonated to decimal
621 * @since 4.0.0
622 */
623 public function getSortColumn(string $sortColumn): string
624 {
625 $sortColumnsMap = [
626 'id' => 'id',
627 'createdAt' => 'date_created',
628 'name' => 'name',
629 'firstName' => 'give_donormeta_attach_meta_firstName.meta_value',
630 'lastName' => 'give_donormeta_attach_meta_lastName.meta_value',
631 'totalAmountDonated' => 'CAST(purchase_value AS DECIMAL(10, 2))',
632 'totalNumberOfDonations' => 'purchase_count',
633 ];
634
635 return $sortColumnsMap[$sortColumn];
636 }
637
638 /**
639 * Get shared parameters for GET methods (both collection and item).
640 *
641 * @since 4.4.0
642 *
643 * @return array
644 */
645 private function getSharedParamsForGetMethods(): array
646 {
647 return [
648 'includeSensitiveData' => [
649 'description' => __(
650 'Include or not include data that can be used to contact or locate the donors, such as phone number, email, billing address, etc. (require proper permissions)',
651 'give'
652 ),
653 'type' => 'boolean',
654 'default' => false,
655 ],
656 'anonymousDonors' => [
657 'description' => __(
658 'Exclude, include, or redact data that can be used to identify the donors, such as ID, first name, last name, etc (require proper permissions).',
659 'give'
660 ),
661 'type' => 'string',
662 'default' => 'exclude',
663 'enum' => ['exclude', 'include', 'redact'],
664 ],
665 ];
666 }
667 }
668