PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.16.8.1
GiveWP – Donation Plugin and Fundraising Platform v4.16.8.1
4.18.0.1 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 All 258 releases
give / src / API / REST / V3 / Routes / Donors / DonorController.php

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

679 lines 26.6 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.16.6 Skip readonly schema properties when applying PATCH updates.
193 * @since 4.8.0 Update donor name when firstName or lastName is updated
194 * @since 4.7.0 Add support for updating custom fields
195 * @since 4.4.0
196 *
197 * @return WP_REST_Response|WP_Error
198 */
199 public function update_item($request)
200 {
201 $donor = Donor::find($request->get_param('id'));
202
203 if (!$donor) {
204 return new WP_REST_Response(__('Donor not found', 'give'), 404);
205 }
206
207 $nonEditableFields = array_merge(
208 [
209 'id',
210 'userId',
211 'createdAt',
212 ],
213 array_keys(
214 array_filter(
215 $this->get_item_schema()['properties'] ?? [],
216 static function (array $property): bool {
217 return ! empty($property['readonly']);
218 }
219 )
220 )
221 );
222
223 foreach ($request->get_params() as $key => $value) {
224 if (! in_array($key, $nonEditableFields, true)) {
225 if ($donor->hasProperty($key)) {
226 if ($key === 'addresses') {
227 $donor->addresses = array_map(function ($address) {
228 return DonorAddress::fromArray($address);
229 }, $value);
230 continue;
231 }
232
233 if (!$donor->isPropertyTypeValid($key, $value)) {
234 $value = null;
235 }
236
237 $donor->$key = $value;
238 }
239 }
240 }
241
242 if ($request->get_param('firstName') || $request->get_param('lastName')) {
243 $donor->name = trim($donor->firstName . ' ' . $donor->lastName);
244 }
245
246 if ($donor->isDirty()) {
247 $donor->save();
248 }
249
250 $item = (new DonorViewModel($donor))->includeSensitiveData(true)->anonymousMode(DonorAnonymousMode::INCLUDED())->exports();
251 $fieldsUpdate = $this->update_additional_fields_for_object($item, $request);
252
253 if (is_wp_error($fieldsUpdate)) {
254 return $fieldsUpdate;
255 }
256
257 $response = $this->prepare_item_for_response($item, $request);
258
259 return rest_ensure_response($response);
260 }
261
262 /**
263 * @since 4.14.0 Use DonorPermissions class
264 * @since 4.0.0
265 *
266 * @param WP_REST_Request $request
267 *
268 * @return true|WP_Error
269 */
270 public function get_items_permissions_check($request)
271 {
272 return DonorPermissions::validationForGetMethods($request);
273 }
274
275 /**
276 * @since 4.14.0 Use DonorPermissions class
277 * @since 4.0.0
278 *
279 * @param WP_REST_Request $request
280 *
281 * @return true|WP_Error
282 */
283 public function get_item_permissions_check($request)
284 {
285 return DonorPermissions::validationForGetMethods($request);
286 }
287
288 /**
289 * @since 4.14.0 Use DonorPermissions class
290 * @since 4.4.0
291 *
292 * @param WP_REST_Request $request
293 *
294 * @return true|WP_Error
295 */
296 public function update_item_permissions_check($request)
297 {
298 return DonorPermissions::validationForUpdateMethod($request);
299 }
300
301 /**
302 * @since 4.14.0 Add links to donations and subscriptions and format dates as strings using Item::formatDatesForResponse
303 * @since 4.7.0 Add support for adding custom fields to the response
304 * @since 4.4.0
305 */
306 public function prepare_item_for_response($item, $request): WP_REST_Response
307 {
308 $donorId = $request->get_param('id');
309 $mode = $request->get_param('mode');
310 $campaignId = $request->get_param('campaignId');
311 $includeSensitiveData = $request->get_param('includeSensitiveData') ? '1' : '0';
312 $anonymousDonors = $request->get_param('anonymousDonors');
313 $anonymousDonations = $anonymousDonors;
314
315 $self_url = rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $donorId));
316
317 $statistics_url = add_query_arg([
318 'mode' => $mode,
319 'campaignId' => $campaignId,
320 ], $self_url . '/statistics');
321
322 $donations_url = rest_url(sprintf('%s/%s', DonationRoute::NAMESPACE, DonationRoute::BASE));
323 $donations_url = add_query_arg([
324 'donorId' => $donorId,
325 'mode' => $mode,
326 'campaignId' => $campaignId,
327 'includeSensitiveData' => $includeSensitiveData,
328 'anonymousDonations' => $anonymousDonations,
329 'page' => 1,
330 'per_page' => 30,
331 ], $donations_url);
332
333 $subscriptions_url = rest_url(sprintf('%s/%s', SubscriptionRoute::NAMESPACE, SubscriptionRoute::BASE));
334 $subscriptions_url = add_query_arg([
335 'donorId' => $donorId,
336 'mode' => $mode,
337 'campaignId' => $campaignId,
338 'includeSensitiveData' => $includeSensitiveData,
339 'anonymousDonors' => $anonymousDonors,
340 'page' => 1,
341 'per_page' => 30,
342 ], $subscriptions_url);
343
344 $links = [
345 'self' => ['href' => $self_url],
346 CURIE::relationUrl('statistics') => [
347 'href' => $statistics_url,
348 'embeddable' => true,
349 ],
350 CURIE::relationUrl('donations') => [
351 'href' => $donations_url,
352 'embeddable' => true,
353 ],
354 CURIE::relationUrl('subscriptions') => [
355 'href' => $subscriptions_url,
356 'embeddable' => true,
357 ],
358 ];
359
360 $response = new WP_REST_Response(Item::formatDatesForResponse($item, ['createdAt']));
361 $response->add_links($links);
362 $response->data = $this->add_additional_fields_to_object($response->data, $request);
363
364 return $response;
365 }
366
367 /**
368 * @since 4.14.0 Add missing properties to the schema
369 * @since 4.13.0 add schema description
370 * @since 4.9.0 Set proper JSON Schema version
371 * @since 4.7.0 Change title to givewp/donor and add custom fields schema
372 * @since 4.4.0
373 */
374 public function get_item_schema(): array
375 {
376 $schema = [
377 '$schema' => 'http://json-schema.org/draft-04/schema#',
378 'title' => 'givewp/donor',
379 'description' => esc_html__('Donor routes for CRUD operations', 'give'),
380 'type' => 'object',
381 'properties' => [
382 'id' => [
383 'type' => 'integer',
384 'description' => esc_html__('Donor ID', 'give'),
385 'readonly' => true,
386 ],
387 'prefix' => [
388 'type' => ['string', 'null'],
389 'description' => esc_html__('Donor prefix', 'give'),
390 'format' => 'text-field',
391 ],
392 'firstName' => [
393 'type' => 'string',
394 'description' => esc_html__('Donor first name', 'give'),
395 'minLength' => 1,
396 'maxLength' => 128,
397 'errorMessage' => esc_html__('First name is required', 'give'),
398 'format' => 'text-field',
399 'required' => true,
400 ],
401 'lastName' => [
402 'type' => 'string',
403 'description' => esc_html__('Donor last name', 'give'),
404 'minLength' => 1,
405 'maxLength' => 128,
406 'errorMessage' => esc_html__('Last name is required', 'give'),
407 'format' => 'text-field',
408 'required' => true,
409 ],
410 'email' => [
411 'type' => 'string',
412 'description' => esc_html__('Donor email', 'give'),
413 'format' => 'email',
414 'required' => true,
415 ],
416 'additionalEmails' => [
417 'type' => 'array',
418 'description' => esc_html__('Donor additional emails', 'give'),
419 'items' => [
420 'type' => 'string',
421 'format' => 'email',
422 ],
423 ],
424 'phone' => [
425 'type' => ['string', 'null'],
426 'description' => esc_html__('Donor phone', 'give'),
427 'pattern' => '^$|^[\+]?[1-9][\d\s\-\(\)]{7,20}$',
428 ],
429 'company' => [
430 'type' => ['string', 'null'],
431 'description' => esc_html__('Donor company', 'give'),
432 'format' => 'text-field',
433 ],
434 'avatarId' => [
435 'type' => ['integer', 'string', 'null'],
436 'description' => esc_html__('Donor avatar ID', 'give'),
437 'pattern' => '^$|^[0-9]+$',
438 'errorMessage' => esc_html__('Invalid avatar ID', 'give'),
439 ],
440 'addresses' => [
441 'type' => 'array',
442 'description' => esc_html__('Donor addresses', 'give'),
443 'items' => [
444 'type' => 'object',
445 'description' => esc_html__('Donor address', 'give'),
446 'properties' => [
447 'address1' => [
448 'type' => 'string',
449 'description' => esc_html__('Donor address line 1', 'give'),
450 'format' => 'text-field',
451 ],
452 'address2' => [
453 'type' => 'string',
454 'description' => esc_html__('Donor address line 2', 'give'),
455 'format' => 'text-field',
456 ],
457 'city' => [
458 'type' => 'string',
459 'description' => esc_html__('Donor address city', 'give'),
460 'format' => 'text-field',
461 ],
462 'state' => [
463 'type' => 'string',
464 'description' => esc_html__('Donor address state', 'give'),
465 'format' => 'text-field',
466 ],
467 'country' => [
468 'type' => 'string',
469 'description' => esc_html__('Donor address country', 'give'),
470 'format' => 'text-field',
471 ],
472 'zip' => [
473 'type' => 'string',
474 'description' => esc_html__('Donor address zip', 'give'),
475 'format' => 'text-field',
476 ],
477 ],
478 ],
479 ],
480 'customFields' => [
481 'type' => 'array',
482 'readonly' => true,
483 'description' => esc_html__('Custom fields (sensitive data)', 'give'),
484 'items' => [
485 'type' => 'object',
486 'properties' => [
487 'label' => [
488 'type' => 'string',
489 'description' => esc_html__('Field label', 'give'),
490 'format' => 'text-field',
491 ],
492 'value' => [
493 'type' => 'string',
494 'description' => esc_html__('Field value', 'give'),
495 'format' => 'text-field',
496 ],
497 ],
498 ],
499 ],
500 'createdAt' => [
501 'type' => ['string', 'null'],
502 'description' => sprintf(
503 /* translators: %s: WordPress documentation URL */
504 esc_html__('Donor creation date in ISO 8601 format. Follows WordPress REST API date format standards. See %s for more information.', 'give'),
505 '<a href="https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#format" target="_blank">WordPress REST API Date and Time</a>'
506 ),
507 'format' => 'date-time',
508 'example' => '2025-09-02T20:27:02',
509 'readonly' => true,
510 ],
511 'userId' => [
512 'type' => ['integer', 'null'],
513 'description' => esc_html__('WordPress user ID associated with the donor', 'give'),
514 'readonly' => true,
515 ],
516 'name' => [
517 'type' => 'string',
518 'description' => esc_html__('Donor full name (calculated from firstName and lastName)', 'give'),
519 'readonly' => true,
520 ],
521 'avatarUrl' => [
522 'type' => ['string', 'null'],
523 'description' => esc_html__('URL of the donor avatar image', 'give'),
524 'format' => 'uri',
525 'readonly' => true,
526 ],
527 'wpUserPermalink' => [
528 'type' => ['string', 'null'],
529 'description' => esc_html__('Link to edit the WordPress user associated with the donor', 'give'),
530 'format' => 'uri',
531 'readonly' => true,
532 ],
533 'totalAmountDonated' => [
534 'type' => 'object',
535 'properties' => [
536 'value' => [
537 'type' => 'number',
538 'description' => esc_html__('Total amount donated in decimal format', 'give'),
539 ],
540 'valueInMinorUnits' => [
541 'type' => 'integer',
542 'description' => esc_html__('Total amount donated in minor units (cents)', 'give'),
543 ],
544 'currency' => [
545 'type' => 'string',
546 'format' => 'text-field',
547 'description' => esc_html__('Currency code (e.g., USD, EUR)', 'give'),
548 ],
549 ],
550 'description' => esc_html__('Total amount donated by the donor', 'give'),
551 'readonly' => true,
552 ],
553 'totalNumberOfDonations' => [
554 'type' => 'integer',
555 'description' => esc_html__('Total number of donations made by the donor', 'give'),
556 'readonly' => true,
557 ],
558 ],
559 ];
560
561 return $this->add_additional_fields_schema($schema);
562 }
563
564 /**
565 * @since 4.8.0 Re-add search parameter
566 * @since 4.4.0
567 */
568 public function get_collection_params(): array
569 {
570 $params = parent::get_collection_params();
571
572 $params['page']['default'] = 1;
573 $params['per_page']['default'] = 30;
574
575 // Remove default parameters not being used
576 unset($params['context']);
577
578 $params += [
579 'sort' => [
580 'description' => __('The field by which to sort the donors.', 'give'),
581 'type' => 'string',
582 'default' => 'id',
583 'enum' => [
584 'id',
585 'createdAt',
586 'name',
587 'firstName',
588 'lastName',
589 'totalAmountDonated',
590 'totalNumberOfDonations',
591 ],
592 ],
593 'direction' => [
594 'description' => __('The direction of sorting: ascending (ASC) or descending (DESC).', 'give'),
595 'type' => 'string',
596 'default' => 'DESC',
597 'enum' => ['ASC', 'DESC'],
598 ],
599 'onlyWithDonations' => [
600 'description' => __('Whether to include only donors who have made donations.', 'give'),
601 'type' => 'boolean',
602 'default' => true,
603 ],
604 'mode' => [
605 'description' => __(
606 'The mode of donations to filter by "live" or "test" (it only gets applied when "onlyWithDonations" is set to true).',
607 'give'
608 ),
609 'type' => 'string',
610 'default' => 'live',
611 'enum' => ['live', 'test'],
612 ],
613 'campaignId' => [
614 'description' => __(
615 '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).',
616 'give'
617 ),
618 'type' => 'integer',
619 'default' => 0,
620 ],
621 'search' => [
622 'description' => __('Search donors by name or email.', 'give'),
623 'type' => 'string',
624 ],
625 ];
626
627 return $params;
628 }
629
630 /**
631 * @since 4.13.1 cast totalAmountDonated to decimal
632 * @since 4.0.0
633 */
634 public function getSortColumn(string $sortColumn): string
635 {
636 $sortColumnsMap = [
637 'id' => 'id',
638 'createdAt' => 'date_created',
639 'name' => 'name',
640 'firstName' => 'give_donormeta_attach_meta_firstName.meta_value',
641 'lastName' => 'give_donormeta_attach_meta_lastName.meta_value',
642 'totalAmountDonated' => 'CAST(purchase_value AS DECIMAL(10, 2))',
643 'totalNumberOfDonations' => 'purchase_count',
644 ];
645
646 return $sortColumnsMap[$sortColumn];
647 }
648
649 /**
650 * Get shared parameters for GET methods (both collection and item).
651 *
652 * @since 4.4.0
653 *
654 * @return array
655 */
656 private function getSharedParamsForGetMethods(): array
657 {
658 return [
659 'includeSensitiveData' => [
660 'description' => __(
661 '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)',
662 'give'
663 ),
664 'type' => 'boolean',
665 'default' => false,
666 ],
667 'anonymousDonors' => [
668 'description' => __(
669 'Exclude, include, or redact data that can be used to identify the donors, such as ID, first name, last name, etc (require proper permissions).',
670 'give'
671 ),
672 'type' => 'string',
673 'default' => 'exclude',
674 'enum' => ['exclude', 'include', 'redact'],
675 ],
676 ];
677 }
678 }
679