Completed
Branch master (89fa75)
by
unknown
05:04
created
core/helpers/EEH_Line_Item.helper.php 2 patches
Indentation   +2236 added lines, -2236 removed lines patch added patch discarded remove patch
@@ -21,2240 +21,2240 @@
 block discarded – undo
21 21
  */
22 22
 class EEH_Line_Item
23 23
 {
24
-    /**
25
-     * @var EE_Line_Item[]|null
26
-     */
27
-    private static ?array $global_taxes = null;
28
-
29
-
30
-    /**
31
-     * Adds a simple item (unrelated to any other model object) to the provided PARENT line item.
32
-     * Does NOT automatically re-calculate the line item totals or update the related transaction.
33
-     * You should call recalculate_total_including_taxes() on the grant total line item after this
34
-     * to update the subtotals, and EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
35
-     * to keep the registration final prices in-sync with the transaction's total.
36
-     *
37
-     * @param EE_Line_Item $parent_line_item
38
-     * @param string       $name
39
-     * @param float        $unit_price
40
-     * @param string       $description
41
-     * @param int          $quantity
42
-     * @param boolean      $taxable
43
-     * @param string|null  $code if set to a value, ensures there is only one line item with that code
44
-     * @param bool         $return_item
45
-     * @param bool         $recalculate_totals
46
-     * @return boolean|EE_Line_Item success
47
-     * @throws EE_Error
48
-     * @throws ReflectionException
49
-     */
50
-    public static function add_unrelated_item(
51
-        EE_Line_Item $parent_line_item,
52
-        string $name,
53
-        float $unit_price,
54
-        string $description = '',
55
-        int $quantity = 1,
56
-        bool $taxable = false,
57
-        ?string $code = null,
58
-        bool $return_item = false,
59
-        bool $recalculate_totals = true
60
-    ) {
61
-        $items_subtotal = self::get_pre_tax_subtotal($parent_line_item);
62
-        $line_item      = EE_Line_Item::new_instance(
63
-            [
64
-                'LIN_name'       => $name,
65
-                'LIN_desc'       => $description,
66
-                'LIN_unit_price' => $unit_price,
67
-                'LIN_quantity'   => $quantity,
68
-                'LIN_percent'    => null,
69
-                'LIN_is_taxable' => $taxable,
70
-                'LIN_order'      => count($items_subtotal->children()),
71
-                'LIN_total'      => $unit_price * $quantity,
72
-                'LIN_type'       => EEM_Line_Item::type_line_item,
73
-                'LIN_code'       => $code,
74
-            ]
75
-        );
76
-        $line_item      = apply_filters(
77
-            'FHEE__EEH_Line_Item__add_unrelated_item__line_item',
78
-            $line_item,
79
-            $parent_line_item
80
-        );
81
-        $added          = self::add_item($parent_line_item, $line_item, $recalculate_totals);
82
-        return $return_item ? $line_item : $added;
83
-    }
84
-
85
-
86
-    /**
87
-     * Adds a simple item ( unrelated to any other model object) to the total line item,
88
-     * in the correct spot in the line item tree. Does not automatically
89
-     * re-calculate the line item totals, nor update the related transaction, nor upgrade the transaction's
90
-     * registrations' final prices (which should probably change because of this).
91
-     * You should call recalculate_total_including_taxes() on the grand total line item, then
92
-     * update the transaction's total, and EE_Registration_Processor::update_registration_final_prices()
93
-     * after using this, to keep the registration final prices in-sync with the transaction's total.
94
-     *
95
-     * @param EE_Line_Item $parent_line_item
96
-     * @param string       $name
97
-     * @param float        $percentage_amount
98
-     * @param string       $description
99
-     * @param boolean      $taxable
100
-     * @param string|null  $code
101
-     * @param bool         $return_item
102
-     * @return boolean|EE_Line_Item success
103
-     * @throws EE_Error
104
-     * @throws ReflectionException
105
-     */
106
-    public static function add_percentage_based_item(
107
-        EE_Line_Item $parent_line_item,
108
-        string $name,
109
-        float $percentage_amount,
110
-        string $description = '',
111
-        bool $taxable = false,
112
-        ?string $code = null,
113
-        bool $return_item = false
114
-    ) {
115
-        $total     = $percentage_amount * $parent_line_item->total() / 100;
116
-        $line_item = EE_Line_Item::new_instance(
117
-            [
118
-                'LIN_name'       => $name,
119
-                'LIN_desc'       => $description,
120
-                'LIN_unit_price' => 0,
121
-                'LIN_percent'    => $percentage_amount,
122
-                'LIN_is_taxable' => $taxable,
123
-                'LIN_total'      => $total,
124
-                'LIN_type'       => EEM_Line_Item::type_line_item,
125
-                'LIN_parent'     => $parent_line_item->ID(),
126
-                'LIN_code'       => $code,
127
-                'TXN_ID'         => $parent_line_item->TXN_ID(),
128
-            ]
129
-        );
130
-        $line_item = apply_filters(
131
-            'FHEE__EEH_Line_Item__add_percentage_based_item__line_item',
132
-            $line_item
133
-        );
134
-        $added     = $parent_line_item->add_child_line_item($line_item, false);
135
-        return $return_item ? $line_item : $added;
136
-    }
137
-
138
-
139
-    /**
140
-     * Returns the new line item created by adding a purchase of the ticket
141
-     * ensures that ticket line item is saved, and that cart total has been recalculated.
142
-     * If this ticket has already been purchased, just increments its count.
143
-     * Automatically re-calculates the line item totals and updates the related transaction. But
144
-     * DOES NOT automatically upgrade the transaction's registrations' final prices (which
145
-     * should probably change because of this).
146
-     * You should call EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
147
-     * after using this, to keep the registration final prices in-sync with the transaction's total.
148
-     *
149
-     * @param EE_Line_Item|null $total_line_item grand total line item of type EEM_Line_Item::type_total
150
-     * @param EE_Ticket         $ticket
151
-     * @param int               $qty
152
-     * @param bool              $recalculate_totals
153
-     * @return EE_Line_Item
154
-     * @throws EE_Error
155
-     * @throws ReflectionException
156
-     */
157
-    public static function add_ticket_purchase(
158
-        ?EE_Line_Item $total_line_item,
159
-        EE_Ticket $ticket,
160
-        int $qty = 1,
161
-        bool $recalculate_totals = true
162
-    ): ?EE_Line_Item {
163
-        if (! $total_line_item instanceof EE_Line_Item || ! $total_line_item->is_total()) {
164
-            throw new EE_Error(
165
-                sprintf(
166
-                    esc_html__(
167
-                        'A valid line item total is required in order to add tickets. A line item of type "%s" was passed.',
168
-                        'event_espresso'
169
-                    ),
170
-                    $ticket->ID(),
171
-                    $total_line_item->ID()
172
-                )
173
-            );
174
-        }
175
-        // either increment the qty for an existing ticket
176
-        $line_item = self::increment_ticket_qty_if_already_in_cart($total_line_item, $ticket, $qty);
177
-        // or add a new one
178
-        if (! $line_item instanceof EE_Line_Item) {
179
-            $line_item = self::create_ticket_line_item($total_line_item, $ticket, $qty);
180
-        }
181
-        if ($recalculate_totals) {
182
-            $total_line_item->recalculate_total_including_taxes();
183
-        }
184
-        return $line_item;
185
-    }
186
-
187
-
188
-    /**
189
-     * Returns the new line item created by adding a purchase of the ticket
190
-     *
191
-     * @param EE_Line_Item|null $total_line_item
192
-     * @param EE_Ticket         $ticket
193
-     * @param int               $qty
194
-     * @return EE_Line_Item
195
-     * @throws EE_Error
196
-     * @throws InvalidArgumentException
197
-     * @throws InvalidDataTypeException
198
-     * @throws InvalidInterfaceException
199
-     * @throws ReflectionException
200
-     */
201
-    public static function increment_ticket_qty_if_already_in_cart(
202
-        ?EE_Line_Item $total_line_item,
203
-        EE_Ticket $ticket,
204
-        int $qty = 1
205
-    ): ?EE_Line_Item {
206
-        $line_item = null;
207
-        if ($total_line_item instanceof EE_Line_Item && $total_line_item->is_total()) {
208
-            $ticket_line_items = EEH_Line_Item::get_ticket_line_items($total_line_item);
209
-            foreach ($ticket_line_items as $ticket_line_item) {
210
-                if (
211
-                    $ticket_line_item instanceof EE_Line_Item
212
-                    && $ticket_line_item->OBJ_ID() === $ticket->ID()
213
-                ) {
214
-                    $line_item = $ticket_line_item;
215
-                    break;
216
-                }
217
-            }
218
-        }
219
-        if ($line_item instanceof EE_Line_Item) {
220
-            EEH_Line_Item::increment_quantity($line_item, $qty);
221
-            return $line_item;
222
-        }
223
-        return null;
224
-    }
225
-
226
-
227
-    /**
228
-     * Increments the line item and all its children's quantity by $qty (but percent line items are unaffected).
229
-     * Does NOT save or recalculate other line items totals
230
-     *
231
-     * @param EE_Line_Item $line_item
232
-     * @param int          $qty
233
-     * @return void
234
-     * @throws EE_Error
235
-     * @throws InvalidArgumentException
236
-     * @throws InvalidDataTypeException
237
-     * @throws InvalidInterfaceException
238
-     * @throws ReflectionException
239
-     */
240
-    public static function increment_quantity(EE_Line_Item $line_item, int $qty = 1)
241
-    {
242
-        if (! $line_item->is_percent()) {
243
-            $qty += $line_item->quantity();
244
-            $line_item->set_quantity($qty);
245
-            $line_item->set_total($line_item->unit_price() * $qty);
246
-            $line_item->save();
247
-        }
248
-        foreach ($line_item->children() as $child) {
249
-            if ($child->is_sub_line_item()) {
250
-                EEH_Line_Item::update_quantity($child, $qty);
251
-            }
252
-        }
253
-    }
254
-
255
-
256
-    /**
257
-     * Decrements the line item and all its children's quantity by $qty (but percent line items are unaffected).
258
-     * Does NOT save or recalculate other line items totals
259
-     *
260
-     * @param EE_Line_Item $line_item
261
-     * @param int          $qty
262
-     * @return void
263
-     * @throws EE_Error
264
-     * @throws InvalidArgumentException
265
-     * @throws InvalidDataTypeException
266
-     * @throws InvalidInterfaceException
267
-     * @throws ReflectionException
268
-     */
269
-    public static function decrement_quantity(EE_Line_Item $line_item, int $qty = 1)
270
-    {
271
-        if (! $line_item->is_percent()) {
272
-            $qty = $line_item->quantity() - $qty;
273
-            $qty = max($qty, 0);
274
-            $line_item->set_quantity($qty);
275
-            $line_item->set_total($line_item->unit_price() * $qty);
276
-            $line_item->save();
277
-        }
278
-        foreach ($line_item->children() as $child) {
279
-            if ($child->is_sub_line_item()) {
280
-                EEH_Line_Item::update_quantity($child, $qty);
281
-            }
282
-        }
283
-    }
284
-
285
-
286
-    /**
287
-     * Updates the line item and its children's quantities to the specified number.
288
-     * Does NOT save them or recalculate totals.
289
-     *
290
-     * @param EE_Line_Item $line_item
291
-     * @param int          $new_quantity
292
-     * @throws EE_Error
293
-     * @throws InvalidArgumentException
294
-     * @throws InvalidDataTypeException
295
-     * @throws InvalidInterfaceException
296
-     * @throws ReflectionException
297
-     */
298
-    public static function update_quantity(EE_Line_Item $line_item, int $new_quantity)
299
-    {
300
-        if (! $line_item->is_percent()) {
301
-            $line_item->set_quantity($new_quantity);
302
-            $line_item->set_total($line_item->unit_price() * $new_quantity);
303
-            $line_item->save();
304
-        }
305
-        foreach ($line_item->children() as $child) {
306
-            if ($child->is_sub_line_item()) {
307
-                EEH_Line_Item::update_quantity($child, $new_quantity);
308
-            }
309
-        }
310
-    }
311
-
312
-
313
-    /**
314
-     * Returns the new line item created by adding a purchase of the ticket
315
-     *
316
-     * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
317
-     * @param EE_Ticket    $ticket
318
-     * @param int          $qty
319
-     * @return EE_Line_Item
320
-     * @throws EE_Error
321
-     * @throws InvalidArgumentException
322
-     * @throws InvalidDataTypeException
323
-     * @throws InvalidInterfaceException
324
-     * @throws ReflectionException
325
-     */
326
-    public static function create_ticket_line_item(
327
-        EE_Line_Item $total_line_item,
328
-        EE_Ticket $ticket,
329
-        int $qty = 1
330
-    ): EE_Line_Item {
331
-        $datetimes           = $ticket->datetimes();
332
-        $first_datetime      = reset($datetimes);
333
-        $first_datetime_name = esc_html__('Event', 'event_espresso');
334
-        if ($first_datetime instanceof EE_Datetime && $first_datetime->event() instanceof EE_Event) {
335
-            $first_datetime_name = $first_datetime->event()->name();
336
-        }
337
-        $event = sprintf(_x('(For %1$s)', '(For Event Name)', 'event_espresso'), $first_datetime_name);
338
-        // get event subtotal line
339
-        $events_sub_total = self::get_event_line_item_for_ticket($total_line_item, $ticket);
340
-        $taxes            = $ticket->tax_price_modifiers();
341
-        // add $ticket to cart
342
-        $line_item = EE_Line_Item::new_instance(
343
-            [
344
-                'LIN_name'       => $ticket->name(),
345
-                'LIN_desc'       => $ticket->description() !== '' ? $ticket->description() . ' ' . $event : $event,
346
-                'LIN_unit_price' => $ticket->price(),
347
-                'LIN_quantity'   => $qty,
348
-                'LIN_is_taxable' => empty($taxes) && $ticket->taxable(),
349
-                'LIN_order'      => count($events_sub_total->children()),
350
-                'LIN_total'      => $ticket->price() * $qty,
351
-                'LIN_type'       => EEM_Line_Item::type_line_item,
352
-                'OBJ_ID'         => $ticket->ID(),
353
-                'OBJ_type'       => EEM_Line_Item::OBJ_TYPE_TICKET,
354
-            ]
355
-        );
356
-        $line_item = apply_filters(
357
-            'FHEE__EEH_Line_Item__create_ticket_line_item__line_item',
358
-            $line_item
359
-        );
360
-        if (! $line_item instanceof EE_Line_Item) {
361
-            throw new DomainException(
362
-                esc_html__('Invalid EE_Line_Item received.', 'event_espresso')
363
-            );
364
-        }
365
-        $events_sub_total->add_child_line_item($line_item);
366
-        // now add the sub-line items
367
-        $running_pre_tax_total = 0;
368
-        $prices                = $ticket->prices();
369
-        if (empty($prices)) {
370
-            // WUT?!?! NO PRICES??? Well, just create a default price then.
371
-            $default_price = EEM_Price::instance()->get_new_price();
372
-            if ($default_price->amount() !== $ticket->price()) {
373
-                $default_price->set_amount($ticket->price());
374
-            }
375
-            $default_price->save();
376
-            $ticket->_add_relation_to($default_price, 'Price');
377
-            $ticket->save();
378
-            $prices = [$default_price];
379
-        }
380
-        foreach ($prices as $price) {
381
-            $sign = $price->is_discount() ? -1 : 1;
382
-            $price_amount = $price->amount();
383
-            if ($price->is_percent()) {
384
-                $price_total = $running_pre_tax_total * $price_amount / 100;
385
-                $percent    = $sign * $price_amount;
386
-                $unit_price = 0;
387
-            } else {
388
-                $price_total = $price_amount * $qty;
389
-                $percent    = 0;
390
-                $unit_price = $sign * $price_amount;
391
-            }
392
-
393
-            $price_desc = $price->desc();
394
-            $price_type = $price->type_obj();
395
-            $price_desc = $price_desc === '' && $price_type instanceof EE_Price_Type
396
-                ? $price_type->name()
397
-                : $price_desc;
398
-
399
-            $sub_line_item         = EE_Line_Item::new_instance(
400
-                [
401
-                    'LIN_name'       => $price->name(),
402
-                    'LIN_desc'       => $price_desc,
403
-                    'LIN_is_taxable' => false,
404
-                    'LIN_order'      => $price->order(),
405
-                    'LIN_total'      => $price_total,
406
-                    'LIN_pretax'     => 0,
407
-                    'LIN_unit_price' => $unit_price,
408
-                    'LIN_percent'    => $percent,
409
-                    'LIN_type'       => $price->is_tax()
410
-                        ? EEM_Line_Item::type_sub_tax
411
-                        : EEM_Line_Item::type_sub_line_item,
412
-                    'OBJ_ID'         => $price->ID(),
413
-                    'OBJ_type'       => EEM_Line_Item::OBJ_TYPE_PRICE,
414
-                ]
415
-            );
416
-            $sub_line_item         = apply_filters(
417
-                'FHEE__EEH_Line_Item__create_ticket_line_item__sub_line_item',
418
-                $sub_line_item
419
-            );
420
-            $running_pre_tax_total += ! $price->is_tax() ? $sign * $price_total : 0;
421
-            $line_item->add_child_line_item($sub_line_item);
422
-        }
423
-        $line_item->setPretaxTotal($running_pre_tax_total);
424
-        return $line_item;
425
-    }
426
-
427
-
428
-    /**
429
-     * Adds the specified item under the pre-tax-sub-total line item. Automatically
430
-     * re-calculates the line item totals and updates the related transaction. But
431
-     * DOES NOT automatically upgrade the transaction's registrations' final prices (which
432
-     * should probably change because of this).
433
-     * You should call EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
434
-     * after using this, to keep the registration final prices in-sync with the transaction's total.
435
-     *
436
-     * @param EE_Line_Item $total_line_item
437
-     * @param EE_Line_Item $item to be added
438
-     * @param bool         $recalculate_totals
439
-     * @return boolean
440
-     * @throws EE_Error
441
-     * @throws InvalidArgumentException
442
-     * @throws InvalidDataTypeException
443
-     * @throws InvalidInterfaceException
444
-     * @throws ReflectionException
445
-     */
446
-    public static function add_item(
447
-        EE_Line_Item $total_line_item,
448
-        EE_Line_Item $item,
449
-        bool $recalculate_totals = true
450
-    ): bool {
451
-        $pre_tax_subtotal = self::get_pre_tax_subtotal($total_line_item);
452
-        $success          = $pre_tax_subtotal->add_child_line_item($item);
453
-        if ($recalculate_totals) {
454
-            $total_line_item->recalculate_total_including_taxes();
455
-        }
456
-        return $success;
457
-    }
458
-
459
-
460
-    /**
461
-     * cancels an existing ticket line item,
462
-     * by decrementing its quantity by 1 and adding a new "type_cancellation" sub-line-item.
463
-     * ALL totals and subtotals will NEED TO BE UPDATED after performing this action
464
-     *
465
-     * @param EE_Line_Item $ticket_line_item
466
-     * @param int          $qty
467
-     * @return bool success
468
-     * @throws EE_Error
469
-     * @throws InvalidArgumentException
470
-     * @throws InvalidDataTypeException
471
-     * @throws InvalidInterfaceException
472
-     * @throws ReflectionException
473
-     */
474
-    public static function cancel_ticket_line_item(EE_Line_Item $ticket_line_item, int $qty = 1): bool
475
-    {
476
-        // validate incoming line_item
477
-        if ($ticket_line_item->OBJ_type() !== EEM_Line_Item::OBJ_TYPE_TICKET) {
478
-            throw new EE_Error(
479
-                sprintf(
480
-                    esc_html__(
481
-                        'The supplied line item must have an Object Type of "Ticket", not %1$s.',
482
-                        'event_espresso'
483
-                    ),
484
-                    $ticket_line_item->type()
485
-                )
486
-            );
487
-        }
488
-        if ($ticket_line_item->quantity() < $qty) {
489
-            throw new EE_Error(
490
-                sprintf(
491
-                    esc_html__(
492
-                        'Can not cancel %1$d ticket(s) because the supplied line item has a quantity of %2$d.',
493
-                        'event_espresso'
494
-                    ),
495
-                    $qty,
496
-                    $ticket_line_item->quantity()
497
-                )
498
-            );
499
-        }
500
-        // decrement ticket quantity; don't rely on auto-fixing when recalculating totals to do this
501
-        $ticket_line_item->set_quantity($ticket_line_item->quantity() - $qty);
502
-        foreach ($ticket_line_item->children() as $child_line_item) {
503
-            if (
504
-                $child_line_item->is_sub_line_item()
505
-                && ! $child_line_item->is_percent()
506
-                && ! $child_line_item->is_cancellation()
507
-            ) {
508
-                $child_line_item->set_quantity($child_line_item->quantity() - $qty);
509
-            }
510
-        }
511
-        // get cancellation sub line item
512
-        $cancellation_line_item = EEH_Line_Item::get_descendants_of_type(
513
-            $ticket_line_item,
514
-            EEM_Line_Item::type_cancellation
515
-        );
516
-        $cancellation_line_item = reset($cancellation_line_item);
517
-        // verify that this ticket was indeed previously cancelled
518
-        if ($cancellation_line_item instanceof EE_Line_Item) {
519
-            // increment cancelled quantity
520
-            $cancellation_line_item->set_quantity($cancellation_line_item->quantity() + $qty);
521
-        } else {
522
-            // create cancellation sub line item
523
-            $cancellation_line_item = EE_Line_Item::new_instance(
524
-                [
525
-                    'LIN_name'       => esc_html__('Cancellation', 'event_espresso'),
526
-                    'LIN_desc'       => sprintf(
527
-                        esc_html_x(
528
-                            'Cancelled %1$s : %2$s',
529
-                            'Cancelled Ticket Name : 2015-01-01 11:11',
530
-                            'event_espresso'
531
-                        ),
532
-                        $ticket_line_item->name(),
533
-                        current_time(get_option('date_format') . ' ' . get_option('time_format'))
534
-                    ),
535
-                    'LIN_total'      => 0,
536
-                    'LIN_unit_price' => 0,
537
-                    'LIN_quantity'   => $qty,
538
-                    'LIN_is_taxable' => $ticket_line_item->is_taxable(),
539
-                    'LIN_order'      => count($ticket_line_item->children()),
540
-                    'LIN_type'       => EEM_Line_Item::type_cancellation,
541
-                ]
542
-            );
543
-            $ticket_line_item->add_child_line_item($cancellation_line_item);
544
-        }
545
-        if ($ticket_line_item->save_this_and_descendants() > 0) {
546
-            // decrement parent line item quantity
547
-            $event_line_item = $ticket_line_item->parent();
548
-            if (
549
-                $event_line_item instanceof EE_Line_Item
550
-                && $event_line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_EVENT
551
-            ) {
552
-                $event_line_item->set_quantity($event_line_item->quantity() - $qty);
553
-                $event_line_item->save();
554
-            }
555
-            EEH_Line_Item::get_grand_total_and_recalculate_everything($ticket_line_item);
556
-            return true;
557
-        }
558
-        return false;
559
-    }
560
-
561
-
562
-    /**
563
-     * reinstates (un-cancels?) a previously canceled ticket line item,
564
-     * by incrementing its quantity by 1, and decrementing its "type_cancellation" sub-line-item.
565
-     * ALL totals and subtotals will NEED TO BE UPDATED after performing this action
566
-     *
567
-     * @param EE_Line_Item $ticket_line_item
568
-     * @param int          $qty
569
-     * @return bool success
570
-     * @throws EE_Error
571
-     * @throws InvalidArgumentException
572
-     * @throws InvalidDataTypeException
573
-     * @throws InvalidInterfaceException
574
-     * @throws ReflectionException
575
-     */
576
-    public static function reinstate_canceled_ticket_line_item(EE_Line_Item $ticket_line_item, int $qty = 1): bool
577
-    {
578
-        // validate incoming line_item
579
-        if ($ticket_line_item->OBJ_type() !== EEM_Line_Item::OBJ_TYPE_TICKET) {
580
-            throw new EE_Error(
581
-                sprintf(
582
-                    esc_html__(
583
-                        'The supplied line item must have an Object Type of "Ticket", not %1$s.',
584
-                        'event_espresso'
585
-                    ),
586
-                    $ticket_line_item->type()
587
-                )
588
-            );
589
-        }
590
-        // get cancellation sub line item
591
-        $cancellation_line_item = EEH_Line_Item::get_descendants_of_type(
592
-            $ticket_line_item,
593
-            EEM_Line_Item::type_cancellation
594
-        );
595
-        $cancellation_line_item = reset($cancellation_line_item);
596
-        // verify that this ticket was indeed previously cancelled
597
-        if (! $cancellation_line_item instanceof EE_Line_Item) {
598
-            return false;
599
-        }
600
-        if ($cancellation_line_item->quantity() > $qty) {
601
-            // decrement cancelled quantity
602
-            $cancellation_line_item->set_quantity($cancellation_line_item->quantity() - $qty);
603
-        } elseif ($cancellation_line_item->quantity() === $qty) {
604
-            // decrement cancelled quantity in case anyone still has the object kicking around
605
-            $cancellation_line_item->set_quantity($cancellation_line_item->quantity() - $qty);
606
-            // delete because quantity will end up as 0
607
-            $cancellation_line_item->delete();
608
-            // and attempt to destroy the object,
609
-            // even though PHP won't actually destroy it until it needs the memory
610
-            unset($cancellation_line_item);
611
-        } else {
612
-            // what ?!?! negative quantity ?!?!
613
-            throw new EE_Error(
614
-                sprintf(
615
-                    esc_html__(
616
-                        'Can not reinstate %1$d cancelled ticket(s) because the cancelled ticket quantity is only %2$d.',
617
-                        'event_espresso'
618
-                    ),
619
-                    $qty,
620
-                    $cancellation_line_item->quantity()
621
-                )
622
-            );
623
-        }
624
-        // increment ticket quantity
625
-        $ticket_line_item->set_quantity($ticket_line_item->quantity() + $qty);
626
-        if ($ticket_line_item->save_this_and_descendants() > 0) {
627
-            // increment parent line item quantity
628
-            $event_line_item = $ticket_line_item->parent();
629
-            if (
630
-                $event_line_item instanceof EE_Line_Item
631
-                && $event_line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_EVENT
632
-            ) {
633
-                $event_line_item->set_quantity($event_line_item->quantity() + $qty);
634
-            }
635
-            EEH_Line_Item::get_grand_total_and_recalculate_everything($ticket_line_item);
636
-            return true;
637
-        }
638
-        return false;
639
-    }
640
-
641
-
642
-    /**
643
-     * calls EEH_Line_Item::find_transaction_grand_total_for_line_item()
644
-     * then EE_Line_Item::recalculate_total_including_taxes() on the result
645
-     *
646
-     * @param EE_Line_Item $line_item
647
-     * @return float
648
-     * @throws EE_Error
649
-     * @throws InvalidArgumentException
650
-     * @throws InvalidDataTypeException
651
-     * @throws InvalidInterfaceException
652
-     * @throws ReflectionException
653
-     */
654
-    public static function get_grand_total_and_recalculate_everything(EE_Line_Item $line_item): float
655
-    {
656
-        $grand_total_line_item = EEH_Line_Item::find_transaction_grand_total_for_line_item($line_item);
657
-        return $grand_total_line_item->recalculate_total_including_taxes();
658
-    }
659
-
660
-
661
-    /**
662
-     * Gets the line item which contains the subtotal of all the items
663
-     *
664
-     * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
665
-     * @return EE_Line_Item
666
-     * @throws EE_Error
667
-     * @throws InvalidArgumentException
668
-     * @throws InvalidDataTypeException
669
-     * @throws InvalidInterfaceException
670
-     * @throws ReflectionException
671
-     */
672
-    public static function get_pre_tax_subtotal(EE_Line_Item $total_line_item): EE_Line_Item
673
-    {
674
-        $pre_tax_subtotal = $total_line_item->get_child_line_item('pre-tax-subtotal');
675
-        return $pre_tax_subtotal instanceof EE_Line_Item
676
-            ? $pre_tax_subtotal
677
-            : self::create_pre_tax_subtotal($total_line_item);
678
-    }
679
-
680
-
681
-    /**
682
-     * Gets the line item for the taxes subtotal
683
-     *
684
-     * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
685
-     * @return EE_Line_Item
686
-     * @throws EE_Error
687
-     * @throws InvalidArgumentException
688
-     * @throws InvalidDataTypeException
689
-     * @throws InvalidInterfaceException
690
-     * @throws ReflectionException
691
-     */
692
-    public static function get_taxes_subtotal(EE_Line_Item $total_line_item): EE_Line_Item
693
-    {
694
-        $taxes = $total_line_item->get_child_line_item('taxes');
695
-        return $taxes ?: self::create_taxes_subtotal($total_line_item);
696
-    }
697
-
698
-
699
-    /**
700
-     * sets the TXN ID on an EE_Line_Item if passed a valid EE_Transaction object
701
-     *
702
-     * @param EE_Line_Item        $line_item
703
-     * @param EE_Transaction|null $transaction
704
-     * @return void
705
-     * @throws EE_Error
706
-     * @throws InvalidArgumentException
707
-     * @throws InvalidDataTypeException
708
-     * @throws InvalidInterfaceException
709
-     * @throws ReflectionException
710
-     */
711
-    public static function set_TXN_ID(EE_Line_Item $line_item, ?EE_Transaction $transaction = null)
712
-    {
713
-        if ($transaction) {
714
-            /** @type EEM_Transaction $EEM_Transaction */
715
-            $EEM_Transaction = EE_Registry::instance()->load_model('Transaction');
716
-            $TXN_ID          = $EEM_Transaction->ensure_is_ID($transaction);
717
-            $line_item->set_TXN_ID($TXN_ID);
718
-        }
719
-    }
720
-
721
-
722
-    /**
723
-     * Creates a new default total line item for the transaction,
724
-     * and its tickets subtotal and taxes subtotal line items (and adds the
725
-     * existing taxes as children of the taxes subtotal line item)
726
-     *
727
-     * @param EE_Transaction|null $transaction
728
-     * @return EE_Line_Item of type total
729
-     * @throws EE_Error
730
-     * @throws InvalidArgumentException
731
-     * @throws InvalidDataTypeException
732
-     * @throws InvalidInterfaceException
733
-     * @throws ReflectionException
734
-     */
735
-    public static function create_total_line_item(?EE_Transaction $transaction = null): EE_Line_Item
736
-    {
737
-        $total_line_item = EE_Line_Item::new_instance(
738
-            [
739
-                'LIN_code' => 'total',
740
-                'LIN_name' => esc_html__('Grand Total', 'event_espresso'),
741
-                'LIN_type' => EEM_Line_Item::type_total,
742
-                'OBJ_type' => EEM_Line_Item::OBJ_TYPE_TRANSACTION,
743
-            ]
744
-        );
745
-        $total_line_item = apply_filters(
746
-            'FHEE__EEH_Line_Item__create_total_line_item__total_line_item',
747
-            $total_line_item
748
-        );
749
-        self::set_TXN_ID($total_line_item, $transaction);
750
-        self::create_pre_tax_subtotal($total_line_item, $transaction);
751
-        self::create_taxes_subtotal($total_line_item, $transaction);
752
-        return $total_line_item;
753
-    }
754
-
755
-
756
-    /**
757
-     * Creates a default items subtotal line item
758
-     *
759
-     * @param EE_Line_Item        $total_line_item
760
-     * @param EE_Transaction|null $transaction
761
-     * @return EE_Line_Item
762
-     * @throws EE_Error
763
-     * @throws InvalidArgumentException
764
-     * @throws InvalidDataTypeException
765
-     * @throws InvalidInterfaceException
766
-     * @throws ReflectionException
767
-     */
768
-    protected static function create_pre_tax_subtotal(
769
-        EE_Line_Item $total_line_item,
770
-        ?EE_Transaction $transaction = null
771
-    ): EE_Line_Item {
772
-        $pre_tax_line_item = EE_Line_Item::new_instance(
773
-            [
774
-                'LIN_code' => 'pre-tax-subtotal',
775
-                'LIN_name' => esc_html__('Pre-Tax Subtotal', 'event_espresso'),
776
-                'LIN_type' => EEM_Line_Item::type_sub_total,
777
-            ]
778
-        );
779
-        $pre_tax_line_item = apply_filters(
780
-            'FHEE__EEH_Line_Item__create_pre_tax_subtotal__pre_tax_line_item',
781
-            $pre_tax_line_item
782
-        );
783
-        self::set_TXN_ID($pre_tax_line_item, $transaction);
784
-        $total_line_item->add_child_line_item($pre_tax_line_item);
785
-        self::create_event_subtotal($pre_tax_line_item, $transaction);
786
-        return $pre_tax_line_item;
787
-    }
788
-
789
-
790
-    /**
791
-     * Creates a line item for the taxes subtotal and finds all the tax prices
792
-     * and applies taxes to it
793
-     *
794
-     * @param EE_Line_Item        $total_line_item of type EEM_Line_Item::type_total
795
-     * @param EE_Transaction|null $transaction
796
-     * @return EE_Line_Item
797
-     * @throws EE_Error
798
-     * @throws InvalidArgumentException
799
-     * @throws InvalidDataTypeException
800
-     * @throws InvalidInterfaceException
801
-     * @throws ReflectionException
802
-     */
803
-    protected static function create_taxes_subtotal(
804
-        EE_Line_Item $total_line_item,
805
-        ?EE_Transaction $transaction = null
806
-    ): EE_Line_Item {
807
-        $tax_line_item = EE_Line_Item::new_instance(
808
-            [
809
-                'LIN_code'  => 'taxes',
810
-                'LIN_name'  => esc_html__('Taxes', 'event_espresso'),
811
-                'LIN_type'  => EEM_Line_Item::type_tax_sub_total,
812
-                'LIN_order' => 1000,// this should always come last
813
-            ]
814
-        );
815
-        $tax_line_item = apply_filters(
816
-            'FHEE__EEH_Line_Item__create_taxes_subtotal__tax_line_item',
817
-            $tax_line_item
818
-        );
819
-        self::set_TXN_ID($tax_line_item, $transaction);
820
-        $total_line_item->add_child_line_item($tax_line_item);
821
-        // and lastly, add the actual taxes
822
-        self::apply_taxes($total_line_item);
823
-        return $tax_line_item;
824
-    }
825
-
826
-
827
-    /**
828
-     * Creates a default items subtotal line item
829
-     *
830
-     * @param EE_Line_Item        $pre_tax_line_item
831
-     * @param EE_Transaction|null $transaction
832
-     * @param EE_Event|null       $event
833
-     * @return EE_Line_Item
834
-     * @throws EE_Error
835
-     * @throws InvalidArgumentException
836
-     * @throws InvalidDataTypeException
837
-     * @throws InvalidInterfaceException
838
-     * @throws ReflectionException
839
-     */
840
-    public static function create_event_subtotal(
841
-        EE_Line_Item $pre_tax_line_item,
842
-        ?EE_Transaction $transaction = null,
843
-        ?EE_Event $event = null
844
-    ): EE_Line_Item {
845
-        // first check if this line item already exists
846
-        $event_line_item = EEM_Line_Item::instance()->get_one(
847
-            [
848
-                [
849
-                    'LIN_type' => EEM_Line_Item::type_sub_total,
850
-                    'OBJ_type' => EEM_Line_Item::OBJ_TYPE_EVENT,
851
-                    'OBJ_ID'   => $event instanceof EE_Event ? $event->ID() : 0,
852
-                    'TXN_ID'   => $transaction instanceof EE_Transaction ? $transaction->ID() : 0,
853
-                ],
854
-            ]
855
-        );
856
-        if ($event_line_item instanceof EE_Line_Item) {
857
-            return $event_line_item;
858
-        }
859
-
860
-        $event_line_item = EE_Line_Item::new_instance(
861
-            [
862
-                'LIN_code' => self::get_event_code($event),
863
-                'LIN_name' => self::get_event_name($event),
864
-                'LIN_desc' => self::get_event_desc($event),
865
-                'LIN_type' => EEM_Line_Item::type_sub_total,
866
-                'OBJ_type' => EEM_Line_Item::OBJ_TYPE_EVENT,
867
-                'OBJ_ID'   => $event instanceof EE_Event ? $event->ID() : 0,
868
-            ]
869
-        );
870
-        $event_line_item = apply_filters(
871
-            'FHEE__EEH_Line_Item__create_event_subtotal__event_line_item',
872
-            $event_line_item
873
-        );
874
-        self::set_TXN_ID($event_line_item, $transaction);
875
-        $pre_tax_line_item->add_child_line_item($event_line_item);
876
-        return $event_line_item;
877
-    }
878
-
879
-
880
-    /**
881
-     * Gets what the event ticket's code SHOULD be
882
-     *
883
-     * @param EE_Event|null $event
884
-     * @return string
885
-     * @throws EE_Error
886
-     * @throws ReflectionException
887
-     */
888
-    public static function get_event_code(?EE_Event $event = null): string
889
-    {
890
-        return 'event-' . ($event instanceof EE_Event ? $event->ID() : '0');
891
-    }
892
-
893
-
894
-    /**
895
-     * Gets the event name
896
-     *
897
-     * @param EE_Event|null $event
898
-     * @return string
899
-     * @throws EE_Error
900
-     * @throws ReflectionException
901
-     */
902
-    public static function get_event_name(?EE_Event $event = null): string
903
-    {
904
-        return $event instanceof EE_Event
905
-            ? mb_substr($event->name(), 0, 245)
906
-            : esc_html__('Event', 'event_espresso');
907
-    }
908
-
909
-
910
-    /**
911
-     * Gets the event excerpt
912
-     *
913
-     * @param EE_Event|null $event
914
-     * @return string
915
-     * @throws EE_Error
916
-     * @throws ReflectionException
917
-     */
918
-    public static function get_event_desc(?EE_Event $event = null): string
919
-    {
920
-        return $event instanceof EE_Event ? $event->short_description() : '';
921
-    }
922
-
923
-
924
-    /**
925
-     * Given the grand total line item and a ticket, finds the event sub-total
926
-     * line item the ticket's purchase should be added onto
927
-     *
928
-     * @access public
929
-     * @param EE_Line_Item $grand_total the grand total line item
930
-     * @param EE_Ticket    $ticket
931
-     * @return EE_Line_Item
932
-     * @throws EE_Error
933
-     * @throws InvalidArgumentException
934
-     * @throws InvalidDataTypeException
935
-     * @throws InvalidInterfaceException
936
-     * @throws ReflectionException
937
-     */
938
-    public static function get_event_line_item_for_ticket(EE_Line_Item $grand_total, EE_Ticket $ticket): EE_Line_Item
939
-    {
940
-        $first_datetime = $ticket->first_datetime();
941
-        if (! $first_datetime instanceof EE_Datetime) {
942
-            throw new EE_Error(
943
-                sprintf(
944
-                    esc_html__('The supplied ticket (ID %d) has no datetimes', 'event_espresso'),
945
-                    $ticket->ID()
946
-                )
947
-            );
948
-        }
949
-        $event = $first_datetime->event();
950
-        if (! $event instanceof EE_Event) {
951
-            throw new EE_Error(
952
-                sprintf(
953
-                    esc_html__(
954
-                        'The supplied ticket (ID %d) has no event data associated with it.',
955
-                        'event_espresso'
956
-                    ),
957
-                    $ticket->ID()
958
-                )
959
-            );
960
-        }
961
-        $events_sub_total = EEH_Line_Item::get_event_line_item($grand_total, $event);
962
-        if (! $events_sub_total instanceof EE_Line_Item) {
963
-            throw new EE_Error(
964
-                sprintf(
965
-                    esc_html__(
966
-                        'There is no events sub-total for ticket %s on total line item %d',
967
-                        'event_espresso'
968
-                    ),
969
-                    $ticket->ID(),
970
-                    $grand_total->ID()
971
-                )
972
-            );
973
-        }
974
-        return $events_sub_total;
975
-    }
976
-
977
-
978
-    /**
979
-     * Gets the event line item
980
-     *
981
-     * @param EE_Line_Item  $total_line_item
982
-     * @param EE_Event|null $event
983
-     * @return EE_Line_Item for the event subtotal which is a child of $grand_total
984
-     * @throws EE_Error
985
-     * @throws InvalidArgumentException
986
-     * @throws InvalidDataTypeException
987
-     * @throws InvalidInterfaceException
988
-     * @throws ReflectionException
989
-     */
990
-    public static function get_event_line_item(EE_Line_Item $total_line_item, ?EE_Event $event = null): ?EE_Line_Item
991
-    {
992
-        /** @type EE_Event $event */
993
-        $event           = EEM_Event::instance()->ensure_is_obj($event, true);
994
-        $event_line_item = null;
995
-        $event_line_items = EEH_Line_Item::get_event_subtotals($total_line_item);
996
-        foreach ($event_line_items as $event_line_item) {
997
-            // default event subtotal, we should only ever find this the first time this method is called
998
-            $OBJ_ID = $event_line_item->OBJ_ID();
999
-            if ($OBJ_ID === $event->ID()) {
1000
-                // found existing line item for this event in the cart, so break out of loop and use this one
1001
-                break;
1002
-            }
1003
-            if (! $OBJ_ID) {
1004
-                // let's use this! but first... set the event details
1005
-                EEH_Line_Item::set_event_subtotal_details($event_line_item, $event);
1006
-                break;
1007
-            }
1008
-        }
1009
-        if (! $event_line_item instanceof EE_Line_Item) {
1010
-            // there is no event sub-total yet, so add it
1011
-            $pre_tax_subtotal = EEH_Line_Item::get_pre_tax_subtotal($total_line_item);
1012
-            // a new "event" subtotal SHOULD have been created
1013
-            $event_subtotals = EEH_Line_Item::get_event_subtotals($total_line_item);
1014
-            $event_line_item = reset($event_subtotals);
1015
-            // but in ccase one wasn't created for some reason...
1016
-            if (! $event_line_item instanceof EE_Line_Item) {
1017
-                // create a new "event" subtotal
1018
-                $txn = $total_line_item->transaction();
1019
-                $event_line_item = EEH_Line_Item::create_event_subtotal($pre_tax_subtotal, $txn, $event);
1020
-            }
1021
-            // and set the event details
1022
-            EEH_Line_Item::set_event_subtotal_details($event_line_item, $event);
1023
-        }
1024
-        return $event_line_item;
1025
-    }
1026
-
1027
-
1028
-    /**
1029
-     * Creates a default items subtotal line item
1030
-     *
1031
-     * @param EE_Line_Item        $event_line_item
1032
-     * @param EE_Event|null       $event
1033
-     * @param EE_Transaction|null $transaction
1034
-     * @return void
1035
-     * @throws EE_Error
1036
-     * @throws InvalidArgumentException
1037
-     * @throws InvalidDataTypeException
1038
-     * @throws InvalidInterfaceException
1039
-     * @throws ReflectionException
1040
-     */
1041
-    public static function set_event_subtotal_details(
1042
-        EE_Line_Item $event_line_item,
1043
-        EE_Event $event = null,
1044
-        ?EE_Transaction $transaction = null
1045
-    ) {
1046
-        if ($event instanceof EE_Event) {
1047
-            $event_line_item->set_code(self::get_event_code($event));
1048
-            $event_line_item->set_name(self::get_event_name($event));
1049
-            $event_line_item->set_desc(self::get_event_desc($event));
1050
-            $event_line_item->set_OBJ_ID($event->ID());
1051
-        }
1052
-        self::set_TXN_ID($event_line_item, $transaction);
1053
-    }
1054
-
1055
-
1056
-    /**
1057
-     * Finds what taxes should apply, adds them as tax line items under the taxes sub-total,
1058
-     * and recalculates the taxes sub-total and the grand total. Resets the taxes, so
1059
-     * any old taxes are removed
1060
-     *
1061
-     * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
1062
-     * @param bool         $update_txn_status
1063
-     * @return bool
1064
-     * @throws EE_Error
1065
-     * @throws InvalidArgumentException
1066
-     * @throws InvalidDataTypeException
1067
-     * @throws InvalidInterfaceException
1068
-     * @throws ReflectionException
1069
-     * @throws RuntimeException
1070
-     */
1071
-    public static function apply_taxes(EE_Line_Item $total_line_item, bool $update_txn_status = false): bool
1072
-    {
1073
-        $total_line_item       = EEH_Line_Item::find_transaction_grand_total_for_line_item($total_line_item);
1074
-        $taxes_line_item       = self::get_taxes_subtotal($total_line_item);
1075
-        $existing_global_taxes = $taxes_line_item->tax_descendants();
1076
-        $updates               = false;
1077
-        // loop thru taxes
1078
-        $global_taxes = EEH_Line_Item::getGlobalTaxes();
1079
-        foreach ($global_taxes as $order => $taxes) {
1080
-            foreach ($taxes as $tax) {
1081
-                if ($tax instanceof EE_Price) {
1082
-                    $found = false;
1083
-                    // check if this is already an existing tax
1084
-                    foreach ($existing_global_taxes as $existing_global_tax) {
1085
-                        if ($tax->ID() === $existing_global_tax->OBJ_ID()) {
1086
-                            // maybe update the tax rate in case it has changed
1087
-                            if ($existing_global_tax->percent() !== $tax->amount()) {
1088
-                                $existing_global_tax->set_percent($tax->amount());
1089
-                                $existing_global_tax->save();
1090
-                                $updates = true;
1091
-                            }
1092
-                            $found = true;
1093
-                            break;
1094
-                        }
1095
-                    }
1096
-                    if (! $found) {
1097
-                        // add a new line item for this global tax
1098
-                        $tax_line_item = apply_filters(
1099
-                            'FHEE__EEH_Line_Item__apply_taxes__tax_line_item',
1100
-                            EE_Line_Item::new_instance(
1101
-                                [
1102
-                                    'LIN_name'       => $tax->name(),
1103
-                                    'LIN_desc'       => $tax->desc(),
1104
-                                    'LIN_percent'    => $tax->amount(),
1105
-                                    'LIN_is_taxable' => false,
1106
-                                    'LIN_order'      => $order,
1107
-                                    'LIN_total'      => 0,
1108
-                                    'LIN_type'       => EEM_Line_Item::type_tax,
1109
-                                    'OBJ_type'       => EEM_Line_Item::OBJ_TYPE_PRICE,
1110
-                                    'OBJ_ID'         => $tax->ID(),
1111
-                                ]
1112
-                            )
1113
-                        );
1114
-                        $updates       = $taxes_line_item->add_child_line_item($tax_line_item) ? true : $updates;
1115
-                    }
1116
-                }
1117
-            }
1118
-        }
1119
-        // only recalculate totals if something changed
1120
-        if ($updates || $update_txn_status) {
1121
-            $total_line_item->recalculate_total_including_taxes($update_txn_status);
1122
-            return true;
1123
-        }
1124
-        return false;
1125
-    }
1126
-
1127
-
1128
-    /**
1129
-     * Ensures that taxes have been applied to the order, if not applies them.
1130
-     * Returns the total amount of tax
1131
-     *
1132
-     * @param EE_Line_Item|null $total_line_item of type EEM_Line_Item::type_total
1133
-     * @return float
1134
-     * @throws EE_Error
1135
-     * @throws InvalidArgumentException
1136
-     * @throws InvalidDataTypeException
1137
-     * @throws InvalidInterfaceException
1138
-     * @throws ReflectionException
1139
-     */
1140
-    public static function ensure_taxes_applied(?EE_Line_Item $total_line_item): float
1141
-    {
1142
-        $taxes_subtotal = self::get_taxes_subtotal($total_line_item);
1143
-        if (! $taxes_subtotal->children()) {
1144
-            self::apply_taxes($total_line_item);
1145
-        }
1146
-        return $taxes_subtotal->total();
1147
-    }
1148
-
1149
-
1150
-    /**
1151
-     * Deletes ALL children of the passed line item
1152
-     *
1153
-     * @param EE_Line_Item $parent_line_item
1154
-     * @return bool
1155
-     * @throws EE_Error
1156
-     * @throws InvalidArgumentException
1157
-     * @throws InvalidDataTypeException
1158
-     * @throws InvalidInterfaceException
1159
-     * @throws ReflectionException
1160
-     */
1161
-    public static function delete_all_child_items(EE_Line_Item $parent_line_item)
1162
-    {
1163
-        $deleted = 0;
1164
-        foreach ($parent_line_item->children() as $child_line_item) {
1165
-            if ($child_line_item instanceof EE_Line_Item) {
1166
-                $deleted += EEH_Line_Item::delete_all_child_items($child_line_item);
1167
-                if ($child_line_item->ID()) {
1168
-                    $child_line_item->delete();
1169
-                    unset($child_line_item);
1170
-                } else {
1171
-                    $parent_line_item->delete_child_line_item($child_line_item->code());
1172
-                }
1173
-                $deleted++;
1174
-            }
1175
-        }
1176
-        return $deleted;
1177
-    }
1178
-
1179
-
1180
-    /**
1181
-     * Deletes the line items as indicated by the line item code(s) provided,
1182
-     * regardless of where they're found in the line item tree. Automatically
1183
-     * re-calculates the line item totals and updates the related transaction. But
1184
-     * DOES NOT automatically upgrade the transaction's registrations' final prices (which
1185
-     * should probably change because of this).
1186
-     * You should call EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
1187
-     * after using this, to keep the registration final prices in-sync with the transaction's total.
1188
-     *
1189
-     * @param EE_Line_Item      $total_line_item of type EEM_Line_Item::type_total
1190
-     * @param array|bool|string $line_item_codes
1191
-     * @return int number of items successfully removed
1192
-     * @throws EE_Error
1193
-     * @throws ReflectionException
1194
-     */
1195
-    public static function delete_items(EE_Line_Item $total_line_item, $line_item_codes = false)
1196
-    {
1197
-        if ($total_line_item->type() !== EEM_Line_Item::type_total) {
1198
-            EE_Error::doing_it_wrong(
1199
-                'EEH_Line_Item::delete_items',
1200
-                esc_html__(
1201
-                    'This static method should only be called with a TOTAL line item, otherwise we won\'t recalculate the totals correctly',
1202
-                    'event_espresso'
1203
-                ),
1204
-                '4.6.18'
1205
-            );
1206
-        }
1207
-
1208
-        // check if only a single line_item_id was passed
1209
-        if (! empty($line_item_codes) && ! is_array($line_item_codes)) {
1210
-            // place single line_item_id in an array to appear as multiple line_item_ids
1211
-            $line_item_codes = [$line_item_codes];
1212
-        }
1213
-        $removals = 0;
1214
-        // cycle thru line_item_ids
1215
-        foreach ($line_item_codes as $line_item_id) {
1216
-            $removals += $total_line_item->delete_child_line_item($line_item_id);
1217
-        }
1218
-
1219
-        if ($removals > 0) {
1220
-            $total_line_item->recalculate_taxes_and_tax_total();
1221
-            return $removals;
1222
-        } else {
1223
-            return false;
1224
-        }
1225
-    }
1226
-
1227
-
1228
-    /**
1229
-     * Overwrites the previous tax by clearing out the old taxes, and creates a new
1230
-     * tax and updates the total line item accordingly
1231
-     *
1232
-     * @param EE_Line_Item $total_line_item
1233
-     * @param float        $amount
1234
-     * @param string       $name
1235
-     * @param string       $description
1236
-     * @param string       $code
1237
-     * @param boolean      $add_to_existing_line_item
1238
-     *                          if true, and a duplicate line item with the same code is found,
1239
-     *                          $amount will be added onto it; otherwise will simply set the taxes to match $amount
1240
-     * @return EE_Line_Item the new tax line item created
1241
-     * @throws EE_Error
1242
-     * @throws InvalidArgumentException
1243
-     * @throws InvalidDataTypeException
1244
-     * @throws InvalidInterfaceException
1245
-     * @throws ReflectionException
1246
-     */
1247
-    public static function set_total_tax_to(
1248
-        EE_Line_Item $total_line_item,
1249
-        float $amount,
1250
-        string $name = '',
1251
-        string $description = '',
1252
-        string $code = '',
1253
-        bool $add_to_existing_line_item = false
1254
-    ): EE_Line_Item {
1255
-        $tax_subtotal  = self::get_taxes_subtotal($total_line_item);
1256
-        $taxable_total = $total_line_item->taxable_total();
1257
-
1258
-        if ($add_to_existing_line_item) {
1259
-            $new_tax = $tax_subtotal->get_child_line_item($code);
1260
-            EEM_Line_Item::instance()->delete(
1261
-                [['LIN_code' => ['!=', $code], 'LIN_parent' => $tax_subtotal->ID()]]
1262
-            );
1263
-        } else {
1264
-            $new_tax = null;
1265
-            $tax_subtotal->delete_children_line_items();
1266
-        }
1267
-        if ($new_tax) {
1268
-            $new_tax->set_total($new_tax->total() + $amount);
1269
-            $new_tax->set_percent($taxable_total ? $new_tax->total() / $taxable_total * 100 : 0);
1270
-        } else {
1271
-            // no existing tax item. Create it
1272
-            $new_tax = EE_Line_Item::new_instance(
1273
-                [
1274
-                    'TXN_ID'      => $total_line_item->TXN_ID(),
1275
-                    'LIN_name'    => $name ?: esc_html__('Tax', 'event_espresso'),
1276
-                    'LIN_desc'    => $description ?: '',
1277
-                    'LIN_percent' => $taxable_total ? ($amount / $taxable_total * 100) : 0,
1278
-                    'LIN_total'   => $amount,
1279
-                    'LIN_parent'  => $tax_subtotal->ID(),
1280
-                    'LIN_type'    => EEM_Line_Item::type_tax,
1281
-                    'LIN_code'    => $code,
1282
-                ]
1283
-            );
1284
-        }
1285
-
1286
-        $new_tax = apply_filters(
1287
-            'FHEE__EEH_Line_Item__set_total_tax_to__new_tax_subtotal',
1288
-            $new_tax,
1289
-            $total_line_item
1290
-        );
1291
-        $new_tax->save();
1292
-        $tax_subtotal->set_total($new_tax->total());
1293
-        $tax_subtotal->save();
1294
-        $total_line_item->recalculate_total_including_taxes();
1295
-        return $new_tax;
1296
-    }
1297
-
1298
-
1299
-    /**
1300
-     * Makes all the line items which are children of $line_item taxable (or not).
1301
-     * Does NOT save the line items
1302
-     *
1303
-     * @param EE_Line_Item $line_item
1304
-     * @param boolean      $taxable
1305
-     * @param string|null  $code_substring_for_whitelist if this string is part of the line item's code
1306
-     *                                                   it will be whitelisted (ie, except from becoming taxable)
1307
-     * @throws EE_Error
1308
-     * @throws ReflectionException
1309
-     */
1310
-    public static function set_line_items_taxable(
1311
-        EE_Line_Item $line_item,
1312
-        bool $taxable = true,
1313
-        ?string $code_substring_for_whitelist = null
1314
-    ) {
1315
-        $whitelisted = false;
1316
-        if ($code_substring_for_whitelist !== null) {
1317
-            $whitelisted = strpos($line_item->code(), $code_substring_for_whitelist) !== false;
1318
-        }
1319
-        if (! $whitelisted && $line_item->is_line_item()) {
1320
-            $line_item->set_is_taxable($taxable);
1321
-        }
1322
-        foreach ($line_item->children() as $child_line_item) {
1323
-            EEH_Line_Item::set_line_items_taxable(
1324
-                $child_line_item,
1325
-                $taxable,
1326
-                $code_substring_for_whitelist
1327
-            );
1328
-        }
1329
-    }
1330
-
1331
-
1332
-    /**
1333
-     * Gets all descendants that are event subtotals
1334
-     *
1335
-     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1336
-     * @return EE_Line_Item[]
1337
-     * @throws EE_Error
1338
-     * @throws ReflectionException
1339
-     * @uses  EEH_Line_Item::get_subtotals_of_object_type()
1340
-     */
1341
-    public static function get_event_subtotals(EE_Line_Item $parent_line_item): array
1342
-    {
1343
-        return self::get_subtotals_of_object_type($parent_line_item, EEM_Line_Item::OBJ_TYPE_EVENT);
1344
-    }
1345
-
1346
-
1347
-    /**
1348
-     * Gets all descendants subtotals that match the supplied object type
1349
-     *
1350
-     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1351
-     * @param string       $obj_type
1352
-     * @return EE_Line_Item[]
1353
-     * @throws EE_Error
1354
-     * @throws ReflectionException
1355
-     * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
1356
-     */
1357
-    public static function get_subtotals_of_object_type(EE_Line_Item $parent_line_item, string $obj_type = ''): array
1358
-    {
1359
-        return self::_get_descendants_by_type_and_object_type(
1360
-            $parent_line_item,
1361
-            EEM_Line_Item::type_sub_total,
1362
-            $obj_type
1363
-        );
1364
-    }
1365
-
1366
-
1367
-    /**
1368
-     * Gets all descendants that are tickets
1369
-     *
1370
-     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1371
-     * @return EE_Line_Item[]
1372
-     * @throws EE_Error
1373
-     * @throws ReflectionException
1374
-     * @uses  EEH_Line_Item::get_line_items_of_object_type()
1375
-     */
1376
-    public static function get_ticket_line_items(EE_Line_Item $parent_line_item): array
1377
-    {
1378
-        return self::get_line_items_of_object_type(
1379
-            $parent_line_item,
1380
-            EEM_Line_Item::OBJ_TYPE_TICKET
1381
-        );
1382
-    }
1383
-
1384
-
1385
-    /**
1386
-     * Gets all descendants subtotals that match the supplied object type
1387
-     *
1388
-     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1389
-     * @param string       $obj_type
1390
-     * @return EE_Line_Item[]
1391
-     * @throws EE_Error
1392
-     * @throws ReflectionException
1393
-     * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
1394
-     */
1395
-    public static function get_line_items_of_object_type(EE_Line_Item $parent_line_item, string $obj_type = ''): array
1396
-    {
1397
-        return self::_get_descendants_by_type_and_object_type(
1398
-            $parent_line_item,
1399
-            EEM_Line_Item::type_line_item,
1400
-            $obj_type
1401
-        );
1402
-    }
1403
-
1404
-
1405
-    /**
1406
-     * Gets all the descendants (ie, children or children of children etc) that are of the type 'tax'
1407
-     *
1408
-     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1409
-     * @return EE_Line_Item[]
1410
-     * @throws EE_Error
1411
-     * @throws ReflectionException
1412
-     * @uses  EEH_Line_Item::get_descendants_of_type()
1413
-     */
1414
-    public static function get_tax_descendants(EE_Line_Item $parent_line_item): array
1415
-    {
1416
-        return EEH_Line_Item::get_descendants_of_type(
1417
-            $parent_line_item,
1418
-            EEM_Line_Item::type_tax
1419
-        );
1420
-    }
1421
-
1422
-
1423
-    /**
1424
-     * Gets all the real items purchased which are children of this item
1425
-     *
1426
-     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1427
-     * @return EE_Line_Item[]
1428
-     * @throws EE_Error
1429
-     * @throws ReflectionException
1430
-     * @uses  EEH_Line_Item::get_descendants_of_type()
1431
-     */
1432
-    public static function get_line_item_descendants(EE_Line_Item $parent_line_item): array
1433
-    {
1434
-        return EEH_Line_Item::get_descendants_of_type(
1435
-            $parent_line_item,
1436
-            EEM_Line_Item::type_line_item
1437
-        );
1438
-    }
1439
-
1440
-
1441
-    /**
1442
-     * Gets all descendants of supplied line item that match the supplied line item type
1443
-     *
1444
-     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1445
-     * @param string       $line_item_type   one of the EEM_Line_Item constants
1446
-     * @return EE_Line_Item[]
1447
-     * @throws EE_Error
1448
-     * @throws ReflectionException
1449
-     * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
1450
-     */
1451
-    public static function get_descendants_of_type(EE_Line_Item $parent_line_item, string $line_item_type): array
1452
-    {
1453
-        return self::_get_descendants_by_type_and_object_type(
1454
-            $parent_line_item,
1455
-            $line_item_type
1456
-        );
1457
-    }
1458
-
1459
-
1460
-    /**
1461
-     * Gets all descendants of supplied line item that match the supplied line item type and possibly the object type
1462
-     * as well
1463
-     *
1464
-     * @param EE_Line_Item $parent_line_item  - the line item to find descendants of
1465
-     * @param string       $line_item_type    one of the EEM_Line_Item constants
1466
-     * @param string|null  $obj_type          object model class name (minus prefix) or NULL to ignore object type when
1467
-     *                                        searching
1468
-     * @return EE_Line_Item[]
1469
-     * @throws EE_Error
1470
-     * @throws ReflectionException
1471
-     */
1472
-    protected static function _get_descendants_by_type_and_object_type(
1473
-        EE_Line_Item $parent_line_item,
1474
-        string $line_item_type,
1475
-        ?string $obj_type = null
1476
-    ): array {
1477
-        $objects = [];
1478
-        foreach ($parent_line_item->children() as $child_line_item) {
1479
-            if ($child_line_item instanceof EE_Line_Item) {
1480
-                if (
1481
-                    $child_line_item->type() === $line_item_type
1482
-                    && (
1483
-                        $child_line_item->OBJ_type() === $obj_type || $obj_type === null
1484
-                    )
1485
-                ) {
1486
-                    $objects[] = $child_line_item;
1487
-                } else {
1488
-                    // go-through-all-its children looking for more matches
1489
-                    $objects = array_merge(
1490
-                        $objects,
1491
-                        self::_get_descendants_by_type_and_object_type(
1492
-                            $child_line_item,
1493
-                            $line_item_type,
1494
-                            $obj_type
1495
-                        )
1496
-                    );
1497
-                }
1498
-            }
1499
-        }
1500
-        return $objects;
1501
-    }
1502
-
1503
-
1504
-    /**
1505
-     * Gets all descendants subtotals that match the supplied object type
1506
-     *
1507
-     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1508
-     * @param string       $OBJ_type         object type (like Event)
1509
-     * @param array        $OBJ_IDs          array of OBJ_IDs
1510
-     * @return EE_Line_Item[]
1511
-     * @throws EE_Error
1512
-     * @throws ReflectionException
1513
-     * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
1514
-     */
1515
-    public static function get_line_items_by_object_type_and_IDs(
1516
-        EE_Line_Item $parent_line_item,
1517
-        string $OBJ_type = '',
1518
-        array $OBJ_IDs = []
1519
-    ): array {
1520
-        return self::_get_descendants_by_object_type_and_object_ID(
1521
-            $parent_line_item,
1522
-            $OBJ_type,
1523
-            $OBJ_IDs
1524
-        );
1525
-    }
1526
-
1527
-
1528
-    /**
1529
-     * Gets all descendants of supplied line item that match the supplied line item type and possibly the object type
1530
-     * as well
1531
-     *
1532
-     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1533
-     * @param string       $OBJ_type         object type (like Event)
1534
-     * @param array        $OBJ_IDs          array of OBJ_IDs
1535
-     * @return EE_Line_Item[]
1536
-     * @throws EE_Error
1537
-     * @throws ReflectionException
1538
-     */
1539
-    protected static function _get_descendants_by_object_type_and_object_ID(
1540
-        EE_Line_Item $parent_line_item,
1541
-        string $OBJ_type,
1542
-        array $OBJ_IDs
1543
-    ): array {
1544
-        $objects = [];
1545
-        foreach ($parent_line_item->children() as $child_line_item) {
1546
-            if ($child_line_item instanceof EE_Line_Item) {
1547
-                if (
1548
-                    $child_line_item->OBJ_type() === $OBJ_type
1549
-                    && in_array($child_line_item->OBJ_ID(), $OBJ_IDs)
1550
-                ) {
1551
-                    $objects[] = $child_line_item;
1552
-                } else {
1553
-                    // go-through-all-its children looking for more matches
1554
-                    $objects = array_merge(
1555
-                        $objects,
1556
-                        self::_get_descendants_by_object_type_and_object_ID(
1557
-                            $child_line_item,
1558
-                            $OBJ_type,
1559
-                            $OBJ_IDs
1560
-                        )
1561
-                    );
1562
-                }
1563
-            }
1564
-        }
1565
-        return $objects;
1566
-    }
1567
-
1568
-
1569
-    /**
1570
-     * Uses a breadth-first-search in order to find the nearest descendant of
1571
-     * the specified type and returns it, else NULL
1572
-     *
1573
-     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1574
-     * @param string       $type             like one of the EEM_Line_Item::type_*
1575
-     * @return EE_Line_Item
1576
-     * @throws EE_Error
1577
-     * @throws InvalidArgumentException
1578
-     * @throws InvalidDataTypeException
1579
-     * @throws InvalidInterfaceException
1580
-     * @throws ReflectionException
1581
-     * @uses  EEH_Line_Item::_get_nearest_descendant()
1582
-     */
1583
-    public static function get_nearest_descendant_of_type(EE_Line_Item $parent_line_item, string $type): ?EE_Line_Item
1584
-    {
1585
-        return self::_get_nearest_descendant($parent_line_item, 'LIN_type', $type);
1586
-    }
1587
-
1588
-
1589
-    /**
1590
-     * Uses a breadth-first-search in order to find the nearest descendant
1591
-     * having the specified LIN_code and returns it, else NULL
1592
-     *
1593
-     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1594
-     * @param string       $code             any value used for LIN_code
1595
-     * @return EE_Line_Item
1596
-     * @throws EE_Error
1597
-     * @throws InvalidArgumentException
1598
-     * @throws InvalidDataTypeException
1599
-     * @throws InvalidInterfaceException
1600
-     * @throws ReflectionException
1601
-     * @uses  EEH_Line_Item::_get_nearest_descendant()
1602
-     */
1603
-    public static function get_nearest_descendant_having_code(
1604
-        EE_Line_Item $parent_line_item,
1605
-        string $code
1606
-    ): ?EE_Line_Item {
1607
-        return self::_get_nearest_descendant($parent_line_item, 'LIN_code', $code);
1608
-    }
1609
-
1610
-
1611
-    /**
1612
-     * Uses a breadth-first-search in order to find the nearest descendant
1613
-     * having the specified LIN_code and returns it, else NULL
1614
-     *
1615
-     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1616
-     * @param string       $search_field     name of EE_Line_Item property
1617
-     * @param mixed        $value            any value stored in $search_field
1618
-     * @return EE_Line_Item
1619
-     * @throws EE_Error
1620
-     * @throws InvalidArgumentException
1621
-     * @throws InvalidDataTypeException
1622
-     * @throws InvalidInterfaceException
1623
-     * @throws ReflectionException
1624
-     */
1625
-    protected static function _get_nearest_descendant(
1626
-        EE_Line_Item $parent_line_item,
1627
-        string $search_field,
1628
-        $value
1629
-    ): ?EE_Line_Item {
1630
-        foreach ($parent_line_item->children() as $child) {
1631
-            if ($child->get($search_field) == $value) {
1632
-                return $child;
1633
-            }
1634
-        }
1635
-        foreach ($parent_line_item->children() as $child) {
1636
-            $descendant_found = self::_get_nearest_descendant(
1637
-                $child,
1638
-                $search_field,
1639
-                $value
1640
-            );
1641
-            if ($descendant_found) {
1642
-                return $descendant_found;
1643
-            }
1644
-        }
1645
-        return null;
1646
-    }
1647
-
1648
-
1649
-    /**
1650
-     * if passed line item has a TXN ID, uses that to jump directly to the grand total line item for the transaction,
1651
-     * else recursively walks up the line item tree until a parent of type total is found,
1652
-     *
1653
-     * @param EE_Line_Item $line_item
1654
-     * @return EE_Line_Item
1655
-     * @throws EE_Error
1656
-     * @throws ReflectionException
1657
-     */
1658
-    public static function find_transaction_grand_total_for_line_item(EE_Line_Item $line_item): EE_Line_Item
1659
-    {
1660
-        if ($line_item->is_total()) {
1661
-            return $line_item;
1662
-        }
1663
-        if ($line_item->TXN_ID()) {
1664
-            $total_line_item = $line_item->transaction()->total_line_item(false);
1665
-            if ($total_line_item instanceof EE_Line_Item) {
1666
-                return $total_line_item;
1667
-            }
1668
-        } else {
1669
-            $line_item_parent = $line_item->parent();
1670
-            if ($line_item_parent instanceof EE_Line_Item) {
1671
-                if ($line_item_parent->is_total()) {
1672
-                    return $line_item_parent;
1673
-                }
1674
-                return EEH_Line_Item::find_transaction_grand_total_for_line_item($line_item_parent);
1675
-            }
1676
-        }
1677
-        throw new EE_Error(
1678
-            sprintf(
1679
-                esc_html__(
1680
-                    'A valid grand total for line item %1$d was not found.',
1681
-                    'event_espresso'
1682
-                ),
1683
-                $line_item->ID()
1684
-            )
1685
-        );
1686
-    }
1687
-
1688
-
1689
-    /**
1690
-     * Prints out a representation of the line item tree
1691
-     *
1692
-     * @param EE_Line_Item $line_item
1693
-     * @param int          $indentation
1694
-     * @param bool         $top_level
1695
-     * @return void
1696
-     * @throws EE_Error
1697
-     * @throws ReflectionException
1698
-     */
1699
-    public static function visualize(EE_Line_Item $line_item, int $indentation = 0, bool $top_level = true)
1700
-    {
1701
-        if (! defined('WP_DEBUG') || ! WP_DEBUG) {
1702
-            return;
1703
-        }
1704
-        $testing  = defined('EE_TESTS_DIR');
1705
-        $new_line = $testing ? "\n" : '<br />';
1706
-        if ($top_level && ! $testing) {
1707
-            echo '<div style="position: relative; z-index: 9999; margin: 2rem;">';
1708
-            echo '<pre style="padding: 2rem 3rem; color: #00CCFF; background: #363636;">';
1709
-        }
1710
-        if ($top_level || $indentation) {
1711
-            // echo $top_level ? "$new_line$new_line" : $new_line;
1712
-            echo $new_line;
1713
-        }
1714
-        echo str_repeat('. ', $indentation);
1715
-        $breakdown = '';
1716
-        if ($line_item->is_line_item() || $line_item->is_sub_line_item() || $line_item->isTax()) {
1717
-            if ($line_item->is_percent()) {
1718
-                $breakdown = "{$line_item->percent()}%";
1719
-            } else {
1720
-                $breakdown = "\${$line_item->unit_price()} x {$line_item->quantity()}";
1721
-            }
1722
-        }
1723
-        echo wp_kses($line_item->name(), AllowedTags::getAllowedTags());
1724
-        echo " [ ID:{$line_item->ID()} · qty:{$line_item->quantity()} ] {$line_item->type()}";
1725
-        echo " · \${$line_item->total()} · \${$line_item->pretaxTotal()}";
1726
-        if ($breakdown) {
1727
-            echo " ( $breakdown )";
1728
-        }
1729
-        if ($line_item->is_taxable()) {
1730
-            echo '  * taxable';
1731
-        }
1732
-        $children = $line_item->children();
1733
-        if ($children) {
1734
-            foreach ($children as $child) {
1735
-                self::visualize($child, $indentation + 1, false);
1736
-            }
1737
-        }
1738
-        if ($top_level) {
1739
-            echo $testing ? $new_line : "$new_line</pre></div>$new_line";
1740
-        }
1741
-    }
1742
-
1743
-
1744
-    /**
1745
-     * Calculates the registration's final price, taking into account that they
1746
-     * need to not only help pay for their OWN ticket, but also any transaction-wide surcharges and taxes,
1747
-     * and receive a portion of any transaction-wide discounts.
1748
-     * eg1, if I buy a $1 ticket and brent buys a $9 ticket, and we receive a $5 discount
1749
-     * then I'll get 1/10 of that $5 discount, which is $0.50, and brent will get
1750
-     * 9/10ths of that $5 discount, which is $4.50. So my final price should be $0.50
1751
-     * and brent's final price should be $5.50.
1752
-     * In order to do this, we basically need to traverse the line item tree calculating
1753
-     * the running totals (just as if we were recalculating the total), but when we identify
1754
-     * regular line items, we need to keep track of their share of the grand total.
1755
-     * Also, we need to keep track of the TAXABLE total for each ticket purchase, so
1756
-     * we can know how to apply taxes to it. (Note: "taxable total" does not equal the "pretax total"
1757
-     * when there are non-taxable items; otherwise they would be the same)
1758
-     *
1759
-     * @param EE_Line_Item $line_item
1760
-     * @param array        $billable_ticket_quantities          array of EE_Ticket IDs and their corresponding quantity
1761
-     *                                                          that can be included in price calculations at this
1762
-     *                                                          moment
1763
-     * @return array        keys are line items for tickets IDs and values are their share of the running total,
1764
-     *                                                          plus the key 'total', and 'taxable' which also has keys
1765
-     *                                                          of all the ticket IDs. Eg array(
1766
-     *                                                          12 => 4.3
1767
-     *                                                          23 => 8.0
1768
-     *                                                          'total' => 16.6,
1769
-     *                                                          'taxable' => array(
1770
-     *                                                          12 => 10,
1771
-     *                                                          23 => 4
1772
-     *                                                          ).
1773
-     *                                                          So to find which registrations have which final price,
1774
-     *                                                          we need to find which line item is theirs, which can be
1775
-     *                                                          done with
1776
-     *                                                          `EEM_Line_Item::instance()->get_line_item_for_registration(
1777
-     *                                                          $registration );`
1778
-     * @throws EE_Error
1779
-     * @throws InvalidArgumentException
1780
-     * @throws InvalidDataTypeException
1781
-     * @throws InvalidInterfaceException
1782
-     * @throws ReflectionException
1783
-     */
1784
-    public static function calculate_reg_final_prices_per_line_item(
1785
-        EE_Line_Item $line_item,
1786
-        array $billable_ticket_quantities = []
1787
-    ): array {
1788
-        $running_totals = [
1789
-            'total'   => 0,
1790
-            'taxable' => ['total' => 0],
1791
-        ];
1792
-        foreach ($line_item->children() as $child_line_item) {
1793
-            switch ($child_line_item->type()) {
1794
-                case EEM_Line_Item::type_sub_total:
1795
-                    $running_totals_from_subtotal = EEH_Line_Item::calculate_reg_final_prices_per_line_item(
1796
-                        $child_line_item,
1797
-                        $billable_ticket_quantities
1798
-                    );
1799
-                    // combine arrays but preserve numeric keys
1800
-                    $running_totals                     = array_replace_recursive(
1801
-                        $running_totals_from_subtotal,
1802
-                        $running_totals
1803
-                    );
1804
-                    $running_totals['total']            += $running_totals_from_subtotal['total'];
1805
-                    $running_totals['taxable']['total'] += $running_totals_from_subtotal['taxable']['total'];
1806
-                    break;
1807
-
1808
-                case EEM_Line_Item::type_tax_sub_total:
1809
-                    // find how much the taxes percentage is
1810
-                    if ($child_line_item->percent() !== 0.0) {
1811
-                        $tax_percent_decimal = $child_line_item->percent() / 100;
1812
-                    } else {
1813
-                        $tax_percent_decimal = EE_Taxes::get_total_taxes_percentage() / 100;
1814
-                    }
1815
-                    // and apply to all the taxable totals, and add to the pretax totals
1816
-                    foreach ($running_totals as $line_item_id => $this_running_total) {
1817
-                        // "total" and "taxable" array key is an exception
1818
-                        if ($line_item_id === 'taxable') {
1819
-                            continue;
1820
-                        }
1821
-                        $taxable_total                   = $running_totals['taxable'][ $line_item_id ];
1822
-                        $running_totals[ $line_item_id ] += ($taxable_total * $tax_percent_decimal);
1823
-                    }
1824
-                    break;
1825
-
1826
-                case EEM_Line_Item::type_line_item:
1827
-                    // ticket line items or ????
1828
-                    if ($child_line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET) {
1829
-                        // kk it's a ticket
1830
-                        if (isset($running_totals[ $child_line_item->ID() ])) {
1831
-                            // huh? that shouldn't happen.
1832
-                            $running_totals['total'] += $child_line_item->total();
1833
-                        } else {
1834
-                            // it's not in our running totals yet. great.
1835
-                            if ($child_line_item->is_taxable()) {
1836
-                                $taxable_amount = $child_line_item->unit_price();
1837
-                            } else {
1838
-                                $taxable_amount = 0;
1839
-                            }
1840
-                            // are we only calculating totals for some tickets?
1841
-                            if (isset($billable_ticket_quantities[ $child_line_item->OBJ_ID() ])) {
1842
-                                $quantity = $billable_ticket_quantities[ $child_line_item->OBJ_ID() ];
1843
-
1844
-                                $running_totals[ $child_line_item->ID() ]            = $quantity
1845
-                                    ? $child_line_item->unit_price()
1846
-                                    : 0;
1847
-                                $running_totals['taxable'][ $child_line_item->ID() ] = $quantity
1848
-                                    ? $taxable_amount
1849
-                                    : 0;
1850
-                            } else {
1851
-                                $quantity                                            = $child_line_item->quantity();
1852
-                                $running_totals[ $child_line_item->ID() ]            = $child_line_item->unit_price();
1853
-                                $running_totals['taxable'][ $child_line_item->ID() ] = $taxable_amount;
1854
-                            }
1855
-                            $running_totals['taxable']['total'] += $taxable_amount * $quantity;
1856
-                            $running_totals['total']            += $child_line_item->unit_price() * $quantity;
1857
-                        }
1858
-                    } else {
1859
-                        // it's some other type of item added to the cart
1860
-                        // it should affect the running totals
1861
-                        // basically we want to convert it into a PERCENT modifier. Because
1862
-                        // more clearly affect all registration's final price equally
1863
-                        $line_items_percent_of_running_total = $running_totals['total'] > 0
1864
-                            ? ($child_line_item->total() / $running_totals['total']) + 1
1865
-                            : 1;
1866
-                        foreach ($running_totals as $line_item_id => $this_running_total) {
1867
-                            // the "taxable" array key is an exception
1868
-                            if ($line_item_id === 'taxable') {
1869
-                                continue;
1870
-                            }
1871
-                            // update the running totals
1872
-                            // yes this actually even works for the running grand total!
1873
-                            $running_totals[ $line_item_id ] =
1874
-                                $line_items_percent_of_running_total * $this_running_total;
1875
-
1876
-                            if ($child_line_item->is_taxable()) {
1877
-                                $running_totals['taxable'][ $line_item_id ] =
1878
-                                    $line_items_percent_of_running_total * $running_totals['taxable'][ $line_item_id ];
1879
-                            }
1880
-                        }
1881
-                    }
1882
-                    break;
1883
-            }
1884
-        }
1885
-        return $running_totals;
1886
-    }
1887
-
1888
-
1889
-    /**
1890
-     * @param EE_Line_Item $total_line_item
1891
-     * @param EE_Line_Item $ticket_line_item
1892
-     * @return float | null
1893
-     * @throws EE_Error
1894
-     * @throws InvalidArgumentException
1895
-     * @throws InvalidDataTypeException
1896
-     * @throws InvalidInterfaceException
1897
-     * @throws OutOfRangeException
1898
-     * @throws ReflectionException
1899
-     */
1900
-    public static function calculate_final_price_for_ticket_line_item(
1901
-        EE_Line_Item $total_line_item,
1902
-        EE_Line_Item $ticket_line_item
1903
-    ): ?float {
1904
-        static $final_prices_per_ticket_line_item = [];
1905
-        if (
1906
-            empty($final_prices_per_ticket_line_item)
1907
-            || empty($final_prices_per_ticket_line_item[ $total_line_item->ID() ])
1908
-        ) {
1909
-            $final_prices_per_ticket_line_item[ $total_line_item->ID() ] =
1910
-                EEH_Line_Item::calculate_reg_final_prices_per_line_item(
1911
-                    $total_line_item
1912
-                );
1913
-        }
1914
-        // ok now find this new registration's final price
1915
-        if (isset($final_prices_per_ticket_line_item[ $total_line_item->ID() ][ $ticket_line_item->ID() ])) {
1916
-            return $final_prices_per_ticket_line_item[ $total_line_item->ID() ][ $ticket_line_item->ID() ];
1917
-        }
1918
-        $message = sprintf(
1919
-            esc_html__(
1920
-                'The final price for the ticket line item (ID:%1$d) on the total line item (ID:%2$d) could not be calculated.',
1921
-                'event_espresso'
1922
-            ),
1923
-            $ticket_line_item->ID(),
1924
-            $total_line_item->ID()
1925
-        );
1926
-        if (WP_DEBUG) {
1927
-            $message .= '<br>' . print_r($final_prices_per_ticket_line_item, true);
1928
-            throw new OutOfRangeException($message);
1929
-        }
1930
-        EE_Log::instance()->log(__CLASS__, __FUNCTION__, $message);
1931
-        return null;
1932
-    }
1933
-
1934
-
1935
-    /**
1936
-     * Creates a duplicate of the line item tree, except only includes billable items
1937
-     * and the portion of line items attributed to billable things
1938
-     *
1939
-     * @param EE_Line_Item      $line_item
1940
-     * @param EE_Registration[] $registrations
1941
-     * @return EE_Line_Item
1942
-     * @throws EE_Error
1943
-     * @throws InvalidArgumentException
1944
-     * @throws InvalidDataTypeException
1945
-     * @throws InvalidInterfaceException
1946
-     * @throws ReflectionException
1947
-     */
1948
-    public static function billable_line_item_tree(EE_Line_Item $line_item, array $registrations): EE_Line_Item
1949
-    {
1950
-        $copy_li = EEH_Line_Item::billable_line_item($line_item, $registrations);
1951
-        foreach ($line_item->children() as $child_li) {
1952
-            $copy_li->add_child_line_item(
1953
-                EEH_Line_Item::billable_line_item_tree($child_li, $registrations)
1954
-            );
1955
-        }
1956
-        // if this is the grand total line item, make sure the totals all add up
1957
-        // (we could have duplicated this logic AS we copied the line items, but
1958
-        // it seems DRYer this way)
1959
-        if ($copy_li->type() === EEM_Line_Item::type_total) {
1960
-            $copy_li->recalculate_total_including_taxes();
1961
-        }
1962
-        return $copy_li;
1963
-    }
1964
-
1965
-
1966
-    /**
1967
-     * Creates a new, unsaved line item from $line_item that factors in the
1968
-     * number of billable registrations on $registrations.
1969
-     *
1970
-     * @param EE_Line_Item      $line_item
1971
-     * @param EE_Registration[] $registrations
1972
-     * @return EE_Line_Item
1973
-     * @throws EE_Error
1974
-     * @throws InvalidArgumentException
1975
-     * @throws InvalidDataTypeException
1976
-     * @throws InvalidInterfaceException
1977
-     * @throws ReflectionException
1978
-     */
1979
-    public static function billable_line_item(EE_Line_Item $line_item, array $registrations): EE_Line_Item
1980
-    {
1981
-        $new_li_fields = $line_item->model_field_array();
1982
-        if (
1983
-            $line_item->type() === EEM_Line_Item::type_line_item &&
1984
-            $line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET
1985
-        ) {
1986
-            $count = 0;
1987
-            foreach ($registrations as $registration) {
1988
-                if (
1989
-                    $line_item->OBJ_ID() === $registration->ticket_ID() &&
1990
-                    in_array(
1991
-                        $registration->status_ID(),
1992
-                        EEM_Registration::reg_statuses_that_allow_payment(),
1993
-                        true
1994
-                    )
1995
-                ) {
1996
-                    $count++;
1997
-                }
1998
-            }
1999
-            $new_li_fields['LIN_quantity'] = $count;
2000
-        }
2001
-        // don't set the total. We'll leave that up to the code that calculates it
2002
-        unset($new_li_fields['LIN_ID'], $new_li_fields['LIN_parent'], $new_li_fields['LIN_total']);
2003
-        return EE_Line_Item::new_instance($new_li_fields);
2004
-    }
2005
-
2006
-
2007
-    /**
2008
-     * Returns a modified line item tree where all the subtotals which have a total of 0
2009
-     * are removed, and line items with a quantity of 0
2010
-     *
2011
-     * @param EE_Line_Item $line_item |null
2012
-     * @return EE_Line_Item|null
2013
-     * @throws EE_Error
2014
-     * @throws InvalidArgumentException
2015
-     * @throws InvalidDataTypeException
2016
-     * @throws InvalidInterfaceException
2017
-     * @throws ReflectionException
2018
-     */
2019
-    public static function non_empty_line_items(EE_Line_Item $line_item): ?EE_Line_Item
2020
-    {
2021
-        $copied_li = EEH_Line_Item::non_empty_line_item($line_item);
2022
-        if ($copied_li === null) {
2023
-            return null;
2024
-        }
2025
-        // if this is an event subtotal, we want to only include it if it
2026
-        // has a non-zero total and at least one ticket line item child
2027
-        $ticket_children = 0;
2028
-        foreach ($line_item->children() as $child_li) {
2029
-            $child_li_copy = EEH_Line_Item::non_empty_line_items($child_li);
2030
-            if ($child_li_copy !== null) {
2031
-                $copied_li->add_child_line_item($child_li_copy);
2032
-                if (
2033
-                    $child_li_copy->type() === EEM_Line_Item::type_line_item &&
2034
-                    $child_li_copy->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET
2035
-                ) {
2036
-                    $ticket_children++;
2037
-                }
2038
-            }
2039
-        }
2040
-        // if this is an event subtotal with NO ticket children
2041
-        // we basically want to ignore it
2042
-        if (
2043
-            $ticket_children === 0
2044
-            && $line_item->type() === EEM_Line_Item::type_sub_total
2045
-            && $line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_EVENT
2046
-            && $line_item->total() === 0.0
2047
-        ) {
2048
-            return null;
2049
-        }
2050
-        return $copied_li;
2051
-    }
2052
-
2053
-
2054
-    /**
2055
-     * Creates a new, unsaved line item, but if it's a ticket line item
2056
-     * with a total of 0, or a subtotal of 0, returns null instead
2057
-     *
2058
-     * @param EE_Line_Item $line_item
2059
-     * @return EE_Line_Item
2060
-     * @throws EE_Error
2061
-     * @throws InvalidArgumentException
2062
-     * @throws InvalidDataTypeException
2063
-     * @throws InvalidInterfaceException
2064
-     * @throws ReflectionException
2065
-     */
2066
-    public static function non_empty_line_item(EE_Line_Item $line_item): ?EE_Line_Item
2067
-    {
2068
-        if (
2069
-            $line_item->type() === EEM_Line_Item::type_line_item
2070
-            && $line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET
2071
-            && $line_item->quantity() === 0
2072
-        ) {
2073
-            return null;
2074
-        }
2075
-        $new_li_fields = $line_item->model_field_array();
2076
-        // don't set the total. We'll leave that up to the code that calculates it
2077
-        unset($new_li_fields['LIN_ID'], $new_li_fields['LIN_parent']);
2078
-        return EE_Line_Item::new_instance($new_li_fields);
2079
-    }
2080
-
2081
-
2082
-    /**
2083
-     * Cycles through all the ticket line items for the supplied total line item
2084
-     * and ensures that the line item's "is_taxable" field matches that of its corresponding ticket
2085
-     *
2086
-     * @param EE_Line_Item $total_line_item
2087
-     * @throws EE_Error
2088
-     * @throws InvalidArgumentException
2089
-     * @throws InvalidDataTypeException
2090
-     * @throws InvalidInterfaceException
2091
-     * @throws ReflectionException
2092
-     * @since 4.9.79.p
2093
-     */
2094
-    public static function resetIsTaxableForTickets(EE_Line_Item $total_line_item)
2095
-    {
2096
-        $ticket_line_items = self::get_ticket_line_items($total_line_item);
2097
-        foreach ($ticket_line_items as $ticket_line_item) {
2098
-            if (
2099
-                $ticket_line_item instanceof EE_Line_Item
2100
-                && $ticket_line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET
2101
-            ) {
2102
-                $ticket = $ticket_line_item->ticket();
2103
-                if ($ticket instanceof EE_Ticket && $ticket->taxable() !== $ticket_line_item->is_taxable()) {
2104
-                    $ticket_line_item->set_is_taxable($ticket->taxable());
2105
-                    $ticket_line_item->save();
2106
-                }
2107
-            }
2108
-        }
2109
-    }
2110
-
2111
-
2112
-    /**
2113
-     * @return EE_Line_Item[]
2114
-     * @throws EE_Error
2115
-     * @throws ReflectionException
2116
-     * @since   5.0.0.p
2117
-     */
2118
-    private static function getGlobalTaxes(): array
2119
-    {
2120
-        if (EEH_Line_Item::$global_taxes === null) {
2121
-            /** @type EEM_Price $EEM_Price */
2122
-            $EEM_Price = EE_Registry::instance()->load_model('Price');
2123
-            // get array of taxes via Price Model
2124
-            EEH_Line_Item::$global_taxes = $EEM_Price->get_all_prices_that_are_taxes();
2125
-            ksort(EEH_Line_Item::$global_taxes);
2126
-        }
2127
-        return EEH_Line_Item::$global_taxes;
2128
-    }
2129
-
2130
-
2131
-
2132
-    /**************************************** @DEPRECATED METHODS *************************************** */
2133
-    /**
2134
-     * @param EE_Line_Item $total_line_item
2135
-     * @return EE_Line_Item
2136
-     * @throws EE_Error
2137
-     * @throws InvalidArgumentException
2138
-     * @throws InvalidDataTypeException
2139
-     * @throws InvalidInterfaceException
2140
-     * @throws ReflectionException
2141
-     * @deprecated
2142
-     */
2143
-    public static function get_items_subtotal(EE_Line_Item $total_line_item): EE_Line_Item
2144
-    {
2145
-        EE_Error::doing_it_wrong(
2146
-            'EEH_Line_Item::get_items_subtotal()',
2147
-            sprintf(
2148
-                esc_html__('Method replaced with %1$s', 'event_espresso'),
2149
-                'EEH_Line_Item::get_pre_tax_subtotal()'
2150
-            ),
2151
-            '4.6.0'
2152
-        );
2153
-        return self::get_pre_tax_subtotal($total_line_item);
2154
-    }
2155
-
2156
-
2157
-    /**
2158
-     * @param EE_Transaction|null $transaction
2159
-     * @return EE_Line_Item
2160
-     * @throws EE_Error
2161
-     * @throws InvalidArgumentException
2162
-     * @throws InvalidDataTypeException
2163
-     * @throws InvalidInterfaceException
2164
-     * @throws ReflectionException
2165
-     * @deprecated
2166
-     */
2167
-    public static function create_default_total_line_item(?EE_Transaction $transaction = null): EE_Line_Item
2168
-    {
2169
-        EE_Error::doing_it_wrong(
2170
-            'EEH_Line_Item::create_default_total_line_item()',
2171
-            sprintf(
2172
-                esc_html__('Method replaced with %1$s', 'event_espresso'),
2173
-                'EEH_Line_Item::create_total_line_item()'
2174
-            ),
2175
-            '4.6.0'
2176
-        );
2177
-        return self::create_total_line_item($transaction);
2178
-    }
2179
-
2180
-
2181
-    /**
2182
-     * @param EE_Line_Item        $total_line_item
2183
-     * @param EE_Transaction|null $transaction
2184
-     * @return EE_Line_Item
2185
-     * @throws EE_Error
2186
-     * @throws InvalidArgumentException
2187
-     * @throws InvalidDataTypeException
2188
-     * @throws InvalidInterfaceException
2189
-     * @throws ReflectionException
2190
-     * @deprecated
2191
-     */
2192
-    public static function create_default_tickets_subtotal(
2193
-        EE_Line_Item $total_line_item,
2194
-        ?EE_Transaction $transaction = null
2195
-    ): EE_Line_Item {
2196
-        EE_Error::doing_it_wrong(
2197
-            'EEH_Line_Item::create_default_tickets_subtotal()',
2198
-            sprintf(
2199
-                esc_html__('Method replaced with %1$s', 'event_espresso'),
2200
-                'EEH_Line_Item::create_pre_tax_subtotal()'
2201
-            ),
2202
-            '4.6.0'
2203
-        );
2204
-        return self::create_pre_tax_subtotal($total_line_item, $transaction);
2205
-    }
2206
-
2207
-
2208
-    /**
2209
-     * @param EE_Line_Item        $total_line_item
2210
-     * @param EE_Transaction|null $transaction
2211
-     * @return EE_Line_Item
2212
-     * @throws EE_Error
2213
-     * @throws InvalidArgumentException
2214
-     * @throws InvalidDataTypeException
2215
-     * @throws InvalidInterfaceException
2216
-     * @throws ReflectionException
2217
-     * @deprecated
2218
-     */
2219
-    public static function create_default_taxes_subtotal(
2220
-        EE_Line_Item $total_line_item,
2221
-        ?EE_Transaction $transaction = null
2222
-    ): EE_Line_Item {
2223
-        EE_Error::doing_it_wrong(
2224
-            'EEH_Line_Item::create_default_taxes_subtotal()',
2225
-            sprintf(
2226
-                esc_html__('Method replaced with %1$s', 'event_espresso'),
2227
-                'EEH_Line_Item::create_taxes_subtotal()'
2228
-            ),
2229
-            '4.6.0'
2230
-        );
2231
-        return self::create_taxes_subtotal($total_line_item, $transaction);
2232
-    }
2233
-
2234
-
2235
-    /**
2236
-     * @param EE_Line_Item        $total_line_item
2237
-     * @param EE_Transaction|null $transaction
2238
-     * @return EE_Line_Item
2239
-     * @throws EE_Error
2240
-     * @throws InvalidArgumentException
2241
-     * @throws InvalidDataTypeException
2242
-     * @throws InvalidInterfaceException
2243
-     * @throws ReflectionException
2244
-     * @deprecated
2245
-     */
2246
-    public static function create_default_event_subtotal(
2247
-        EE_Line_Item $total_line_item,
2248
-        ?EE_Transaction $transaction = null
2249
-    ): EE_Line_Item {
2250
-        EE_Error::doing_it_wrong(
2251
-            'EEH_Line_Item::create_default_event_subtotal()',
2252
-            sprintf(
2253
-                esc_html__('Method replaced with %1$s', 'event_espresso'),
2254
-                'EEH_Line_Item::create_event_subtotal()'
2255
-            ),
2256
-            '4.6.0'
2257
-        );
2258
-        return self::create_event_subtotal($total_line_item, $transaction);
2259
-    }
24
+	/**
25
+	 * @var EE_Line_Item[]|null
26
+	 */
27
+	private static ?array $global_taxes = null;
28
+
29
+
30
+	/**
31
+	 * Adds a simple item (unrelated to any other model object) to the provided PARENT line item.
32
+	 * Does NOT automatically re-calculate the line item totals or update the related transaction.
33
+	 * You should call recalculate_total_including_taxes() on the grant total line item after this
34
+	 * to update the subtotals, and EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
35
+	 * to keep the registration final prices in-sync with the transaction's total.
36
+	 *
37
+	 * @param EE_Line_Item $parent_line_item
38
+	 * @param string       $name
39
+	 * @param float        $unit_price
40
+	 * @param string       $description
41
+	 * @param int          $quantity
42
+	 * @param boolean      $taxable
43
+	 * @param string|null  $code if set to a value, ensures there is only one line item with that code
44
+	 * @param bool         $return_item
45
+	 * @param bool         $recalculate_totals
46
+	 * @return boolean|EE_Line_Item success
47
+	 * @throws EE_Error
48
+	 * @throws ReflectionException
49
+	 */
50
+	public static function add_unrelated_item(
51
+		EE_Line_Item $parent_line_item,
52
+		string $name,
53
+		float $unit_price,
54
+		string $description = '',
55
+		int $quantity = 1,
56
+		bool $taxable = false,
57
+		?string $code = null,
58
+		bool $return_item = false,
59
+		bool $recalculate_totals = true
60
+	) {
61
+		$items_subtotal = self::get_pre_tax_subtotal($parent_line_item);
62
+		$line_item      = EE_Line_Item::new_instance(
63
+			[
64
+				'LIN_name'       => $name,
65
+				'LIN_desc'       => $description,
66
+				'LIN_unit_price' => $unit_price,
67
+				'LIN_quantity'   => $quantity,
68
+				'LIN_percent'    => null,
69
+				'LIN_is_taxable' => $taxable,
70
+				'LIN_order'      => count($items_subtotal->children()),
71
+				'LIN_total'      => $unit_price * $quantity,
72
+				'LIN_type'       => EEM_Line_Item::type_line_item,
73
+				'LIN_code'       => $code,
74
+			]
75
+		);
76
+		$line_item      = apply_filters(
77
+			'FHEE__EEH_Line_Item__add_unrelated_item__line_item',
78
+			$line_item,
79
+			$parent_line_item
80
+		);
81
+		$added          = self::add_item($parent_line_item, $line_item, $recalculate_totals);
82
+		return $return_item ? $line_item : $added;
83
+	}
84
+
85
+
86
+	/**
87
+	 * Adds a simple item ( unrelated to any other model object) to the total line item,
88
+	 * in the correct spot in the line item tree. Does not automatically
89
+	 * re-calculate the line item totals, nor update the related transaction, nor upgrade the transaction's
90
+	 * registrations' final prices (which should probably change because of this).
91
+	 * You should call recalculate_total_including_taxes() on the grand total line item, then
92
+	 * update the transaction's total, and EE_Registration_Processor::update_registration_final_prices()
93
+	 * after using this, to keep the registration final prices in-sync with the transaction's total.
94
+	 *
95
+	 * @param EE_Line_Item $parent_line_item
96
+	 * @param string       $name
97
+	 * @param float        $percentage_amount
98
+	 * @param string       $description
99
+	 * @param boolean      $taxable
100
+	 * @param string|null  $code
101
+	 * @param bool         $return_item
102
+	 * @return boolean|EE_Line_Item success
103
+	 * @throws EE_Error
104
+	 * @throws ReflectionException
105
+	 */
106
+	public static function add_percentage_based_item(
107
+		EE_Line_Item $parent_line_item,
108
+		string $name,
109
+		float $percentage_amount,
110
+		string $description = '',
111
+		bool $taxable = false,
112
+		?string $code = null,
113
+		bool $return_item = false
114
+	) {
115
+		$total     = $percentage_amount * $parent_line_item->total() / 100;
116
+		$line_item = EE_Line_Item::new_instance(
117
+			[
118
+				'LIN_name'       => $name,
119
+				'LIN_desc'       => $description,
120
+				'LIN_unit_price' => 0,
121
+				'LIN_percent'    => $percentage_amount,
122
+				'LIN_is_taxable' => $taxable,
123
+				'LIN_total'      => $total,
124
+				'LIN_type'       => EEM_Line_Item::type_line_item,
125
+				'LIN_parent'     => $parent_line_item->ID(),
126
+				'LIN_code'       => $code,
127
+				'TXN_ID'         => $parent_line_item->TXN_ID(),
128
+			]
129
+		);
130
+		$line_item = apply_filters(
131
+			'FHEE__EEH_Line_Item__add_percentage_based_item__line_item',
132
+			$line_item
133
+		);
134
+		$added     = $parent_line_item->add_child_line_item($line_item, false);
135
+		return $return_item ? $line_item : $added;
136
+	}
137
+
138
+
139
+	/**
140
+	 * Returns the new line item created by adding a purchase of the ticket
141
+	 * ensures that ticket line item is saved, and that cart total has been recalculated.
142
+	 * If this ticket has already been purchased, just increments its count.
143
+	 * Automatically re-calculates the line item totals and updates the related transaction. But
144
+	 * DOES NOT automatically upgrade the transaction's registrations' final prices (which
145
+	 * should probably change because of this).
146
+	 * You should call EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
147
+	 * after using this, to keep the registration final prices in-sync with the transaction's total.
148
+	 *
149
+	 * @param EE_Line_Item|null $total_line_item grand total line item of type EEM_Line_Item::type_total
150
+	 * @param EE_Ticket         $ticket
151
+	 * @param int               $qty
152
+	 * @param bool              $recalculate_totals
153
+	 * @return EE_Line_Item
154
+	 * @throws EE_Error
155
+	 * @throws ReflectionException
156
+	 */
157
+	public static function add_ticket_purchase(
158
+		?EE_Line_Item $total_line_item,
159
+		EE_Ticket $ticket,
160
+		int $qty = 1,
161
+		bool $recalculate_totals = true
162
+	): ?EE_Line_Item {
163
+		if (! $total_line_item instanceof EE_Line_Item || ! $total_line_item->is_total()) {
164
+			throw new EE_Error(
165
+				sprintf(
166
+					esc_html__(
167
+						'A valid line item total is required in order to add tickets. A line item of type "%s" was passed.',
168
+						'event_espresso'
169
+					),
170
+					$ticket->ID(),
171
+					$total_line_item->ID()
172
+				)
173
+			);
174
+		}
175
+		// either increment the qty for an existing ticket
176
+		$line_item = self::increment_ticket_qty_if_already_in_cart($total_line_item, $ticket, $qty);
177
+		// or add a new one
178
+		if (! $line_item instanceof EE_Line_Item) {
179
+			$line_item = self::create_ticket_line_item($total_line_item, $ticket, $qty);
180
+		}
181
+		if ($recalculate_totals) {
182
+			$total_line_item->recalculate_total_including_taxes();
183
+		}
184
+		return $line_item;
185
+	}
186
+
187
+
188
+	/**
189
+	 * Returns the new line item created by adding a purchase of the ticket
190
+	 *
191
+	 * @param EE_Line_Item|null $total_line_item
192
+	 * @param EE_Ticket         $ticket
193
+	 * @param int               $qty
194
+	 * @return EE_Line_Item
195
+	 * @throws EE_Error
196
+	 * @throws InvalidArgumentException
197
+	 * @throws InvalidDataTypeException
198
+	 * @throws InvalidInterfaceException
199
+	 * @throws ReflectionException
200
+	 */
201
+	public static function increment_ticket_qty_if_already_in_cart(
202
+		?EE_Line_Item $total_line_item,
203
+		EE_Ticket $ticket,
204
+		int $qty = 1
205
+	): ?EE_Line_Item {
206
+		$line_item = null;
207
+		if ($total_line_item instanceof EE_Line_Item && $total_line_item->is_total()) {
208
+			$ticket_line_items = EEH_Line_Item::get_ticket_line_items($total_line_item);
209
+			foreach ($ticket_line_items as $ticket_line_item) {
210
+				if (
211
+					$ticket_line_item instanceof EE_Line_Item
212
+					&& $ticket_line_item->OBJ_ID() === $ticket->ID()
213
+				) {
214
+					$line_item = $ticket_line_item;
215
+					break;
216
+				}
217
+			}
218
+		}
219
+		if ($line_item instanceof EE_Line_Item) {
220
+			EEH_Line_Item::increment_quantity($line_item, $qty);
221
+			return $line_item;
222
+		}
223
+		return null;
224
+	}
225
+
226
+
227
+	/**
228
+	 * Increments the line item and all its children's quantity by $qty (but percent line items are unaffected).
229
+	 * Does NOT save or recalculate other line items totals
230
+	 *
231
+	 * @param EE_Line_Item $line_item
232
+	 * @param int          $qty
233
+	 * @return void
234
+	 * @throws EE_Error
235
+	 * @throws InvalidArgumentException
236
+	 * @throws InvalidDataTypeException
237
+	 * @throws InvalidInterfaceException
238
+	 * @throws ReflectionException
239
+	 */
240
+	public static function increment_quantity(EE_Line_Item $line_item, int $qty = 1)
241
+	{
242
+		if (! $line_item->is_percent()) {
243
+			$qty += $line_item->quantity();
244
+			$line_item->set_quantity($qty);
245
+			$line_item->set_total($line_item->unit_price() * $qty);
246
+			$line_item->save();
247
+		}
248
+		foreach ($line_item->children() as $child) {
249
+			if ($child->is_sub_line_item()) {
250
+				EEH_Line_Item::update_quantity($child, $qty);
251
+			}
252
+		}
253
+	}
254
+
255
+
256
+	/**
257
+	 * Decrements the line item and all its children's quantity by $qty (but percent line items are unaffected).
258
+	 * Does NOT save or recalculate other line items totals
259
+	 *
260
+	 * @param EE_Line_Item $line_item
261
+	 * @param int          $qty
262
+	 * @return void
263
+	 * @throws EE_Error
264
+	 * @throws InvalidArgumentException
265
+	 * @throws InvalidDataTypeException
266
+	 * @throws InvalidInterfaceException
267
+	 * @throws ReflectionException
268
+	 */
269
+	public static function decrement_quantity(EE_Line_Item $line_item, int $qty = 1)
270
+	{
271
+		if (! $line_item->is_percent()) {
272
+			$qty = $line_item->quantity() - $qty;
273
+			$qty = max($qty, 0);
274
+			$line_item->set_quantity($qty);
275
+			$line_item->set_total($line_item->unit_price() * $qty);
276
+			$line_item->save();
277
+		}
278
+		foreach ($line_item->children() as $child) {
279
+			if ($child->is_sub_line_item()) {
280
+				EEH_Line_Item::update_quantity($child, $qty);
281
+			}
282
+		}
283
+	}
284
+
285
+
286
+	/**
287
+	 * Updates the line item and its children's quantities to the specified number.
288
+	 * Does NOT save them or recalculate totals.
289
+	 *
290
+	 * @param EE_Line_Item $line_item
291
+	 * @param int          $new_quantity
292
+	 * @throws EE_Error
293
+	 * @throws InvalidArgumentException
294
+	 * @throws InvalidDataTypeException
295
+	 * @throws InvalidInterfaceException
296
+	 * @throws ReflectionException
297
+	 */
298
+	public static function update_quantity(EE_Line_Item $line_item, int $new_quantity)
299
+	{
300
+		if (! $line_item->is_percent()) {
301
+			$line_item->set_quantity($new_quantity);
302
+			$line_item->set_total($line_item->unit_price() * $new_quantity);
303
+			$line_item->save();
304
+		}
305
+		foreach ($line_item->children() as $child) {
306
+			if ($child->is_sub_line_item()) {
307
+				EEH_Line_Item::update_quantity($child, $new_quantity);
308
+			}
309
+		}
310
+	}
311
+
312
+
313
+	/**
314
+	 * Returns the new line item created by adding a purchase of the ticket
315
+	 *
316
+	 * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
317
+	 * @param EE_Ticket    $ticket
318
+	 * @param int          $qty
319
+	 * @return EE_Line_Item
320
+	 * @throws EE_Error
321
+	 * @throws InvalidArgumentException
322
+	 * @throws InvalidDataTypeException
323
+	 * @throws InvalidInterfaceException
324
+	 * @throws ReflectionException
325
+	 */
326
+	public static function create_ticket_line_item(
327
+		EE_Line_Item $total_line_item,
328
+		EE_Ticket $ticket,
329
+		int $qty = 1
330
+	): EE_Line_Item {
331
+		$datetimes           = $ticket->datetimes();
332
+		$first_datetime      = reset($datetimes);
333
+		$first_datetime_name = esc_html__('Event', 'event_espresso');
334
+		if ($first_datetime instanceof EE_Datetime && $first_datetime->event() instanceof EE_Event) {
335
+			$first_datetime_name = $first_datetime->event()->name();
336
+		}
337
+		$event = sprintf(_x('(For %1$s)', '(For Event Name)', 'event_espresso'), $first_datetime_name);
338
+		// get event subtotal line
339
+		$events_sub_total = self::get_event_line_item_for_ticket($total_line_item, $ticket);
340
+		$taxes            = $ticket->tax_price_modifiers();
341
+		// add $ticket to cart
342
+		$line_item = EE_Line_Item::new_instance(
343
+			[
344
+				'LIN_name'       => $ticket->name(),
345
+				'LIN_desc'       => $ticket->description() !== '' ? $ticket->description() . ' ' . $event : $event,
346
+				'LIN_unit_price' => $ticket->price(),
347
+				'LIN_quantity'   => $qty,
348
+				'LIN_is_taxable' => empty($taxes) && $ticket->taxable(),
349
+				'LIN_order'      => count($events_sub_total->children()),
350
+				'LIN_total'      => $ticket->price() * $qty,
351
+				'LIN_type'       => EEM_Line_Item::type_line_item,
352
+				'OBJ_ID'         => $ticket->ID(),
353
+				'OBJ_type'       => EEM_Line_Item::OBJ_TYPE_TICKET,
354
+			]
355
+		);
356
+		$line_item = apply_filters(
357
+			'FHEE__EEH_Line_Item__create_ticket_line_item__line_item',
358
+			$line_item
359
+		);
360
+		if (! $line_item instanceof EE_Line_Item) {
361
+			throw new DomainException(
362
+				esc_html__('Invalid EE_Line_Item received.', 'event_espresso')
363
+			);
364
+		}
365
+		$events_sub_total->add_child_line_item($line_item);
366
+		// now add the sub-line items
367
+		$running_pre_tax_total = 0;
368
+		$prices                = $ticket->prices();
369
+		if (empty($prices)) {
370
+			// WUT?!?! NO PRICES??? Well, just create a default price then.
371
+			$default_price = EEM_Price::instance()->get_new_price();
372
+			if ($default_price->amount() !== $ticket->price()) {
373
+				$default_price->set_amount($ticket->price());
374
+			}
375
+			$default_price->save();
376
+			$ticket->_add_relation_to($default_price, 'Price');
377
+			$ticket->save();
378
+			$prices = [$default_price];
379
+		}
380
+		foreach ($prices as $price) {
381
+			$sign = $price->is_discount() ? -1 : 1;
382
+			$price_amount = $price->amount();
383
+			if ($price->is_percent()) {
384
+				$price_total = $running_pre_tax_total * $price_amount / 100;
385
+				$percent    = $sign * $price_amount;
386
+				$unit_price = 0;
387
+			} else {
388
+				$price_total = $price_amount * $qty;
389
+				$percent    = 0;
390
+				$unit_price = $sign * $price_amount;
391
+			}
392
+
393
+			$price_desc = $price->desc();
394
+			$price_type = $price->type_obj();
395
+			$price_desc = $price_desc === '' && $price_type instanceof EE_Price_Type
396
+				? $price_type->name()
397
+				: $price_desc;
398
+
399
+			$sub_line_item         = EE_Line_Item::new_instance(
400
+				[
401
+					'LIN_name'       => $price->name(),
402
+					'LIN_desc'       => $price_desc,
403
+					'LIN_is_taxable' => false,
404
+					'LIN_order'      => $price->order(),
405
+					'LIN_total'      => $price_total,
406
+					'LIN_pretax'     => 0,
407
+					'LIN_unit_price' => $unit_price,
408
+					'LIN_percent'    => $percent,
409
+					'LIN_type'       => $price->is_tax()
410
+						? EEM_Line_Item::type_sub_tax
411
+						: EEM_Line_Item::type_sub_line_item,
412
+					'OBJ_ID'         => $price->ID(),
413
+					'OBJ_type'       => EEM_Line_Item::OBJ_TYPE_PRICE,
414
+				]
415
+			);
416
+			$sub_line_item         = apply_filters(
417
+				'FHEE__EEH_Line_Item__create_ticket_line_item__sub_line_item',
418
+				$sub_line_item
419
+			);
420
+			$running_pre_tax_total += ! $price->is_tax() ? $sign * $price_total : 0;
421
+			$line_item->add_child_line_item($sub_line_item);
422
+		}
423
+		$line_item->setPretaxTotal($running_pre_tax_total);
424
+		return $line_item;
425
+	}
426
+
427
+
428
+	/**
429
+	 * Adds the specified item under the pre-tax-sub-total line item. Automatically
430
+	 * re-calculates the line item totals and updates the related transaction. But
431
+	 * DOES NOT automatically upgrade the transaction's registrations' final prices (which
432
+	 * should probably change because of this).
433
+	 * You should call EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
434
+	 * after using this, to keep the registration final prices in-sync with the transaction's total.
435
+	 *
436
+	 * @param EE_Line_Item $total_line_item
437
+	 * @param EE_Line_Item $item to be added
438
+	 * @param bool         $recalculate_totals
439
+	 * @return boolean
440
+	 * @throws EE_Error
441
+	 * @throws InvalidArgumentException
442
+	 * @throws InvalidDataTypeException
443
+	 * @throws InvalidInterfaceException
444
+	 * @throws ReflectionException
445
+	 */
446
+	public static function add_item(
447
+		EE_Line_Item $total_line_item,
448
+		EE_Line_Item $item,
449
+		bool $recalculate_totals = true
450
+	): bool {
451
+		$pre_tax_subtotal = self::get_pre_tax_subtotal($total_line_item);
452
+		$success          = $pre_tax_subtotal->add_child_line_item($item);
453
+		if ($recalculate_totals) {
454
+			$total_line_item->recalculate_total_including_taxes();
455
+		}
456
+		return $success;
457
+	}
458
+
459
+
460
+	/**
461
+	 * cancels an existing ticket line item,
462
+	 * by decrementing its quantity by 1 and adding a new "type_cancellation" sub-line-item.
463
+	 * ALL totals and subtotals will NEED TO BE UPDATED after performing this action
464
+	 *
465
+	 * @param EE_Line_Item $ticket_line_item
466
+	 * @param int          $qty
467
+	 * @return bool success
468
+	 * @throws EE_Error
469
+	 * @throws InvalidArgumentException
470
+	 * @throws InvalidDataTypeException
471
+	 * @throws InvalidInterfaceException
472
+	 * @throws ReflectionException
473
+	 */
474
+	public static function cancel_ticket_line_item(EE_Line_Item $ticket_line_item, int $qty = 1): bool
475
+	{
476
+		// validate incoming line_item
477
+		if ($ticket_line_item->OBJ_type() !== EEM_Line_Item::OBJ_TYPE_TICKET) {
478
+			throw new EE_Error(
479
+				sprintf(
480
+					esc_html__(
481
+						'The supplied line item must have an Object Type of "Ticket", not %1$s.',
482
+						'event_espresso'
483
+					),
484
+					$ticket_line_item->type()
485
+				)
486
+			);
487
+		}
488
+		if ($ticket_line_item->quantity() < $qty) {
489
+			throw new EE_Error(
490
+				sprintf(
491
+					esc_html__(
492
+						'Can not cancel %1$d ticket(s) because the supplied line item has a quantity of %2$d.',
493
+						'event_espresso'
494
+					),
495
+					$qty,
496
+					$ticket_line_item->quantity()
497
+				)
498
+			);
499
+		}
500
+		// decrement ticket quantity; don't rely on auto-fixing when recalculating totals to do this
501
+		$ticket_line_item->set_quantity($ticket_line_item->quantity() - $qty);
502
+		foreach ($ticket_line_item->children() as $child_line_item) {
503
+			if (
504
+				$child_line_item->is_sub_line_item()
505
+				&& ! $child_line_item->is_percent()
506
+				&& ! $child_line_item->is_cancellation()
507
+			) {
508
+				$child_line_item->set_quantity($child_line_item->quantity() - $qty);
509
+			}
510
+		}
511
+		// get cancellation sub line item
512
+		$cancellation_line_item = EEH_Line_Item::get_descendants_of_type(
513
+			$ticket_line_item,
514
+			EEM_Line_Item::type_cancellation
515
+		);
516
+		$cancellation_line_item = reset($cancellation_line_item);
517
+		// verify that this ticket was indeed previously cancelled
518
+		if ($cancellation_line_item instanceof EE_Line_Item) {
519
+			// increment cancelled quantity
520
+			$cancellation_line_item->set_quantity($cancellation_line_item->quantity() + $qty);
521
+		} else {
522
+			// create cancellation sub line item
523
+			$cancellation_line_item = EE_Line_Item::new_instance(
524
+				[
525
+					'LIN_name'       => esc_html__('Cancellation', 'event_espresso'),
526
+					'LIN_desc'       => sprintf(
527
+						esc_html_x(
528
+							'Cancelled %1$s : %2$s',
529
+							'Cancelled Ticket Name : 2015-01-01 11:11',
530
+							'event_espresso'
531
+						),
532
+						$ticket_line_item->name(),
533
+						current_time(get_option('date_format') . ' ' . get_option('time_format'))
534
+					),
535
+					'LIN_total'      => 0,
536
+					'LIN_unit_price' => 0,
537
+					'LIN_quantity'   => $qty,
538
+					'LIN_is_taxable' => $ticket_line_item->is_taxable(),
539
+					'LIN_order'      => count($ticket_line_item->children()),
540
+					'LIN_type'       => EEM_Line_Item::type_cancellation,
541
+				]
542
+			);
543
+			$ticket_line_item->add_child_line_item($cancellation_line_item);
544
+		}
545
+		if ($ticket_line_item->save_this_and_descendants() > 0) {
546
+			// decrement parent line item quantity
547
+			$event_line_item = $ticket_line_item->parent();
548
+			if (
549
+				$event_line_item instanceof EE_Line_Item
550
+				&& $event_line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_EVENT
551
+			) {
552
+				$event_line_item->set_quantity($event_line_item->quantity() - $qty);
553
+				$event_line_item->save();
554
+			}
555
+			EEH_Line_Item::get_grand_total_and_recalculate_everything($ticket_line_item);
556
+			return true;
557
+		}
558
+		return false;
559
+	}
560
+
561
+
562
+	/**
563
+	 * reinstates (un-cancels?) a previously canceled ticket line item,
564
+	 * by incrementing its quantity by 1, and decrementing its "type_cancellation" sub-line-item.
565
+	 * ALL totals and subtotals will NEED TO BE UPDATED after performing this action
566
+	 *
567
+	 * @param EE_Line_Item $ticket_line_item
568
+	 * @param int          $qty
569
+	 * @return bool success
570
+	 * @throws EE_Error
571
+	 * @throws InvalidArgumentException
572
+	 * @throws InvalidDataTypeException
573
+	 * @throws InvalidInterfaceException
574
+	 * @throws ReflectionException
575
+	 */
576
+	public static function reinstate_canceled_ticket_line_item(EE_Line_Item $ticket_line_item, int $qty = 1): bool
577
+	{
578
+		// validate incoming line_item
579
+		if ($ticket_line_item->OBJ_type() !== EEM_Line_Item::OBJ_TYPE_TICKET) {
580
+			throw new EE_Error(
581
+				sprintf(
582
+					esc_html__(
583
+						'The supplied line item must have an Object Type of "Ticket", not %1$s.',
584
+						'event_espresso'
585
+					),
586
+					$ticket_line_item->type()
587
+				)
588
+			);
589
+		}
590
+		// get cancellation sub line item
591
+		$cancellation_line_item = EEH_Line_Item::get_descendants_of_type(
592
+			$ticket_line_item,
593
+			EEM_Line_Item::type_cancellation
594
+		);
595
+		$cancellation_line_item = reset($cancellation_line_item);
596
+		// verify that this ticket was indeed previously cancelled
597
+		if (! $cancellation_line_item instanceof EE_Line_Item) {
598
+			return false;
599
+		}
600
+		if ($cancellation_line_item->quantity() > $qty) {
601
+			// decrement cancelled quantity
602
+			$cancellation_line_item->set_quantity($cancellation_line_item->quantity() - $qty);
603
+		} elseif ($cancellation_line_item->quantity() === $qty) {
604
+			// decrement cancelled quantity in case anyone still has the object kicking around
605
+			$cancellation_line_item->set_quantity($cancellation_line_item->quantity() - $qty);
606
+			// delete because quantity will end up as 0
607
+			$cancellation_line_item->delete();
608
+			// and attempt to destroy the object,
609
+			// even though PHP won't actually destroy it until it needs the memory
610
+			unset($cancellation_line_item);
611
+		} else {
612
+			// what ?!?! negative quantity ?!?!
613
+			throw new EE_Error(
614
+				sprintf(
615
+					esc_html__(
616
+						'Can not reinstate %1$d cancelled ticket(s) because the cancelled ticket quantity is only %2$d.',
617
+						'event_espresso'
618
+					),
619
+					$qty,
620
+					$cancellation_line_item->quantity()
621
+				)
622
+			);
623
+		}
624
+		// increment ticket quantity
625
+		$ticket_line_item->set_quantity($ticket_line_item->quantity() + $qty);
626
+		if ($ticket_line_item->save_this_and_descendants() > 0) {
627
+			// increment parent line item quantity
628
+			$event_line_item = $ticket_line_item->parent();
629
+			if (
630
+				$event_line_item instanceof EE_Line_Item
631
+				&& $event_line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_EVENT
632
+			) {
633
+				$event_line_item->set_quantity($event_line_item->quantity() + $qty);
634
+			}
635
+			EEH_Line_Item::get_grand_total_and_recalculate_everything($ticket_line_item);
636
+			return true;
637
+		}
638
+		return false;
639
+	}
640
+
641
+
642
+	/**
643
+	 * calls EEH_Line_Item::find_transaction_grand_total_for_line_item()
644
+	 * then EE_Line_Item::recalculate_total_including_taxes() on the result
645
+	 *
646
+	 * @param EE_Line_Item $line_item
647
+	 * @return float
648
+	 * @throws EE_Error
649
+	 * @throws InvalidArgumentException
650
+	 * @throws InvalidDataTypeException
651
+	 * @throws InvalidInterfaceException
652
+	 * @throws ReflectionException
653
+	 */
654
+	public static function get_grand_total_and_recalculate_everything(EE_Line_Item $line_item): float
655
+	{
656
+		$grand_total_line_item = EEH_Line_Item::find_transaction_grand_total_for_line_item($line_item);
657
+		return $grand_total_line_item->recalculate_total_including_taxes();
658
+	}
659
+
660
+
661
+	/**
662
+	 * Gets the line item which contains the subtotal of all the items
663
+	 *
664
+	 * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
665
+	 * @return EE_Line_Item
666
+	 * @throws EE_Error
667
+	 * @throws InvalidArgumentException
668
+	 * @throws InvalidDataTypeException
669
+	 * @throws InvalidInterfaceException
670
+	 * @throws ReflectionException
671
+	 */
672
+	public static function get_pre_tax_subtotal(EE_Line_Item $total_line_item): EE_Line_Item
673
+	{
674
+		$pre_tax_subtotal = $total_line_item->get_child_line_item('pre-tax-subtotal');
675
+		return $pre_tax_subtotal instanceof EE_Line_Item
676
+			? $pre_tax_subtotal
677
+			: self::create_pre_tax_subtotal($total_line_item);
678
+	}
679
+
680
+
681
+	/**
682
+	 * Gets the line item for the taxes subtotal
683
+	 *
684
+	 * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
685
+	 * @return EE_Line_Item
686
+	 * @throws EE_Error
687
+	 * @throws InvalidArgumentException
688
+	 * @throws InvalidDataTypeException
689
+	 * @throws InvalidInterfaceException
690
+	 * @throws ReflectionException
691
+	 */
692
+	public static function get_taxes_subtotal(EE_Line_Item $total_line_item): EE_Line_Item
693
+	{
694
+		$taxes = $total_line_item->get_child_line_item('taxes');
695
+		return $taxes ?: self::create_taxes_subtotal($total_line_item);
696
+	}
697
+
698
+
699
+	/**
700
+	 * sets the TXN ID on an EE_Line_Item if passed a valid EE_Transaction object
701
+	 *
702
+	 * @param EE_Line_Item        $line_item
703
+	 * @param EE_Transaction|null $transaction
704
+	 * @return void
705
+	 * @throws EE_Error
706
+	 * @throws InvalidArgumentException
707
+	 * @throws InvalidDataTypeException
708
+	 * @throws InvalidInterfaceException
709
+	 * @throws ReflectionException
710
+	 */
711
+	public static function set_TXN_ID(EE_Line_Item $line_item, ?EE_Transaction $transaction = null)
712
+	{
713
+		if ($transaction) {
714
+			/** @type EEM_Transaction $EEM_Transaction */
715
+			$EEM_Transaction = EE_Registry::instance()->load_model('Transaction');
716
+			$TXN_ID          = $EEM_Transaction->ensure_is_ID($transaction);
717
+			$line_item->set_TXN_ID($TXN_ID);
718
+		}
719
+	}
720
+
721
+
722
+	/**
723
+	 * Creates a new default total line item for the transaction,
724
+	 * and its tickets subtotal and taxes subtotal line items (and adds the
725
+	 * existing taxes as children of the taxes subtotal line item)
726
+	 *
727
+	 * @param EE_Transaction|null $transaction
728
+	 * @return EE_Line_Item of type total
729
+	 * @throws EE_Error
730
+	 * @throws InvalidArgumentException
731
+	 * @throws InvalidDataTypeException
732
+	 * @throws InvalidInterfaceException
733
+	 * @throws ReflectionException
734
+	 */
735
+	public static function create_total_line_item(?EE_Transaction $transaction = null): EE_Line_Item
736
+	{
737
+		$total_line_item = EE_Line_Item::new_instance(
738
+			[
739
+				'LIN_code' => 'total',
740
+				'LIN_name' => esc_html__('Grand Total', 'event_espresso'),
741
+				'LIN_type' => EEM_Line_Item::type_total,
742
+				'OBJ_type' => EEM_Line_Item::OBJ_TYPE_TRANSACTION,
743
+			]
744
+		);
745
+		$total_line_item = apply_filters(
746
+			'FHEE__EEH_Line_Item__create_total_line_item__total_line_item',
747
+			$total_line_item
748
+		);
749
+		self::set_TXN_ID($total_line_item, $transaction);
750
+		self::create_pre_tax_subtotal($total_line_item, $transaction);
751
+		self::create_taxes_subtotal($total_line_item, $transaction);
752
+		return $total_line_item;
753
+	}
754
+
755
+
756
+	/**
757
+	 * Creates a default items subtotal line item
758
+	 *
759
+	 * @param EE_Line_Item        $total_line_item
760
+	 * @param EE_Transaction|null $transaction
761
+	 * @return EE_Line_Item
762
+	 * @throws EE_Error
763
+	 * @throws InvalidArgumentException
764
+	 * @throws InvalidDataTypeException
765
+	 * @throws InvalidInterfaceException
766
+	 * @throws ReflectionException
767
+	 */
768
+	protected static function create_pre_tax_subtotal(
769
+		EE_Line_Item $total_line_item,
770
+		?EE_Transaction $transaction = null
771
+	): EE_Line_Item {
772
+		$pre_tax_line_item = EE_Line_Item::new_instance(
773
+			[
774
+				'LIN_code' => 'pre-tax-subtotal',
775
+				'LIN_name' => esc_html__('Pre-Tax Subtotal', 'event_espresso'),
776
+				'LIN_type' => EEM_Line_Item::type_sub_total,
777
+			]
778
+		);
779
+		$pre_tax_line_item = apply_filters(
780
+			'FHEE__EEH_Line_Item__create_pre_tax_subtotal__pre_tax_line_item',
781
+			$pre_tax_line_item
782
+		);
783
+		self::set_TXN_ID($pre_tax_line_item, $transaction);
784
+		$total_line_item->add_child_line_item($pre_tax_line_item);
785
+		self::create_event_subtotal($pre_tax_line_item, $transaction);
786
+		return $pre_tax_line_item;
787
+	}
788
+
789
+
790
+	/**
791
+	 * Creates a line item for the taxes subtotal and finds all the tax prices
792
+	 * and applies taxes to it
793
+	 *
794
+	 * @param EE_Line_Item        $total_line_item of type EEM_Line_Item::type_total
795
+	 * @param EE_Transaction|null $transaction
796
+	 * @return EE_Line_Item
797
+	 * @throws EE_Error
798
+	 * @throws InvalidArgumentException
799
+	 * @throws InvalidDataTypeException
800
+	 * @throws InvalidInterfaceException
801
+	 * @throws ReflectionException
802
+	 */
803
+	protected static function create_taxes_subtotal(
804
+		EE_Line_Item $total_line_item,
805
+		?EE_Transaction $transaction = null
806
+	): EE_Line_Item {
807
+		$tax_line_item = EE_Line_Item::new_instance(
808
+			[
809
+				'LIN_code'  => 'taxes',
810
+				'LIN_name'  => esc_html__('Taxes', 'event_espresso'),
811
+				'LIN_type'  => EEM_Line_Item::type_tax_sub_total,
812
+				'LIN_order' => 1000,// this should always come last
813
+			]
814
+		);
815
+		$tax_line_item = apply_filters(
816
+			'FHEE__EEH_Line_Item__create_taxes_subtotal__tax_line_item',
817
+			$tax_line_item
818
+		);
819
+		self::set_TXN_ID($tax_line_item, $transaction);
820
+		$total_line_item->add_child_line_item($tax_line_item);
821
+		// and lastly, add the actual taxes
822
+		self::apply_taxes($total_line_item);
823
+		return $tax_line_item;
824
+	}
825
+
826
+
827
+	/**
828
+	 * Creates a default items subtotal line item
829
+	 *
830
+	 * @param EE_Line_Item        $pre_tax_line_item
831
+	 * @param EE_Transaction|null $transaction
832
+	 * @param EE_Event|null       $event
833
+	 * @return EE_Line_Item
834
+	 * @throws EE_Error
835
+	 * @throws InvalidArgumentException
836
+	 * @throws InvalidDataTypeException
837
+	 * @throws InvalidInterfaceException
838
+	 * @throws ReflectionException
839
+	 */
840
+	public static function create_event_subtotal(
841
+		EE_Line_Item $pre_tax_line_item,
842
+		?EE_Transaction $transaction = null,
843
+		?EE_Event $event = null
844
+	): EE_Line_Item {
845
+		// first check if this line item already exists
846
+		$event_line_item = EEM_Line_Item::instance()->get_one(
847
+			[
848
+				[
849
+					'LIN_type' => EEM_Line_Item::type_sub_total,
850
+					'OBJ_type' => EEM_Line_Item::OBJ_TYPE_EVENT,
851
+					'OBJ_ID'   => $event instanceof EE_Event ? $event->ID() : 0,
852
+					'TXN_ID'   => $transaction instanceof EE_Transaction ? $transaction->ID() : 0,
853
+				],
854
+			]
855
+		);
856
+		if ($event_line_item instanceof EE_Line_Item) {
857
+			return $event_line_item;
858
+		}
859
+
860
+		$event_line_item = EE_Line_Item::new_instance(
861
+			[
862
+				'LIN_code' => self::get_event_code($event),
863
+				'LIN_name' => self::get_event_name($event),
864
+				'LIN_desc' => self::get_event_desc($event),
865
+				'LIN_type' => EEM_Line_Item::type_sub_total,
866
+				'OBJ_type' => EEM_Line_Item::OBJ_TYPE_EVENT,
867
+				'OBJ_ID'   => $event instanceof EE_Event ? $event->ID() : 0,
868
+			]
869
+		);
870
+		$event_line_item = apply_filters(
871
+			'FHEE__EEH_Line_Item__create_event_subtotal__event_line_item',
872
+			$event_line_item
873
+		);
874
+		self::set_TXN_ID($event_line_item, $transaction);
875
+		$pre_tax_line_item->add_child_line_item($event_line_item);
876
+		return $event_line_item;
877
+	}
878
+
879
+
880
+	/**
881
+	 * Gets what the event ticket's code SHOULD be
882
+	 *
883
+	 * @param EE_Event|null $event
884
+	 * @return string
885
+	 * @throws EE_Error
886
+	 * @throws ReflectionException
887
+	 */
888
+	public static function get_event_code(?EE_Event $event = null): string
889
+	{
890
+		return 'event-' . ($event instanceof EE_Event ? $event->ID() : '0');
891
+	}
892
+
893
+
894
+	/**
895
+	 * Gets the event name
896
+	 *
897
+	 * @param EE_Event|null $event
898
+	 * @return string
899
+	 * @throws EE_Error
900
+	 * @throws ReflectionException
901
+	 */
902
+	public static function get_event_name(?EE_Event $event = null): string
903
+	{
904
+		return $event instanceof EE_Event
905
+			? mb_substr($event->name(), 0, 245)
906
+			: esc_html__('Event', 'event_espresso');
907
+	}
908
+
909
+
910
+	/**
911
+	 * Gets the event excerpt
912
+	 *
913
+	 * @param EE_Event|null $event
914
+	 * @return string
915
+	 * @throws EE_Error
916
+	 * @throws ReflectionException
917
+	 */
918
+	public static function get_event_desc(?EE_Event $event = null): string
919
+	{
920
+		return $event instanceof EE_Event ? $event->short_description() : '';
921
+	}
922
+
923
+
924
+	/**
925
+	 * Given the grand total line item and a ticket, finds the event sub-total
926
+	 * line item the ticket's purchase should be added onto
927
+	 *
928
+	 * @access public
929
+	 * @param EE_Line_Item $grand_total the grand total line item
930
+	 * @param EE_Ticket    $ticket
931
+	 * @return EE_Line_Item
932
+	 * @throws EE_Error
933
+	 * @throws InvalidArgumentException
934
+	 * @throws InvalidDataTypeException
935
+	 * @throws InvalidInterfaceException
936
+	 * @throws ReflectionException
937
+	 */
938
+	public static function get_event_line_item_for_ticket(EE_Line_Item $grand_total, EE_Ticket $ticket): EE_Line_Item
939
+	{
940
+		$first_datetime = $ticket->first_datetime();
941
+		if (! $first_datetime instanceof EE_Datetime) {
942
+			throw new EE_Error(
943
+				sprintf(
944
+					esc_html__('The supplied ticket (ID %d) has no datetimes', 'event_espresso'),
945
+					$ticket->ID()
946
+				)
947
+			);
948
+		}
949
+		$event = $first_datetime->event();
950
+		if (! $event instanceof EE_Event) {
951
+			throw new EE_Error(
952
+				sprintf(
953
+					esc_html__(
954
+						'The supplied ticket (ID %d) has no event data associated with it.',
955
+						'event_espresso'
956
+					),
957
+					$ticket->ID()
958
+				)
959
+			);
960
+		}
961
+		$events_sub_total = EEH_Line_Item::get_event_line_item($grand_total, $event);
962
+		if (! $events_sub_total instanceof EE_Line_Item) {
963
+			throw new EE_Error(
964
+				sprintf(
965
+					esc_html__(
966
+						'There is no events sub-total for ticket %s on total line item %d',
967
+						'event_espresso'
968
+					),
969
+					$ticket->ID(),
970
+					$grand_total->ID()
971
+				)
972
+			);
973
+		}
974
+		return $events_sub_total;
975
+	}
976
+
977
+
978
+	/**
979
+	 * Gets the event line item
980
+	 *
981
+	 * @param EE_Line_Item  $total_line_item
982
+	 * @param EE_Event|null $event
983
+	 * @return EE_Line_Item for the event subtotal which is a child of $grand_total
984
+	 * @throws EE_Error
985
+	 * @throws InvalidArgumentException
986
+	 * @throws InvalidDataTypeException
987
+	 * @throws InvalidInterfaceException
988
+	 * @throws ReflectionException
989
+	 */
990
+	public static function get_event_line_item(EE_Line_Item $total_line_item, ?EE_Event $event = null): ?EE_Line_Item
991
+	{
992
+		/** @type EE_Event $event */
993
+		$event           = EEM_Event::instance()->ensure_is_obj($event, true);
994
+		$event_line_item = null;
995
+		$event_line_items = EEH_Line_Item::get_event_subtotals($total_line_item);
996
+		foreach ($event_line_items as $event_line_item) {
997
+			// default event subtotal, we should only ever find this the first time this method is called
998
+			$OBJ_ID = $event_line_item->OBJ_ID();
999
+			if ($OBJ_ID === $event->ID()) {
1000
+				// found existing line item for this event in the cart, so break out of loop and use this one
1001
+				break;
1002
+			}
1003
+			if (! $OBJ_ID) {
1004
+				// let's use this! but first... set the event details
1005
+				EEH_Line_Item::set_event_subtotal_details($event_line_item, $event);
1006
+				break;
1007
+			}
1008
+		}
1009
+		if (! $event_line_item instanceof EE_Line_Item) {
1010
+			// there is no event sub-total yet, so add it
1011
+			$pre_tax_subtotal = EEH_Line_Item::get_pre_tax_subtotal($total_line_item);
1012
+			// a new "event" subtotal SHOULD have been created
1013
+			$event_subtotals = EEH_Line_Item::get_event_subtotals($total_line_item);
1014
+			$event_line_item = reset($event_subtotals);
1015
+			// but in ccase one wasn't created for some reason...
1016
+			if (! $event_line_item instanceof EE_Line_Item) {
1017
+				// create a new "event" subtotal
1018
+				$txn = $total_line_item->transaction();
1019
+				$event_line_item = EEH_Line_Item::create_event_subtotal($pre_tax_subtotal, $txn, $event);
1020
+			}
1021
+			// and set the event details
1022
+			EEH_Line_Item::set_event_subtotal_details($event_line_item, $event);
1023
+		}
1024
+		return $event_line_item;
1025
+	}
1026
+
1027
+
1028
+	/**
1029
+	 * Creates a default items subtotal line item
1030
+	 *
1031
+	 * @param EE_Line_Item        $event_line_item
1032
+	 * @param EE_Event|null       $event
1033
+	 * @param EE_Transaction|null $transaction
1034
+	 * @return void
1035
+	 * @throws EE_Error
1036
+	 * @throws InvalidArgumentException
1037
+	 * @throws InvalidDataTypeException
1038
+	 * @throws InvalidInterfaceException
1039
+	 * @throws ReflectionException
1040
+	 */
1041
+	public static function set_event_subtotal_details(
1042
+		EE_Line_Item $event_line_item,
1043
+		EE_Event $event = null,
1044
+		?EE_Transaction $transaction = null
1045
+	) {
1046
+		if ($event instanceof EE_Event) {
1047
+			$event_line_item->set_code(self::get_event_code($event));
1048
+			$event_line_item->set_name(self::get_event_name($event));
1049
+			$event_line_item->set_desc(self::get_event_desc($event));
1050
+			$event_line_item->set_OBJ_ID($event->ID());
1051
+		}
1052
+		self::set_TXN_ID($event_line_item, $transaction);
1053
+	}
1054
+
1055
+
1056
+	/**
1057
+	 * Finds what taxes should apply, adds them as tax line items under the taxes sub-total,
1058
+	 * and recalculates the taxes sub-total and the grand total. Resets the taxes, so
1059
+	 * any old taxes are removed
1060
+	 *
1061
+	 * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
1062
+	 * @param bool         $update_txn_status
1063
+	 * @return bool
1064
+	 * @throws EE_Error
1065
+	 * @throws InvalidArgumentException
1066
+	 * @throws InvalidDataTypeException
1067
+	 * @throws InvalidInterfaceException
1068
+	 * @throws ReflectionException
1069
+	 * @throws RuntimeException
1070
+	 */
1071
+	public static function apply_taxes(EE_Line_Item $total_line_item, bool $update_txn_status = false): bool
1072
+	{
1073
+		$total_line_item       = EEH_Line_Item::find_transaction_grand_total_for_line_item($total_line_item);
1074
+		$taxes_line_item       = self::get_taxes_subtotal($total_line_item);
1075
+		$existing_global_taxes = $taxes_line_item->tax_descendants();
1076
+		$updates               = false;
1077
+		// loop thru taxes
1078
+		$global_taxes = EEH_Line_Item::getGlobalTaxes();
1079
+		foreach ($global_taxes as $order => $taxes) {
1080
+			foreach ($taxes as $tax) {
1081
+				if ($tax instanceof EE_Price) {
1082
+					$found = false;
1083
+					// check if this is already an existing tax
1084
+					foreach ($existing_global_taxes as $existing_global_tax) {
1085
+						if ($tax->ID() === $existing_global_tax->OBJ_ID()) {
1086
+							// maybe update the tax rate in case it has changed
1087
+							if ($existing_global_tax->percent() !== $tax->amount()) {
1088
+								$existing_global_tax->set_percent($tax->amount());
1089
+								$existing_global_tax->save();
1090
+								$updates = true;
1091
+							}
1092
+							$found = true;
1093
+							break;
1094
+						}
1095
+					}
1096
+					if (! $found) {
1097
+						// add a new line item for this global tax
1098
+						$tax_line_item = apply_filters(
1099
+							'FHEE__EEH_Line_Item__apply_taxes__tax_line_item',
1100
+							EE_Line_Item::new_instance(
1101
+								[
1102
+									'LIN_name'       => $tax->name(),
1103
+									'LIN_desc'       => $tax->desc(),
1104
+									'LIN_percent'    => $tax->amount(),
1105
+									'LIN_is_taxable' => false,
1106
+									'LIN_order'      => $order,
1107
+									'LIN_total'      => 0,
1108
+									'LIN_type'       => EEM_Line_Item::type_tax,
1109
+									'OBJ_type'       => EEM_Line_Item::OBJ_TYPE_PRICE,
1110
+									'OBJ_ID'         => $tax->ID(),
1111
+								]
1112
+							)
1113
+						);
1114
+						$updates       = $taxes_line_item->add_child_line_item($tax_line_item) ? true : $updates;
1115
+					}
1116
+				}
1117
+			}
1118
+		}
1119
+		// only recalculate totals if something changed
1120
+		if ($updates || $update_txn_status) {
1121
+			$total_line_item->recalculate_total_including_taxes($update_txn_status);
1122
+			return true;
1123
+		}
1124
+		return false;
1125
+	}
1126
+
1127
+
1128
+	/**
1129
+	 * Ensures that taxes have been applied to the order, if not applies them.
1130
+	 * Returns the total amount of tax
1131
+	 *
1132
+	 * @param EE_Line_Item|null $total_line_item of type EEM_Line_Item::type_total
1133
+	 * @return float
1134
+	 * @throws EE_Error
1135
+	 * @throws InvalidArgumentException
1136
+	 * @throws InvalidDataTypeException
1137
+	 * @throws InvalidInterfaceException
1138
+	 * @throws ReflectionException
1139
+	 */
1140
+	public static function ensure_taxes_applied(?EE_Line_Item $total_line_item): float
1141
+	{
1142
+		$taxes_subtotal = self::get_taxes_subtotal($total_line_item);
1143
+		if (! $taxes_subtotal->children()) {
1144
+			self::apply_taxes($total_line_item);
1145
+		}
1146
+		return $taxes_subtotal->total();
1147
+	}
1148
+
1149
+
1150
+	/**
1151
+	 * Deletes ALL children of the passed line item
1152
+	 *
1153
+	 * @param EE_Line_Item $parent_line_item
1154
+	 * @return bool
1155
+	 * @throws EE_Error
1156
+	 * @throws InvalidArgumentException
1157
+	 * @throws InvalidDataTypeException
1158
+	 * @throws InvalidInterfaceException
1159
+	 * @throws ReflectionException
1160
+	 */
1161
+	public static function delete_all_child_items(EE_Line_Item $parent_line_item)
1162
+	{
1163
+		$deleted = 0;
1164
+		foreach ($parent_line_item->children() as $child_line_item) {
1165
+			if ($child_line_item instanceof EE_Line_Item) {
1166
+				$deleted += EEH_Line_Item::delete_all_child_items($child_line_item);
1167
+				if ($child_line_item->ID()) {
1168
+					$child_line_item->delete();
1169
+					unset($child_line_item);
1170
+				} else {
1171
+					$parent_line_item->delete_child_line_item($child_line_item->code());
1172
+				}
1173
+				$deleted++;
1174
+			}
1175
+		}
1176
+		return $deleted;
1177
+	}
1178
+
1179
+
1180
+	/**
1181
+	 * Deletes the line items as indicated by the line item code(s) provided,
1182
+	 * regardless of where they're found in the line item tree. Automatically
1183
+	 * re-calculates the line item totals and updates the related transaction. But
1184
+	 * DOES NOT automatically upgrade the transaction's registrations' final prices (which
1185
+	 * should probably change because of this).
1186
+	 * You should call EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
1187
+	 * after using this, to keep the registration final prices in-sync with the transaction's total.
1188
+	 *
1189
+	 * @param EE_Line_Item      $total_line_item of type EEM_Line_Item::type_total
1190
+	 * @param array|bool|string $line_item_codes
1191
+	 * @return int number of items successfully removed
1192
+	 * @throws EE_Error
1193
+	 * @throws ReflectionException
1194
+	 */
1195
+	public static function delete_items(EE_Line_Item $total_line_item, $line_item_codes = false)
1196
+	{
1197
+		if ($total_line_item->type() !== EEM_Line_Item::type_total) {
1198
+			EE_Error::doing_it_wrong(
1199
+				'EEH_Line_Item::delete_items',
1200
+				esc_html__(
1201
+					'This static method should only be called with a TOTAL line item, otherwise we won\'t recalculate the totals correctly',
1202
+					'event_espresso'
1203
+				),
1204
+				'4.6.18'
1205
+			);
1206
+		}
1207
+
1208
+		// check if only a single line_item_id was passed
1209
+		if (! empty($line_item_codes) && ! is_array($line_item_codes)) {
1210
+			// place single line_item_id in an array to appear as multiple line_item_ids
1211
+			$line_item_codes = [$line_item_codes];
1212
+		}
1213
+		$removals = 0;
1214
+		// cycle thru line_item_ids
1215
+		foreach ($line_item_codes as $line_item_id) {
1216
+			$removals += $total_line_item->delete_child_line_item($line_item_id);
1217
+		}
1218
+
1219
+		if ($removals > 0) {
1220
+			$total_line_item->recalculate_taxes_and_tax_total();
1221
+			return $removals;
1222
+		} else {
1223
+			return false;
1224
+		}
1225
+	}
1226
+
1227
+
1228
+	/**
1229
+	 * Overwrites the previous tax by clearing out the old taxes, and creates a new
1230
+	 * tax and updates the total line item accordingly
1231
+	 *
1232
+	 * @param EE_Line_Item $total_line_item
1233
+	 * @param float        $amount
1234
+	 * @param string       $name
1235
+	 * @param string       $description
1236
+	 * @param string       $code
1237
+	 * @param boolean      $add_to_existing_line_item
1238
+	 *                          if true, and a duplicate line item with the same code is found,
1239
+	 *                          $amount will be added onto it; otherwise will simply set the taxes to match $amount
1240
+	 * @return EE_Line_Item the new tax line item created
1241
+	 * @throws EE_Error
1242
+	 * @throws InvalidArgumentException
1243
+	 * @throws InvalidDataTypeException
1244
+	 * @throws InvalidInterfaceException
1245
+	 * @throws ReflectionException
1246
+	 */
1247
+	public static function set_total_tax_to(
1248
+		EE_Line_Item $total_line_item,
1249
+		float $amount,
1250
+		string $name = '',
1251
+		string $description = '',
1252
+		string $code = '',
1253
+		bool $add_to_existing_line_item = false
1254
+	): EE_Line_Item {
1255
+		$tax_subtotal  = self::get_taxes_subtotal($total_line_item);
1256
+		$taxable_total = $total_line_item->taxable_total();
1257
+
1258
+		if ($add_to_existing_line_item) {
1259
+			$new_tax = $tax_subtotal->get_child_line_item($code);
1260
+			EEM_Line_Item::instance()->delete(
1261
+				[['LIN_code' => ['!=', $code], 'LIN_parent' => $tax_subtotal->ID()]]
1262
+			);
1263
+		} else {
1264
+			$new_tax = null;
1265
+			$tax_subtotal->delete_children_line_items();
1266
+		}
1267
+		if ($new_tax) {
1268
+			$new_tax->set_total($new_tax->total() + $amount);
1269
+			$new_tax->set_percent($taxable_total ? $new_tax->total() / $taxable_total * 100 : 0);
1270
+		} else {
1271
+			// no existing tax item. Create it
1272
+			$new_tax = EE_Line_Item::new_instance(
1273
+				[
1274
+					'TXN_ID'      => $total_line_item->TXN_ID(),
1275
+					'LIN_name'    => $name ?: esc_html__('Tax', 'event_espresso'),
1276
+					'LIN_desc'    => $description ?: '',
1277
+					'LIN_percent' => $taxable_total ? ($amount / $taxable_total * 100) : 0,
1278
+					'LIN_total'   => $amount,
1279
+					'LIN_parent'  => $tax_subtotal->ID(),
1280
+					'LIN_type'    => EEM_Line_Item::type_tax,
1281
+					'LIN_code'    => $code,
1282
+				]
1283
+			);
1284
+		}
1285
+
1286
+		$new_tax = apply_filters(
1287
+			'FHEE__EEH_Line_Item__set_total_tax_to__new_tax_subtotal',
1288
+			$new_tax,
1289
+			$total_line_item
1290
+		);
1291
+		$new_tax->save();
1292
+		$tax_subtotal->set_total($new_tax->total());
1293
+		$tax_subtotal->save();
1294
+		$total_line_item->recalculate_total_including_taxes();
1295
+		return $new_tax;
1296
+	}
1297
+
1298
+
1299
+	/**
1300
+	 * Makes all the line items which are children of $line_item taxable (or not).
1301
+	 * Does NOT save the line items
1302
+	 *
1303
+	 * @param EE_Line_Item $line_item
1304
+	 * @param boolean      $taxable
1305
+	 * @param string|null  $code_substring_for_whitelist if this string is part of the line item's code
1306
+	 *                                                   it will be whitelisted (ie, except from becoming taxable)
1307
+	 * @throws EE_Error
1308
+	 * @throws ReflectionException
1309
+	 */
1310
+	public static function set_line_items_taxable(
1311
+		EE_Line_Item $line_item,
1312
+		bool $taxable = true,
1313
+		?string $code_substring_for_whitelist = null
1314
+	) {
1315
+		$whitelisted = false;
1316
+		if ($code_substring_for_whitelist !== null) {
1317
+			$whitelisted = strpos($line_item->code(), $code_substring_for_whitelist) !== false;
1318
+		}
1319
+		if (! $whitelisted && $line_item->is_line_item()) {
1320
+			$line_item->set_is_taxable($taxable);
1321
+		}
1322
+		foreach ($line_item->children() as $child_line_item) {
1323
+			EEH_Line_Item::set_line_items_taxable(
1324
+				$child_line_item,
1325
+				$taxable,
1326
+				$code_substring_for_whitelist
1327
+			);
1328
+		}
1329
+	}
1330
+
1331
+
1332
+	/**
1333
+	 * Gets all descendants that are event subtotals
1334
+	 *
1335
+	 * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1336
+	 * @return EE_Line_Item[]
1337
+	 * @throws EE_Error
1338
+	 * @throws ReflectionException
1339
+	 * @uses  EEH_Line_Item::get_subtotals_of_object_type()
1340
+	 */
1341
+	public static function get_event_subtotals(EE_Line_Item $parent_line_item): array
1342
+	{
1343
+		return self::get_subtotals_of_object_type($parent_line_item, EEM_Line_Item::OBJ_TYPE_EVENT);
1344
+	}
1345
+
1346
+
1347
+	/**
1348
+	 * Gets all descendants subtotals that match the supplied object type
1349
+	 *
1350
+	 * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1351
+	 * @param string       $obj_type
1352
+	 * @return EE_Line_Item[]
1353
+	 * @throws EE_Error
1354
+	 * @throws ReflectionException
1355
+	 * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
1356
+	 */
1357
+	public static function get_subtotals_of_object_type(EE_Line_Item $parent_line_item, string $obj_type = ''): array
1358
+	{
1359
+		return self::_get_descendants_by_type_and_object_type(
1360
+			$parent_line_item,
1361
+			EEM_Line_Item::type_sub_total,
1362
+			$obj_type
1363
+		);
1364
+	}
1365
+
1366
+
1367
+	/**
1368
+	 * Gets all descendants that are tickets
1369
+	 *
1370
+	 * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1371
+	 * @return EE_Line_Item[]
1372
+	 * @throws EE_Error
1373
+	 * @throws ReflectionException
1374
+	 * @uses  EEH_Line_Item::get_line_items_of_object_type()
1375
+	 */
1376
+	public static function get_ticket_line_items(EE_Line_Item $parent_line_item): array
1377
+	{
1378
+		return self::get_line_items_of_object_type(
1379
+			$parent_line_item,
1380
+			EEM_Line_Item::OBJ_TYPE_TICKET
1381
+		);
1382
+	}
1383
+
1384
+
1385
+	/**
1386
+	 * Gets all descendants subtotals that match the supplied object type
1387
+	 *
1388
+	 * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1389
+	 * @param string       $obj_type
1390
+	 * @return EE_Line_Item[]
1391
+	 * @throws EE_Error
1392
+	 * @throws ReflectionException
1393
+	 * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
1394
+	 */
1395
+	public static function get_line_items_of_object_type(EE_Line_Item $parent_line_item, string $obj_type = ''): array
1396
+	{
1397
+		return self::_get_descendants_by_type_and_object_type(
1398
+			$parent_line_item,
1399
+			EEM_Line_Item::type_line_item,
1400
+			$obj_type
1401
+		);
1402
+	}
1403
+
1404
+
1405
+	/**
1406
+	 * Gets all the descendants (ie, children or children of children etc) that are of the type 'tax'
1407
+	 *
1408
+	 * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1409
+	 * @return EE_Line_Item[]
1410
+	 * @throws EE_Error
1411
+	 * @throws ReflectionException
1412
+	 * @uses  EEH_Line_Item::get_descendants_of_type()
1413
+	 */
1414
+	public static function get_tax_descendants(EE_Line_Item $parent_line_item): array
1415
+	{
1416
+		return EEH_Line_Item::get_descendants_of_type(
1417
+			$parent_line_item,
1418
+			EEM_Line_Item::type_tax
1419
+		);
1420
+	}
1421
+
1422
+
1423
+	/**
1424
+	 * Gets all the real items purchased which are children of this item
1425
+	 *
1426
+	 * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1427
+	 * @return EE_Line_Item[]
1428
+	 * @throws EE_Error
1429
+	 * @throws ReflectionException
1430
+	 * @uses  EEH_Line_Item::get_descendants_of_type()
1431
+	 */
1432
+	public static function get_line_item_descendants(EE_Line_Item $parent_line_item): array
1433
+	{
1434
+		return EEH_Line_Item::get_descendants_of_type(
1435
+			$parent_line_item,
1436
+			EEM_Line_Item::type_line_item
1437
+		);
1438
+	}
1439
+
1440
+
1441
+	/**
1442
+	 * Gets all descendants of supplied line item that match the supplied line item type
1443
+	 *
1444
+	 * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1445
+	 * @param string       $line_item_type   one of the EEM_Line_Item constants
1446
+	 * @return EE_Line_Item[]
1447
+	 * @throws EE_Error
1448
+	 * @throws ReflectionException
1449
+	 * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
1450
+	 */
1451
+	public static function get_descendants_of_type(EE_Line_Item $parent_line_item, string $line_item_type): array
1452
+	{
1453
+		return self::_get_descendants_by_type_and_object_type(
1454
+			$parent_line_item,
1455
+			$line_item_type
1456
+		);
1457
+	}
1458
+
1459
+
1460
+	/**
1461
+	 * Gets all descendants of supplied line item that match the supplied line item type and possibly the object type
1462
+	 * as well
1463
+	 *
1464
+	 * @param EE_Line_Item $parent_line_item  - the line item to find descendants of
1465
+	 * @param string       $line_item_type    one of the EEM_Line_Item constants
1466
+	 * @param string|null  $obj_type          object model class name (minus prefix) or NULL to ignore object type when
1467
+	 *                                        searching
1468
+	 * @return EE_Line_Item[]
1469
+	 * @throws EE_Error
1470
+	 * @throws ReflectionException
1471
+	 */
1472
+	protected static function _get_descendants_by_type_and_object_type(
1473
+		EE_Line_Item $parent_line_item,
1474
+		string $line_item_type,
1475
+		?string $obj_type = null
1476
+	): array {
1477
+		$objects = [];
1478
+		foreach ($parent_line_item->children() as $child_line_item) {
1479
+			if ($child_line_item instanceof EE_Line_Item) {
1480
+				if (
1481
+					$child_line_item->type() === $line_item_type
1482
+					&& (
1483
+						$child_line_item->OBJ_type() === $obj_type || $obj_type === null
1484
+					)
1485
+				) {
1486
+					$objects[] = $child_line_item;
1487
+				} else {
1488
+					// go-through-all-its children looking for more matches
1489
+					$objects = array_merge(
1490
+						$objects,
1491
+						self::_get_descendants_by_type_and_object_type(
1492
+							$child_line_item,
1493
+							$line_item_type,
1494
+							$obj_type
1495
+						)
1496
+					);
1497
+				}
1498
+			}
1499
+		}
1500
+		return $objects;
1501
+	}
1502
+
1503
+
1504
+	/**
1505
+	 * Gets all descendants subtotals that match the supplied object type
1506
+	 *
1507
+	 * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1508
+	 * @param string       $OBJ_type         object type (like Event)
1509
+	 * @param array        $OBJ_IDs          array of OBJ_IDs
1510
+	 * @return EE_Line_Item[]
1511
+	 * @throws EE_Error
1512
+	 * @throws ReflectionException
1513
+	 * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
1514
+	 */
1515
+	public static function get_line_items_by_object_type_and_IDs(
1516
+		EE_Line_Item $parent_line_item,
1517
+		string $OBJ_type = '',
1518
+		array $OBJ_IDs = []
1519
+	): array {
1520
+		return self::_get_descendants_by_object_type_and_object_ID(
1521
+			$parent_line_item,
1522
+			$OBJ_type,
1523
+			$OBJ_IDs
1524
+		);
1525
+	}
1526
+
1527
+
1528
+	/**
1529
+	 * Gets all descendants of supplied line item that match the supplied line item type and possibly the object type
1530
+	 * as well
1531
+	 *
1532
+	 * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1533
+	 * @param string       $OBJ_type         object type (like Event)
1534
+	 * @param array        $OBJ_IDs          array of OBJ_IDs
1535
+	 * @return EE_Line_Item[]
1536
+	 * @throws EE_Error
1537
+	 * @throws ReflectionException
1538
+	 */
1539
+	protected static function _get_descendants_by_object_type_and_object_ID(
1540
+		EE_Line_Item $parent_line_item,
1541
+		string $OBJ_type,
1542
+		array $OBJ_IDs
1543
+	): array {
1544
+		$objects = [];
1545
+		foreach ($parent_line_item->children() as $child_line_item) {
1546
+			if ($child_line_item instanceof EE_Line_Item) {
1547
+				if (
1548
+					$child_line_item->OBJ_type() === $OBJ_type
1549
+					&& in_array($child_line_item->OBJ_ID(), $OBJ_IDs)
1550
+				) {
1551
+					$objects[] = $child_line_item;
1552
+				} else {
1553
+					// go-through-all-its children looking for more matches
1554
+					$objects = array_merge(
1555
+						$objects,
1556
+						self::_get_descendants_by_object_type_and_object_ID(
1557
+							$child_line_item,
1558
+							$OBJ_type,
1559
+							$OBJ_IDs
1560
+						)
1561
+					);
1562
+				}
1563
+			}
1564
+		}
1565
+		return $objects;
1566
+	}
1567
+
1568
+
1569
+	/**
1570
+	 * Uses a breadth-first-search in order to find the nearest descendant of
1571
+	 * the specified type and returns it, else NULL
1572
+	 *
1573
+	 * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1574
+	 * @param string       $type             like one of the EEM_Line_Item::type_*
1575
+	 * @return EE_Line_Item
1576
+	 * @throws EE_Error
1577
+	 * @throws InvalidArgumentException
1578
+	 * @throws InvalidDataTypeException
1579
+	 * @throws InvalidInterfaceException
1580
+	 * @throws ReflectionException
1581
+	 * @uses  EEH_Line_Item::_get_nearest_descendant()
1582
+	 */
1583
+	public static function get_nearest_descendant_of_type(EE_Line_Item $parent_line_item, string $type): ?EE_Line_Item
1584
+	{
1585
+		return self::_get_nearest_descendant($parent_line_item, 'LIN_type', $type);
1586
+	}
1587
+
1588
+
1589
+	/**
1590
+	 * Uses a breadth-first-search in order to find the nearest descendant
1591
+	 * having the specified LIN_code and returns it, else NULL
1592
+	 *
1593
+	 * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1594
+	 * @param string       $code             any value used for LIN_code
1595
+	 * @return EE_Line_Item
1596
+	 * @throws EE_Error
1597
+	 * @throws InvalidArgumentException
1598
+	 * @throws InvalidDataTypeException
1599
+	 * @throws InvalidInterfaceException
1600
+	 * @throws ReflectionException
1601
+	 * @uses  EEH_Line_Item::_get_nearest_descendant()
1602
+	 */
1603
+	public static function get_nearest_descendant_having_code(
1604
+		EE_Line_Item $parent_line_item,
1605
+		string $code
1606
+	): ?EE_Line_Item {
1607
+		return self::_get_nearest_descendant($parent_line_item, 'LIN_code', $code);
1608
+	}
1609
+
1610
+
1611
+	/**
1612
+	 * Uses a breadth-first-search in order to find the nearest descendant
1613
+	 * having the specified LIN_code and returns it, else NULL
1614
+	 *
1615
+	 * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1616
+	 * @param string       $search_field     name of EE_Line_Item property
1617
+	 * @param mixed        $value            any value stored in $search_field
1618
+	 * @return EE_Line_Item
1619
+	 * @throws EE_Error
1620
+	 * @throws InvalidArgumentException
1621
+	 * @throws InvalidDataTypeException
1622
+	 * @throws InvalidInterfaceException
1623
+	 * @throws ReflectionException
1624
+	 */
1625
+	protected static function _get_nearest_descendant(
1626
+		EE_Line_Item $parent_line_item,
1627
+		string $search_field,
1628
+		$value
1629
+	): ?EE_Line_Item {
1630
+		foreach ($parent_line_item->children() as $child) {
1631
+			if ($child->get($search_field) == $value) {
1632
+				return $child;
1633
+			}
1634
+		}
1635
+		foreach ($parent_line_item->children() as $child) {
1636
+			$descendant_found = self::_get_nearest_descendant(
1637
+				$child,
1638
+				$search_field,
1639
+				$value
1640
+			);
1641
+			if ($descendant_found) {
1642
+				return $descendant_found;
1643
+			}
1644
+		}
1645
+		return null;
1646
+	}
1647
+
1648
+
1649
+	/**
1650
+	 * if passed line item has a TXN ID, uses that to jump directly to the grand total line item for the transaction,
1651
+	 * else recursively walks up the line item tree until a parent of type total is found,
1652
+	 *
1653
+	 * @param EE_Line_Item $line_item
1654
+	 * @return EE_Line_Item
1655
+	 * @throws EE_Error
1656
+	 * @throws ReflectionException
1657
+	 */
1658
+	public static function find_transaction_grand_total_for_line_item(EE_Line_Item $line_item): EE_Line_Item
1659
+	{
1660
+		if ($line_item->is_total()) {
1661
+			return $line_item;
1662
+		}
1663
+		if ($line_item->TXN_ID()) {
1664
+			$total_line_item = $line_item->transaction()->total_line_item(false);
1665
+			if ($total_line_item instanceof EE_Line_Item) {
1666
+				return $total_line_item;
1667
+			}
1668
+		} else {
1669
+			$line_item_parent = $line_item->parent();
1670
+			if ($line_item_parent instanceof EE_Line_Item) {
1671
+				if ($line_item_parent->is_total()) {
1672
+					return $line_item_parent;
1673
+				}
1674
+				return EEH_Line_Item::find_transaction_grand_total_for_line_item($line_item_parent);
1675
+			}
1676
+		}
1677
+		throw new EE_Error(
1678
+			sprintf(
1679
+				esc_html__(
1680
+					'A valid grand total for line item %1$d was not found.',
1681
+					'event_espresso'
1682
+				),
1683
+				$line_item->ID()
1684
+			)
1685
+		);
1686
+	}
1687
+
1688
+
1689
+	/**
1690
+	 * Prints out a representation of the line item tree
1691
+	 *
1692
+	 * @param EE_Line_Item $line_item
1693
+	 * @param int          $indentation
1694
+	 * @param bool         $top_level
1695
+	 * @return void
1696
+	 * @throws EE_Error
1697
+	 * @throws ReflectionException
1698
+	 */
1699
+	public static function visualize(EE_Line_Item $line_item, int $indentation = 0, bool $top_level = true)
1700
+	{
1701
+		if (! defined('WP_DEBUG') || ! WP_DEBUG) {
1702
+			return;
1703
+		}
1704
+		$testing  = defined('EE_TESTS_DIR');
1705
+		$new_line = $testing ? "\n" : '<br />';
1706
+		if ($top_level && ! $testing) {
1707
+			echo '<div style="position: relative; z-index: 9999; margin: 2rem;">';
1708
+			echo '<pre style="padding: 2rem 3rem; color: #00CCFF; background: #363636;">';
1709
+		}
1710
+		if ($top_level || $indentation) {
1711
+			// echo $top_level ? "$new_line$new_line" : $new_line;
1712
+			echo $new_line;
1713
+		}
1714
+		echo str_repeat('. ', $indentation);
1715
+		$breakdown = '';
1716
+		if ($line_item->is_line_item() || $line_item->is_sub_line_item() || $line_item->isTax()) {
1717
+			if ($line_item->is_percent()) {
1718
+				$breakdown = "{$line_item->percent()}%";
1719
+			} else {
1720
+				$breakdown = "\${$line_item->unit_price()} x {$line_item->quantity()}";
1721
+			}
1722
+		}
1723
+		echo wp_kses($line_item->name(), AllowedTags::getAllowedTags());
1724
+		echo " [ ID:{$line_item->ID()} · qty:{$line_item->quantity()} ] {$line_item->type()}";
1725
+		echo " · \${$line_item->total()} · \${$line_item->pretaxTotal()}";
1726
+		if ($breakdown) {
1727
+			echo " ( $breakdown )";
1728
+		}
1729
+		if ($line_item->is_taxable()) {
1730
+			echo '  * taxable';
1731
+		}
1732
+		$children = $line_item->children();
1733
+		if ($children) {
1734
+			foreach ($children as $child) {
1735
+				self::visualize($child, $indentation + 1, false);
1736
+			}
1737
+		}
1738
+		if ($top_level) {
1739
+			echo $testing ? $new_line : "$new_line</pre></div>$new_line";
1740
+		}
1741
+	}
1742
+
1743
+
1744
+	/**
1745
+	 * Calculates the registration's final price, taking into account that they
1746
+	 * need to not only help pay for their OWN ticket, but also any transaction-wide surcharges and taxes,
1747
+	 * and receive a portion of any transaction-wide discounts.
1748
+	 * eg1, if I buy a $1 ticket and brent buys a $9 ticket, and we receive a $5 discount
1749
+	 * then I'll get 1/10 of that $5 discount, which is $0.50, and brent will get
1750
+	 * 9/10ths of that $5 discount, which is $4.50. So my final price should be $0.50
1751
+	 * and brent's final price should be $5.50.
1752
+	 * In order to do this, we basically need to traverse the line item tree calculating
1753
+	 * the running totals (just as if we were recalculating the total), but when we identify
1754
+	 * regular line items, we need to keep track of their share of the grand total.
1755
+	 * Also, we need to keep track of the TAXABLE total for each ticket purchase, so
1756
+	 * we can know how to apply taxes to it. (Note: "taxable total" does not equal the "pretax total"
1757
+	 * when there are non-taxable items; otherwise they would be the same)
1758
+	 *
1759
+	 * @param EE_Line_Item $line_item
1760
+	 * @param array        $billable_ticket_quantities          array of EE_Ticket IDs and their corresponding quantity
1761
+	 *                                                          that can be included in price calculations at this
1762
+	 *                                                          moment
1763
+	 * @return array        keys are line items for tickets IDs and values are their share of the running total,
1764
+	 *                                                          plus the key 'total', and 'taxable' which also has keys
1765
+	 *                                                          of all the ticket IDs. Eg array(
1766
+	 *                                                          12 => 4.3
1767
+	 *                                                          23 => 8.0
1768
+	 *                                                          'total' => 16.6,
1769
+	 *                                                          'taxable' => array(
1770
+	 *                                                          12 => 10,
1771
+	 *                                                          23 => 4
1772
+	 *                                                          ).
1773
+	 *                                                          So to find which registrations have which final price,
1774
+	 *                                                          we need to find which line item is theirs, which can be
1775
+	 *                                                          done with
1776
+	 *                                                          `EEM_Line_Item::instance()->get_line_item_for_registration(
1777
+	 *                                                          $registration );`
1778
+	 * @throws EE_Error
1779
+	 * @throws InvalidArgumentException
1780
+	 * @throws InvalidDataTypeException
1781
+	 * @throws InvalidInterfaceException
1782
+	 * @throws ReflectionException
1783
+	 */
1784
+	public static function calculate_reg_final_prices_per_line_item(
1785
+		EE_Line_Item $line_item,
1786
+		array $billable_ticket_quantities = []
1787
+	): array {
1788
+		$running_totals = [
1789
+			'total'   => 0,
1790
+			'taxable' => ['total' => 0],
1791
+		];
1792
+		foreach ($line_item->children() as $child_line_item) {
1793
+			switch ($child_line_item->type()) {
1794
+				case EEM_Line_Item::type_sub_total:
1795
+					$running_totals_from_subtotal = EEH_Line_Item::calculate_reg_final_prices_per_line_item(
1796
+						$child_line_item,
1797
+						$billable_ticket_quantities
1798
+					);
1799
+					// combine arrays but preserve numeric keys
1800
+					$running_totals                     = array_replace_recursive(
1801
+						$running_totals_from_subtotal,
1802
+						$running_totals
1803
+					);
1804
+					$running_totals['total']            += $running_totals_from_subtotal['total'];
1805
+					$running_totals['taxable']['total'] += $running_totals_from_subtotal['taxable']['total'];
1806
+					break;
1807
+
1808
+				case EEM_Line_Item::type_tax_sub_total:
1809
+					// find how much the taxes percentage is
1810
+					if ($child_line_item->percent() !== 0.0) {
1811
+						$tax_percent_decimal = $child_line_item->percent() / 100;
1812
+					} else {
1813
+						$tax_percent_decimal = EE_Taxes::get_total_taxes_percentage() / 100;
1814
+					}
1815
+					// and apply to all the taxable totals, and add to the pretax totals
1816
+					foreach ($running_totals as $line_item_id => $this_running_total) {
1817
+						// "total" and "taxable" array key is an exception
1818
+						if ($line_item_id === 'taxable') {
1819
+							continue;
1820
+						}
1821
+						$taxable_total                   = $running_totals['taxable'][ $line_item_id ];
1822
+						$running_totals[ $line_item_id ] += ($taxable_total * $tax_percent_decimal);
1823
+					}
1824
+					break;
1825
+
1826
+				case EEM_Line_Item::type_line_item:
1827
+					// ticket line items or ????
1828
+					if ($child_line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET) {
1829
+						// kk it's a ticket
1830
+						if (isset($running_totals[ $child_line_item->ID() ])) {
1831
+							// huh? that shouldn't happen.
1832
+							$running_totals['total'] += $child_line_item->total();
1833
+						} else {
1834
+							// it's not in our running totals yet. great.
1835
+							if ($child_line_item->is_taxable()) {
1836
+								$taxable_amount = $child_line_item->unit_price();
1837
+							} else {
1838
+								$taxable_amount = 0;
1839
+							}
1840
+							// are we only calculating totals for some tickets?
1841
+							if (isset($billable_ticket_quantities[ $child_line_item->OBJ_ID() ])) {
1842
+								$quantity = $billable_ticket_quantities[ $child_line_item->OBJ_ID() ];
1843
+
1844
+								$running_totals[ $child_line_item->ID() ]            = $quantity
1845
+									? $child_line_item->unit_price()
1846
+									: 0;
1847
+								$running_totals['taxable'][ $child_line_item->ID() ] = $quantity
1848
+									? $taxable_amount
1849
+									: 0;
1850
+							} else {
1851
+								$quantity                                            = $child_line_item->quantity();
1852
+								$running_totals[ $child_line_item->ID() ]            = $child_line_item->unit_price();
1853
+								$running_totals['taxable'][ $child_line_item->ID() ] = $taxable_amount;
1854
+							}
1855
+							$running_totals['taxable']['total'] += $taxable_amount * $quantity;
1856
+							$running_totals['total']            += $child_line_item->unit_price() * $quantity;
1857
+						}
1858
+					} else {
1859
+						// it's some other type of item added to the cart
1860
+						// it should affect the running totals
1861
+						// basically we want to convert it into a PERCENT modifier. Because
1862
+						// more clearly affect all registration's final price equally
1863
+						$line_items_percent_of_running_total = $running_totals['total'] > 0
1864
+							? ($child_line_item->total() / $running_totals['total']) + 1
1865
+							: 1;
1866
+						foreach ($running_totals as $line_item_id => $this_running_total) {
1867
+							// the "taxable" array key is an exception
1868
+							if ($line_item_id === 'taxable') {
1869
+								continue;
1870
+							}
1871
+							// update the running totals
1872
+							// yes this actually even works for the running grand total!
1873
+							$running_totals[ $line_item_id ] =
1874
+								$line_items_percent_of_running_total * $this_running_total;
1875
+
1876
+							if ($child_line_item->is_taxable()) {
1877
+								$running_totals['taxable'][ $line_item_id ] =
1878
+									$line_items_percent_of_running_total * $running_totals['taxable'][ $line_item_id ];
1879
+							}
1880
+						}
1881
+					}
1882
+					break;
1883
+			}
1884
+		}
1885
+		return $running_totals;
1886
+	}
1887
+
1888
+
1889
+	/**
1890
+	 * @param EE_Line_Item $total_line_item
1891
+	 * @param EE_Line_Item $ticket_line_item
1892
+	 * @return float | null
1893
+	 * @throws EE_Error
1894
+	 * @throws InvalidArgumentException
1895
+	 * @throws InvalidDataTypeException
1896
+	 * @throws InvalidInterfaceException
1897
+	 * @throws OutOfRangeException
1898
+	 * @throws ReflectionException
1899
+	 */
1900
+	public static function calculate_final_price_for_ticket_line_item(
1901
+		EE_Line_Item $total_line_item,
1902
+		EE_Line_Item $ticket_line_item
1903
+	): ?float {
1904
+		static $final_prices_per_ticket_line_item = [];
1905
+		if (
1906
+			empty($final_prices_per_ticket_line_item)
1907
+			|| empty($final_prices_per_ticket_line_item[ $total_line_item->ID() ])
1908
+		) {
1909
+			$final_prices_per_ticket_line_item[ $total_line_item->ID() ] =
1910
+				EEH_Line_Item::calculate_reg_final_prices_per_line_item(
1911
+					$total_line_item
1912
+				);
1913
+		}
1914
+		// ok now find this new registration's final price
1915
+		if (isset($final_prices_per_ticket_line_item[ $total_line_item->ID() ][ $ticket_line_item->ID() ])) {
1916
+			return $final_prices_per_ticket_line_item[ $total_line_item->ID() ][ $ticket_line_item->ID() ];
1917
+		}
1918
+		$message = sprintf(
1919
+			esc_html__(
1920
+				'The final price for the ticket line item (ID:%1$d) on the total line item (ID:%2$d) could not be calculated.',
1921
+				'event_espresso'
1922
+			),
1923
+			$ticket_line_item->ID(),
1924
+			$total_line_item->ID()
1925
+		);
1926
+		if (WP_DEBUG) {
1927
+			$message .= '<br>' . print_r($final_prices_per_ticket_line_item, true);
1928
+			throw new OutOfRangeException($message);
1929
+		}
1930
+		EE_Log::instance()->log(__CLASS__, __FUNCTION__, $message);
1931
+		return null;
1932
+	}
1933
+
1934
+
1935
+	/**
1936
+	 * Creates a duplicate of the line item tree, except only includes billable items
1937
+	 * and the portion of line items attributed to billable things
1938
+	 *
1939
+	 * @param EE_Line_Item      $line_item
1940
+	 * @param EE_Registration[] $registrations
1941
+	 * @return EE_Line_Item
1942
+	 * @throws EE_Error
1943
+	 * @throws InvalidArgumentException
1944
+	 * @throws InvalidDataTypeException
1945
+	 * @throws InvalidInterfaceException
1946
+	 * @throws ReflectionException
1947
+	 */
1948
+	public static function billable_line_item_tree(EE_Line_Item $line_item, array $registrations): EE_Line_Item
1949
+	{
1950
+		$copy_li = EEH_Line_Item::billable_line_item($line_item, $registrations);
1951
+		foreach ($line_item->children() as $child_li) {
1952
+			$copy_li->add_child_line_item(
1953
+				EEH_Line_Item::billable_line_item_tree($child_li, $registrations)
1954
+			);
1955
+		}
1956
+		// if this is the grand total line item, make sure the totals all add up
1957
+		// (we could have duplicated this logic AS we copied the line items, but
1958
+		// it seems DRYer this way)
1959
+		if ($copy_li->type() === EEM_Line_Item::type_total) {
1960
+			$copy_li->recalculate_total_including_taxes();
1961
+		}
1962
+		return $copy_li;
1963
+	}
1964
+
1965
+
1966
+	/**
1967
+	 * Creates a new, unsaved line item from $line_item that factors in the
1968
+	 * number of billable registrations on $registrations.
1969
+	 *
1970
+	 * @param EE_Line_Item      $line_item
1971
+	 * @param EE_Registration[] $registrations
1972
+	 * @return EE_Line_Item
1973
+	 * @throws EE_Error
1974
+	 * @throws InvalidArgumentException
1975
+	 * @throws InvalidDataTypeException
1976
+	 * @throws InvalidInterfaceException
1977
+	 * @throws ReflectionException
1978
+	 */
1979
+	public static function billable_line_item(EE_Line_Item $line_item, array $registrations): EE_Line_Item
1980
+	{
1981
+		$new_li_fields = $line_item->model_field_array();
1982
+		if (
1983
+			$line_item->type() === EEM_Line_Item::type_line_item &&
1984
+			$line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET
1985
+		) {
1986
+			$count = 0;
1987
+			foreach ($registrations as $registration) {
1988
+				if (
1989
+					$line_item->OBJ_ID() === $registration->ticket_ID() &&
1990
+					in_array(
1991
+						$registration->status_ID(),
1992
+						EEM_Registration::reg_statuses_that_allow_payment(),
1993
+						true
1994
+					)
1995
+				) {
1996
+					$count++;
1997
+				}
1998
+			}
1999
+			$new_li_fields['LIN_quantity'] = $count;
2000
+		}
2001
+		// don't set the total. We'll leave that up to the code that calculates it
2002
+		unset($new_li_fields['LIN_ID'], $new_li_fields['LIN_parent'], $new_li_fields['LIN_total']);
2003
+		return EE_Line_Item::new_instance($new_li_fields);
2004
+	}
2005
+
2006
+
2007
+	/**
2008
+	 * Returns a modified line item tree where all the subtotals which have a total of 0
2009
+	 * are removed, and line items with a quantity of 0
2010
+	 *
2011
+	 * @param EE_Line_Item $line_item |null
2012
+	 * @return EE_Line_Item|null
2013
+	 * @throws EE_Error
2014
+	 * @throws InvalidArgumentException
2015
+	 * @throws InvalidDataTypeException
2016
+	 * @throws InvalidInterfaceException
2017
+	 * @throws ReflectionException
2018
+	 */
2019
+	public static function non_empty_line_items(EE_Line_Item $line_item): ?EE_Line_Item
2020
+	{
2021
+		$copied_li = EEH_Line_Item::non_empty_line_item($line_item);
2022
+		if ($copied_li === null) {
2023
+			return null;
2024
+		}
2025
+		// if this is an event subtotal, we want to only include it if it
2026
+		// has a non-zero total and at least one ticket line item child
2027
+		$ticket_children = 0;
2028
+		foreach ($line_item->children() as $child_li) {
2029
+			$child_li_copy = EEH_Line_Item::non_empty_line_items($child_li);
2030
+			if ($child_li_copy !== null) {
2031
+				$copied_li->add_child_line_item($child_li_copy);
2032
+				if (
2033
+					$child_li_copy->type() === EEM_Line_Item::type_line_item &&
2034
+					$child_li_copy->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET
2035
+				) {
2036
+					$ticket_children++;
2037
+				}
2038
+			}
2039
+		}
2040
+		// if this is an event subtotal with NO ticket children
2041
+		// we basically want to ignore it
2042
+		if (
2043
+			$ticket_children === 0
2044
+			&& $line_item->type() === EEM_Line_Item::type_sub_total
2045
+			&& $line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_EVENT
2046
+			&& $line_item->total() === 0.0
2047
+		) {
2048
+			return null;
2049
+		}
2050
+		return $copied_li;
2051
+	}
2052
+
2053
+
2054
+	/**
2055
+	 * Creates a new, unsaved line item, but if it's a ticket line item
2056
+	 * with a total of 0, or a subtotal of 0, returns null instead
2057
+	 *
2058
+	 * @param EE_Line_Item $line_item
2059
+	 * @return EE_Line_Item
2060
+	 * @throws EE_Error
2061
+	 * @throws InvalidArgumentException
2062
+	 * @throws InvalidDataTypeException
2063
+	 * @throws InvalidInterfaceException
2064
+	 * @throws ReflectionException
2065
+	 */
2066
+	public static function non_empty_line_item(EE_Line_Item $line_item): ?EE_Line_Item
2067
+	{
2068
+		if (
2069
+			$line_item->type() === EEM_Line_Item::type_line_item
2070
+			&& $line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET
2071
+			&& $line_item->quantity() === 0
2072
+		) {
2073
+			return null;
2074
+		}
2075
+		$new_li_fields = $line_item->model_field_array();
2076
+		// don't set the total. We'll leave that up to the code that calculates it
2077
+		unset($new_li_fields['LIN_ID'], $new_li_fields['LIN_parent']);
2078
+		return EE_Line_Item::new_instance($new_li_fields);
2079
+	}
2080
+
2081
+
2082
+	/**
2083
+	 * Cycles through all the ticket line items for the supplied total line item
2084
+	 * and ensures that the line item's "is_taxable" field matches that of its corresponding ticket
2085
+	 *
2086
+	 * @param EE_Line_Item $total_line_item
2087
+	 * @throws EE_Error
2088
+	 * @throws InvalidArgumentException
2089
+	 * @throws InvalidDataTypeException
2090
+	 * @throws InvalidInterfaceException
2091
+	 * @throws ReflectionException
2092
+	 * @since 4.9.79.p
2093
+	 */
2094
+	public static function resetIsTaxableForTickets(EE_Line_Item $total_line_item)
2095
+	{
2096
+		$ticket_line_items = self::get_ticket_line_items($total_line_item);
2097
+		foreach ($ticket_line_items as $ticket_line_item) {
2098
+			if (
2099
+				$ticket_line_item instanceof EE_Line_Item
2100
+				&& $ticket_line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET
2101
+			) {
2102
+				$ticket = $ticket_line_item->ticket();
2103
+				if ($ticket instanceof EE_Ticket && $ticket->taxable() !== $ticket_line_item->is_taxable()) {
2104
+					$ticket_line_item->set_is_taxable($ticket->taxable());
2105
+					$ticket_line_item->save();
2106
+				}
2107
+			}
2108
+		}
2109
+	}
2110
+
2111
+
2112
+	/**
2113
+	 * @return EE_Line_Item[]
2114
+	 * @throws EE_Error
2115
+	 * @throws ReflectionException
2116
+	 * @since   5.0.0.p
2117
+	 */
2118
+	private static function getGlobalTaxes(): array
2119
+	{
2120
+		if (EEH_Line_Item::$global_taxes === null) {
2121
+			/** @type EEM_Price $EEM_Price */
2122
+			$EEM_Price = EE_Registry::instance()->load_model('Price');
2123
+			// get array of taxes via Price Model
2124
+			EEH_Line_Item::$global_taxes = $EEM_Price->get_all_prices_that_are_taxes();
2125
+			ksort(EEH_Line_Item::$global_taxes);
2126
+		}
2127
+		return EEH_Line_Item::$global_taxes;
2128
+	}
2129
+
2130
+
2131
+
2132
+	/**************************************** @DEPRECATED METHODS *************************************** */
2133
+	/**
2134
+	 * @param EE_Line_Item $total_line_item
2135
+	 * @return EE_Line_Item
2136
+	 * @throws EE_Error
2137
+	 * @throws InvalidArgumentException
2138
+	 * @throws InvalidDataTypeException
2139
+	 * @throws InvalidInterfaceException
2140
+	 * @throws ReflectionException
2141
+	 * @deprecated
2142
+	 */
2143
+	public static function get_items_subtotal(EE_Line_Item $total_line_item): EE_Line_Item
2144
+	{
2145
+		EE_Error::doing_it_wrong(
2146
+			'EEH_Line_Item::get_items_subtotal()',
2147
+			sprintf(
2148
+				esc_html__('Method replaced with %1$s', 'event_espresso'),
2149
+				'EEH_Line_Item::get_pre_tax_subtotal()'
2150
+			),
2151
+			'4.6.0'
2152
+		);
2153
+		return self::get_pre_tax_subtotal($total_line_item);
2154
+	}
2155
+
2156
+
2157
+	/**
2158
+	 * @param EE_Transaction|null $transaction
2159
+	 * @return EE_Line_Item
2160
+	 * @throws EE_Error
2161
+	 * @throws InvalidArgumentException
2162
+	 * @throws InvalidDataTypeException
2163
+	 * @throws InvalidInterfaceException
2164
+	 * @throws ReflectionException
2165
+	 * @deprecated
2166
+	 */
2167
+	public static function create_default_total_line_item(?EE_Transaction $transaction = null): EE_Line_Item
2168
+	{
2169
+		EE_Error::doing_it_wrong(
2170
+			'EEH_Line_Item::create_default_total_line_item()',
2171
+			sprintf(
2172
+				esc_html__('Method replaced with %1$s', 'event_espresso'),
2173
+				'EEH_Line_Item::create_total_line_item()'
2174
+			),
2175
+			'4.6.0'
2176
+		);
2177
+		return self::create_total_line_item($transaction);
2178
+	}
2179
+
2180
+
2181
+	/**
2182
+	 * @param EE_Line_Item        $total_line_item
2183
+	 * @param EE_Transaction|null $transaction
2184
+	 * @return EE_Line_Item
2185
+	 * @throws EE_Error
2186
+	 * @throws InvalidArgumentException
2187
+	 * @throws InvalidDataTypeException
2188
+	 * @throws InvalidInterfaceException
2189
+	 * @throws ReflectionException
2190
+	 * @deprecated
2191
+	 */
2192
+	public static function create_default_tickets_subtotal(
2193
+		EE_Line_Item $total_line_item,
2194
+		?EE_Transaction $transaction = null
2195
+	): EE_Line_Item {
2196
+		EE_Error::doing_it_wrong(
2197
+			'EEH_Line_Item::create_default_tickets_subtotal()',
2198
+			sprintf(
2199
+				esc_html__('Method replaced with %1$s', 'event_espresso'),
2200
+				'EEH_Line_Item::create_pre_tax_subtotal()'
2201
+			),
2202
+			'4.6.0'
2203
+		);
2204
+		return self::create_pre_tax_subtotal($total_line_item, $transaction);
2205
+	}
2206
+
2207
+
2208
+	/**
2209
+	 * @param EE_Line_Item        $total_line_item
2210
+	 * @param EE_Transaction|null $transaction
2211
+	 * @return EE_Line_Item
2212
+	 * @throws EE_Error
2213
+	 * @throws InvalidArgumentException
2214
+	 * @throws InvalidDataTypeException
2215
+	 * @throws InvalidInterfaceException
2216
+	 * @throws ReflectionException
2217
+	 * @deprecated
2218
+	 */
2219
+	public static function create_default_taxes_subtotal(
2220
+		EE_Line_Item $total_line_item,
2221
+		?EE_Transaction $transaction = null
2222
+	): EE_Line_Item {
2223
+		EE_Error::doing_it_wrong(
2224
+			'EEH_Line_Item::create_default_taxes_subtotal()',
2225
+			sprintf(
2226
+				esc_html__('Method replaced with %1$s', 'event_espresso'),
2227
+				'EEH_Line_Item::create_taxes_subtotal()'
2228
+			),
2229
+			'4.6.0'
2230
+		);
2231
+		return self::create_taxes_subtotal($total_line_item, $transaction);
2232
+	}
2233
+
2234
+
2235
+	/**
2236
+	 * @param EE_Line_Item        $total_line_item
2237
+	 * @param EE_Transaction|null $transaction
2238
+	 * @return EE_Line_Item
2239
+	 * @throws EE_Error
2240
+	 * @throws InvalidArgumentException
2241
+	 * @throws InvalidDataTypeException
2242
+	 * @throws InvalidInterfaceException
2243
+	 * @throws ReflectionException
2244
+	 * @deprecated
2245
+	 */
2246
+	public static function create_default_event_subtotal(
2247
+		EE_Line_Item $total_line_item,
2248
+		?EE_Transaction $transaction = null
2249
+	): EE_Line_Item {
2250
+		EE_Error::doing_it_wrong(
2251
+			'EEH_Line_Item::create_default_event_subtotal()',
2252
+			sprintf(
2253
+				esc_html__('Method replaced with %1$s', 'event_espresso'),
2254
+				'EEH_Line_Item::create_event_subtotal()'
2255
+			),
2256
+			'4.6.0'
2257
+		);
2258
+		return self::create_event_subtotal($total_line_item, $transaction);
2259
+	}
2260 2260
 }
Please login to merge, or discard this patch.
Spacing   +47 added lines, -47 removed lines patch added patch discarded remove patch
@@ -24,7 +24,7 @@  discard block
 block discarded – undo
24 24
     /**
25 25
      * @var EE_Line_Item[]|null
26 26
      */
27
-    private static ?array $global_taxes = null;
27
+    private static ? array $global_taxes = null;
28 28
 
29 29
 
30 30
     /**
@@ -73,12 +73,12 @@  discard block
 block discarded – undo
73 73
                 'LIN_code'       => $code,
74 74
             ]
75 75
         );
76
-        $line_item      = apply_filters(
76
+        $line_item = apply_filters(
77 77
             'FHEE__EEH_Line_Item__add_unrelated_item__line_item',
78 78
             $line_item,
79 79
             $parent_line_item
80 80
         );
81
-        $added          = self::add_item($parent_line_item, $line_item, $recalculate_totals);
81
+        $added = self::add_item($parent_line_item, $line_item, $recalculate_totals);
82 82
         return $return_item ? $line_item : $added;
83 83
     }
84 84
 
@@ -131,7 +131,7 @@  discard block
 block discarded – undo
131 131
             'FHEE__EEH_Line_Item__add_percentage_based_item__line_item',
132 132
             $line_item
133 133
         );
134
-        $added     = $parent_line_item->add_child_line_item($line_item, false);
134
+        $added = $parent_line_item->add_child_line_item($line_item, false);
135 135
         return $return_item ? $line_item : $added;
136 136
     }
137 137
 
@@ -160,7 +160,7 @@  discard block
 block discarded – undo
160 160
         int $qty = 1,
161 161
         bool $recalculate_totals = true
162 162
     ): ?EE_Line_Item {
163
-        if (! $total_line_item instanceof EE_Line_Item || ! $total_line_item->is_total()) {
163
+        if ( ! $total_line_item instanceof EE_Line_Item || ! $total_line_item->is_total()) {
164 164
             throw new EE_Error(
165 165
                 sprintf(
166 166
                     esc_html__(
@@ -175,7 +175,7 @@  discard block
 block discarded – undo
175 175
         // either increment the qty for an existing ticket
176 176
         $line_item = self::increment_ticket_qty_if_already_in_cart($total_line_item, $ticket, $qty);
177 177
         // or add a new one
178
-        if (! $line_item instanceof EE_Line_Item) {
178
+        if ( ! $line_item instanceof EE_Line_Item) {
179 179
             $line_item = self::create_ticket_line_item($total_line_item, $ticket, $qty);
180 180
         }
181 181
         if ($recalculate_totals) {
@@ -239,7 +239,7 @@  discard block
 block discarded – undo
239 239
      */
240 240
     public static function increment_quantity(EE_Line_Item $line_item, int $qty = 1)
241 241
     {
242
-        if (! $line_item->is_percent()) {
242
+        if ( ! $line_item->is_percent()) {
243 243
             $qty += $line_item->quantity();
244 244
             $line_item->set_quantity($qty);
245 245
             $line_item->set_total($line_item->unit_price() * $qty);
@@ -268,7 +268,7 @@  discard block
 block discarded – undo
268 268
      */
269 269
     public static function decrement_quantity(EE_Line_Item $line_item, int $qty = 1)
270 270
     {
271
-        if (! $line_item->is_percent()) {
271
+        if ( ! $line_item->is_percent()) {
272 272
             $qty = $line_item->quantity() - $qty;
273 273
             $qty = max($qty, 0);
274 274
             $line_item->set_quantity($qty);
@@ -297,7 +297,7 @@  discard block
 block discarded – undo
297 297
      */
298 298
     public static function update_quantity(EE_Line_Item $line_item, int $new_quantity)
299 299
     {
300
-        if (! $line_item->is_percent()) {
300
+        if ( ! $line_item->is_percent()) {
301 301
             $line_item->set_quantity($new_quantity);
302 302
             $line_item->set_total($line_item->unit_price() * $new_quantity);
303 303
             $line_item->save();
@@ -342,7 +342,7 @@  discard block
 block discarded – undo
342 342
         $line_item = EE_Line_Item::new_instance(
343 343
             [
344 344
                 'LIN_name'       => $ticket->name(),
345
-                'LIN_desc'       => $ticket->description() !== '' ? $ticket->description() . ' ' . $event : $event,
345
+                'LIN_desc'       => $ticket->description() !== '' ? $ticket->description().' '.$event : $event,
346 346
                 'LIN_unit_price' => $ticket->price(),
347 347
                 'LIN_quantity'   => $qty,
348 348
                 'LIN_is_taxable' => empty($taxes) && $ticket->taxable(),
@@ -357,7 +357,7 @@  discard block
 block discarded – undo
357 357
             'FHEE__EEH_Line_Item__create_ticket_line_item__line_item',
358 358
             $line_item
359 359
         );
360
-        if (! $line_item instanceof EE_Line_Item) {
360
+        if ( ! $line_item instanceof EE_Line_Item) {
361 361
             throw new DomainException(
362 362
                 esc_html__('Invalid EE_Line_Item received.', 'event_espresso')
363 363
             );
@@ -396,7 +396,7 @@  discard block
 block discarded – undo
396 396
                 ? $price_type->name()
397 397
                 : $price_desc;
398 398
 
399
-            $sub_line_item         = EE_Line_Item::new_instance(
399
+            $sub_line_item = EE_Line_Item::new_instance(
400 400
                 [
401 401
                     'LIN_name'       => $price->name(),
402 402
                     'LIN_desc'       => $price_desc,
@@ -413,7 +413,7 @@  discard block
 block discarded – undo
413 413
                     'OBJ_type'       => EEM_Line_Item::OBJ_TYPE_PRICE,
414 414
                 ]
415 415
             );
416
-            $sub_line_item         = apply_filters(
416
+            $sub_line_item = apply_filters(
417 417
                 'FHEE__EEH_Line_Item__create_ticket_line_item__sub_line_item',
418 418
                 $sub_line_item
419 419
             );
@@ -530,7 +530,7 @@  discard block
 block discarded – undo
530 530
                             'event_espresso'
531 531
                         ),
532 532
                         $ticket_line_item->name(),
533
-                        current_time(get_option('date_format') . ' ' . get_option('time_format'))
533
+                        current_time(get_option('date_format').' '.get_option('time_format'))
534 534
                     ),
535 535
                     'LIN_total'      => 0,
536 536
                     'LIN_unit_price' => 0,
@@ -594,7 +594,7 @@  discard block
 block discarded – undo
594 594
         );
595 595
         $cancellation_line_item = reset($cancellation_line_item);
596 596
         // verify that this ticket was indeed previously cancelled
597
-        if (! $cancellation_line_item instanceof EE_Line_Item) {
597
+        if ( ! $cancellation_line_item instanceof EE_Line_Item) {
598 598
             return false;
599 599
         }
600 600
         if ($cancellation_line_item->quantity() > $qty) {
@@ -809,7 +809,7 @@  discard block
 block discarded – undo
809 809
                 'LIN_code'  => 'taxes',
810 810
                 'LIN_name'  => esc_html__('Taxes', 'event_espresso'),
811 811
                 'LIN_type'  => EEM_Line_Item::type_tax_sub_total,
812
-                'LIN_order' => 1000,// this should always come last
812
+                'LIN_order' => 1000, // this should always come last
813 813
             ]
814 814
         );
815 815
         $tax_line_item = apply_filters(
@@ -887,7 +887,7 @@  discard block
 block discarded – undo
887 887
      */
888 888
     public static function get_event_code(?EE_Event $event = null): string
889 889
     {
890
-        return 'event-' . ($event instanceof EE_Event ? $event->ID() : '0');
890
+        return 'event-'.($event instanceof EE_Event ? $event->ID() : '0');
891 891
     }
892 892
 
893 893
 
@@ -938,7 +938,7 @@  discard block
 block discarded – undo
938 938
     public static function get_event_line_item_for_ticket(EE_Line_Item $grand_total, EE_Ticket $ticket): EE_Line_Item
939 939
     {
940 940
         $first_datetime = $ticket->first_datetime();
941
-        if (! $first_datetime instanceof EE_Datetime) {
941
+        if ( ! $first_datetime instanceof EE_Datetime) {
942 942
             throw new EE_Error(
943 943
                 sprintf(
944 944
                     esc_html__('The supplied ticket (ID %d) has no datetimes', 'event_espresso'),
@@ -947,7 +947,7 @@  discard block
 block discarded – undo
947 947
             );
948 948
         }
949 949
         $event = $first_datetime->event();
950
-        if (! $event instanceof EE_Event) {
950
+        if ( ! $event instanceof EE_Event) {
951 951
             throw new EE_Error(
952 952
                 sprintf(
953 953
                     esc_html__(
@@ -959,7 +959,7 @@  discard block
 block discarded – undo
959 959
             );
960 960
         }
961 961
         $events_sub_total = EEH_Line_Item::get_event_line_item($grand_total, $event);
962
-        if (! $events_sub_total instanceof EE_Line_Item) {
962
+        if ( ! $events_sub_total instanceof EE_Line_Item) {
963 963
             throw new EE_Error(
964 964
                 sprintf(
965 965
                     esc_html__(
@@ -1000,20 +1000,20 @@  discard block
 block discarded – undo
1000 1000
                 // found existing line item for this event in the cart, so break out of loop and use this one
1001 1001
                 break;
1002 1002
             }
1003
-            if (! $OBJ_ID) {
1003
+            if ( ! $OBJ_ID) {
1004 1004
                 // let's use this! but first... set the event details
1005 1005
                 EEH_Line_Item::set_event_subtotal_details($event_line_item, $event);
1006 1006
                 break;
1007 1007
             }
1008 1008
         }
1009
-        if (! $event_line_item instanceof EE_Line_Item) {
1009
+        if ( ! $event_line_item instanceof EE_Line_Item) {
1010 1010
             // there is no event sub-total yet, so add it
1011 1011
             $pre_tax_subtotal = EEH_Line_Item::get_pre_tax_subtotal($total_line_item);
1012 1012
             // a new "event" subtotal SHOULD have been created
1013 1013
             $event_subtotals = EEH_Line_Item::get_event_subtotals($total_line_item);
1014 1014
             $event_line_item = reset($event_subtotals);
1015 1015
             // but in ccase one wasn't created for some reason...
1016
-            if (! $event_line_item instanceof EE_Line_Item) {
1016
+            if ( ! $event_line_item instanceof EE_Line_Item) {
1017 1017
                 // create a new "event" subtotal
1018 1018
                 $txn = $total_line_item->transaction();
1019 1019
                 $event_line_item = EEH_Line_Item::create_event_subtotal($pre_tax_subtotal, $txn, $event);
@@ -1093,7 +1093,7 @@  discard block
 block discarded – undo
1093 1093
                             break;
1094 1094
                         }
1095 1095
                     }
1096
-                    if (! $found) {
1096
+                    if ( ! $found) {
1097 1097
                         // add a new line item for this global tax
1098 1098
                         $tax_line_item = apply_filters(
1099 1099
                             'FHEE__EEH_Line_Item__apply_taxes__tax_line_item',
@@ -1111,7 +1111,7 @@  discard block
 block discarded – undo
1111 1111
                                 ]
1112 1112
                             )
1113 1113
                         );
1114
-                        $updates       = $taxes_line_item->add_child_line_item($tax_line_item) ? true : $updates;
1114
+                        $updates = $taxes_line_item->add_child_line_item($tax_line_item) ? true : $updates;
1115 1115
                     }
1116 1116
                 }
1117 1117
             }
@@ -1140,7 +1140,7 @@  discard block
 block discarded – undo
1140 1140
     public static function ensure_taxes_applied(?EE_Line_Item $total_line_item): float
1141 1141
     {
1142 1142
         $taxes_subtotal = self::get_taxes_subtotal($total_line_item);
1143
-        if (! $taxes_subtotal->children()) {
1143
+        if ( ! $taxes_subtotal->children()) {
1144 1144
             self::apply_taxes($total_line_item);
1145 1145
         }
1146 1146
         return $taxes_subtotal->total();
@@ -1206,7 +1206,7 @@  discard block
 block discarded – undo
1206 1206
         }
1207 1207
 
1208 1208
         // check if only a single line_item_id was passed
1209
-        if (! empty($line_item_codes) && ! is_array($line_item_codes)) {
1209
+        if ( ! empty($line_item_codes) && ! is_array($line_item_codes)) {
1210 1210
             // place single line_item_id in an array to appear as multiple line_item_ids
1211 1211
             $line_item_codes = [$line_item_codes];
1212 1212
         }
@@ -1316,7 +1316,7 @@  discard block
 block discarded – undo
1316 1316
         if ($code_substring_for_whitelist !== null) {
1317 1317
             $whitelisted = strpos($line_item->code(), $code_substring_for_whitelist) !== false;
1318 1318
         }
1319
-        if (! $whitelisted && $line_item->is_line_item()) {
1319
+        if ( ! $whitelisted && $line_item->is_line_item()) {
1320 1320
             $line_item->set_is_taxable($taxable);
1321 1321
         }
1322 1322
         foreach ($line_item->children() as $child_line_item) {
@@ -1698,7 +1698,7 @@  discard block
 block discarded – undo
1698 1698
      */
1699 1699
     public static function visualize(EE_Line_Item $line_item, int $indentation = 0, bool $top_level = true)
1700 1700
     {
1701
-        if (! defined('WP_DEBUG') || ! WP_DEBUG) {
1701
+        if ( ! defined('WP_DEBUG') || ! WP_DEBUG) {
1702 1702
             return;
1703 1703
         }
1704 1704
         $testing  = defined('EE_TESTS_DIR');
@@ -1797,7 +1797,7 @@  discard block
 block discarded – undo
1797 1797
                         $billable_ticket_quantities
1798 1798
                     );
1799 1799
                     // combine arrays but preserve numeric keys
1800
-                    $running_totals                     = array_replace_recursive(
1800
+                    $running_totals = array_replace_recursive(
1801 1801
                         $running_totals_from_subtotal,
1802 1802
                         $running_totals
1803 1803
                     );
@@ -1818,8 +1818,8 @@  discard block
 block discarded – undo
1818 1818
                         if ($line_item_id === 'taxable') {
1819 1819
                             continue;
1820 1820
                         }
1821
-                        $taxable_total                   = $running_totals['taxable'][ $line_item_id ];
1822
-                        $running_totals[ $line_item_id ] += ($taxable_total * $tax_percent_decimal);
1821
+                        $taxable_total = $running_totals['taxable'][$line_item_id];
1822
+                        $running_totals[$line_item_id] += ($taxable_total * $tax_percent_decimal);
1823 1823
                     }
1824 1824
                     break;
1825 1825
 
@@ -1827,7 +1827,7 @@  discard block
 block discarded – undo
1827 1827
                     // ticket line items or ????
1828 1828
                     if ($child_line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET) {
1829 1829
                         // kk it's a ticket
1830
-                        if (isset($running_totals[ $child_line_item->ID() ])) {
1830
+                        if (isset($running_totals[$child_line_item->ID()])) {
1831 1831
                             // huh? that shouldn't happen.
1832 1832
                             $running_totals['total'] += $child_line_item->total();
1833 1833
                         } else {
@@ -1838,19 +1838,19 @@  discard block
 block discarded – undo
1838 1838
                                 $taxable_amount = 0;
1839 1839
                             }
1840 1840
                             // are we only calculating totals for some tickets?
1841
-                            if (isset($billable_ticket_quantities[ $child_line_item->OBJ_ID() ])) {
1842
-                                $quantity = $billable_ticket_quantities[ $child_line_item->OBJ_ID() ];
1841
+                            if (isset($billable_ticket_quantities[$child_line_item->OBJ_ID()])) {
1842
+                                $quantity = $billable_ticket_quantities[$child_line_item->OBJ_ID()];
1843 1843
 
1844
-                                $running_totals[ $child_line_item->ID() ]            = $quantity
1844
+                                $running_totals[$child_line_item->ID()]            = $quantity
1845 1845
                                     ? $child_line_item->unit_price()
1846 1846
                                     : 0;
1847
-                                $running_totals['taxable'][ $child_line_item->ID() ] = $quantity
1847
+                                $running_totals['taxable'][$child_line_item->ID()] = $quantity
1848 1848
                                     ? $taxable_amount
1849 1849
                                     : 0;
1850 1850
                             } else {
1851 1851
                                 $quantity                                            = $child_line_item->quantity();
1852
-                                $running_totals[ $child_line_item->ID() ]            = $child_line_item->unit_price();
1853
-                                $running_totals['taxable'][ $child_line_item->ID() ] = $taxable_amount;
1852
+                                $running_totals[$child_line_item->ID()]            = $child_line_item->unit_price();
1853
+                                $running_totals['taxable'][$child_line_item->ID()] = $taxable_amount;
1854 1854
                             }
1855 1855
                             $running_totals['taxable']['total'] += $taxable_amount * $quantity;
1856 1856
                             $running_totals['total']            += $child_line_item->unit_price() * $quantity;
@@ -1870,12 +1870,12 @@  discard block
 block discarded – undo
1870 1870
                             }
1871 1871
                             // update the running totals
1872 1872
                             // yes this actually even works for the running grand total!
1873
-                            $running_totals[ $line_item_id ] =
1873
+                            $running_totals[$line_item_id] =
1874 1874
                                 $line_items_percent_of_running_total * $this_running_total;
1875 1875
 
1876 1876
                             if ($child_line_item->is_taxable()) {
1877
-                                $running_totals['taxable'][ $line_item_id ] =
1878
-                                    $line_items_percent_of_running_total * $running_totals['taxable'][ $line_item_id ];
1877
+                                $running_totals['taxable'][$line_item_id] =
1878
+                                    $line_items_percent_of_running_total * $running_totals['taxable'][$line_item_id];
1879 1879
                             }
1880 1880
                         }
1881 1881
                     }
@@ -1904,16 +1904,16 @@  discard block
 block discarded – undo
1904 1904
         static $final_prices_per_ticket_line_item = [];
1905 1905
         if (
1906 1906
             empty($final_prices_per_ticket_line_item)
1907
-            || empty($final_prices_per_ticket_line_item[ $total_line_item->ID() ])
1907
+            || empty($final_prices_per_ticket_line_item[$total_line_item->ID()])
1908 1908
         ) {
1909
-            $final_prices_per_ticket_line_item[ $total_line_item->ID() ] =
1909
+            $final_prices_per_ticket_line_item[$total_line_item->ID()] =
1910 1910
                 EEH_Line_Item::calculate_reg_final_prices_per_line_item(
1911 1911
                     $total_line_item
1912 1912
                 );
1913 1913
         }
1914 1914
         // ok now find this new registration's final price
1915
-        if (isset($final_prices_per_ticket_line_item[ $total_line_item->ID() ][ $ticket_line_item->ID() ])) {
1916
-            return $final_prices_per_ticket_line_item[ $total_line_item->ID() ][ $ticket_line_item->ID() ];
1915
+        if (isset($final_prices_per_ticket_line_item[$total_line_item->ID()][$ticket_line_item->ID()])) {
1916
+            return $final_prices_per_ticket_line_item[$total_line_item->ID()][$ticket_line_item->ID()];
1917 1917
         }
1918 1918
         $message = sprintf(
1919 1919
             esc_html__(
@@ -1924,7 +1924,7 @@  discard block
 block discarded – undo
1924 1924
             $total_line_item->ID()
1925 1925
         );
1926 1926
         if (WP_DEBUG) {
1927
-            $message .= '<br>' . print_r($final_prices_per_ticket_line_item, true);
1927
+            $message .= '<br>'.print_r($final_prices_per_ticket_line_item, true);
1928 1928
             throw new OutOfRangeException($message);
1929 1929
         }
1930 1930
         EE_Log::instance()->log(__CLASS__, __FUNCTION__, $message);
Please login to merge, or discard this patch.
core/helpers/EEH_Sideloader.helper.php 1 patch
Indentation   +337 added lines, -337 removed lines patch added patch discarded remove patch
@@ -13,341 +13,341 @@
 block discarded – undo
13 13
  */
14 14
 class EEH_Sideloader extends EEH_Base
15 15
 {
16
-    /**
17
-     * @since   4.1.0
18
-     * @var     string
19
-     */
20
-    private $_upload_to;
21
-
22
-    /**
23
-     * @since   4.10.5.p
24
-     * @var     string
25
-     */
26
-    private $_download_from;
27
-
28
-    /**
29
-     * @since   4.1.0
30
-     * @var     int|string
31
-     */
32
-    private $_permissions;
33
-
34
-    /**
35
-     * @since   4.1.0
36
-     * @var     string
37
-     */
38
-    private $_new_file_name;
39
-
40
-
41
-    /**
42
-     * constructor allows the user to set the properties on the sideloader on construction.
43
-     * However, there are also setters for doing so.
44
-     *
45
-     * @param array $init array fo initializing the sideloader if keys match the properties.
46
-     * @since 4.1.0
47
-     */
48
-    public function __construct(array $init = [])
49
-    {
50
-        $this->_init($init);
51
-    }
52
-
53
-
54
-    /**
55
-     * sets the properties for class either to defaults or using incoming initialization array
56
-     *
57
-     * @param array $props values passed to setters
58
-     * @return void
59
-     * @since 4.1.0
60
-     */
61
-    private function _init(array $props)
62
-    {
63
-        $props += [
64
-            '_upload_to'     => $this->_get_wp_uploads_dir(),
65
-            '_download_from' => '',
66
-            '_permissions'   => 0644,
67
-            '_new_file_name' => 'EE_Sideloader_' . uniqid() . '.default',
68
-        ];
69
-
70
-        $this->set_upload_to($props['_upload_to']);
71
-        $this->set_download_from($props['_download_from']);
72
-        $this->set_permissions($props['_permissions']);
73
-        $this->set_new_file_name($props['_new_file_name']);
74
-
75
-        // make sure we include the required wp file for needed functions
76
-        require_once(ABSPATH . 'wp-admin/includes/file.php');
77
-    }
78
-
79
-
80
-    // utilities
81
-
82
-
83
-    /**
84
-     * @return string
85
-     * @since 4.1.0
86
-     */
87
-    private function _get_wp_uploads_dir(): string
88
-    {
89
-        $uploads = wp_upload_dir();
90
-        return $uploads['basedir'];
91
-    }
92
-
93
-    // setters
94
-
95
-
96
-    /**
97
-     * sets the _upload_to property to the directory to upload to.
98
-     *
99
-     * @param string $upload_to_folder
100
-     * @return void
101
-     * @since 4.1.0
102
-     */
103
-    public function set_upload_to(string $upload_to_folder)
104
-    {
105
-        $this->_upload_to = trailingslashit($upload_to_folder);
106
-    }
107
-
108
-
109
-    /**
110
-     * sets the _download_from property to the location we should download the file from.
111
-     *
112
-     * @param string $download_from The full path to the file we should sideload.
113
-     * @return void
114
-     * @since 4.10.5.p
115
-     */
116
-    public function set_download_from(string $download_from)
117
-    {
118
-        $this->_download_from = $download_from;
119
-    }
120
-
121
-
122
-    /**
123
-     * sets the _permissions property used on the sideloaded file.
124
-     *
125
-     * @param int|string $permissions
126
-     * @return void
127
-     * @since 4.1.0
128
-     */
129
-    public function set_permissions($permissions)
130
-    {
131
-        $this->_permissions = $permissions;
132
-    }
133
-
134
-
135
-    /**
136
-     * sets the _new_file_name property used on the sideloaded file.
137
-     *
138
-     * @param string $new_file_name
139
-     * @return void
140
-     * @since 4.1.0
141
-     */
142
-    public function set_new_file_name(string $new_file_name)
143
-    {
144
-        $this->_new_file_name = $new_file_name;
145
-    }
146
-
147
-    // getters
148
-
149
-
150
-    /**
151
-     * @return string
152
-     * @since 4.1.0
153
-     */
154
-    public function get_upload_to(): string
155
-    {
156
-        return $this->_upload_to;
157
-    }
158
-
159
-
160
-    /**
161
-     * @return string
162
-     * @since 4.10.5.p
163
-     */
164
-    public function get_download_from(): string
165
-    {
166
-        return $this->_download_from;
167
-    }
168
-
169
-
170
-    /**
171
-     * @return int|string
172
-     * @since 4.1.0
173
-     */
174
-    public function get_permissions()
175
-    {
176
-        return $this->_permissions;
177
-    }
178
-
179
-
180
-    /**
181
-     * @return string
182
-     * @since 4.1.0
183
-     */
184
-    public function get_new_file_name(): string
185
-    {
186
-        return $this->_new_file_name;
187
-    }
188
-
189
-
190
-    // upload methods
191
-
192
-
193
-    /**
194
-     * Downloads the file using the WordPress HTTP API.
195
-     *
196
-     * @return bool
197
-     * @since 4.1.0
198
-     */
199
-    public function sideload(): bool
200
-    {
201
-        try {
202
-            // setup temp dir
203
-            $temp_file = wp_tempnam($this->_download_from);
204
-
205
-            if (! $temp_file) {
206
-                throw new RuntimeException(
207
-                    esc_html__(
208
-                        'Something went wrong with the upload.  Unable to create a tmp file for the uploaded file on the server',
209
-                        'event_espresso'
210
-                    )
211
-                );
212
-            }
213
-
214
-            do_action('AHEE__EEH_Sideloader__sideload__before', $this, $temp_file);
215
-
216
-            $wp_remote_args = apply_filters(
217
-                'FHEE__EEH_Sideloader__sideload__wp_remote_args',
218
-                ['timeout' => 500, 'stream' => true, 'filename' => $temp_file],
219
-                $this,
220
-                $temp_file
221
-            );
222
-
223
-            $response = wp_safe_remote_get($this->_download_from, $wp_remote_args);
224
-
225
-            if ($this->isResponseError($response) || $this->isDownloadError($response)) {
226
-                EEH_File::delete($temp_file);
227
-                return false;
228
-            }
229
-
230
-            // possible md5 check
231
-            $content_md5 = wp_remote_retrieve_header($response, 'content-md5');
232
-            if ($content_md5) {
233
-                $md5_check = verify_file_md5($temp_file, $content_md5);
234
-                if ($this->isResponseError($md5_check)) {
235
-                    EEH_File::delete($temp_file);
236
-                    return false;
237
-                }
238
-            }
239
-
240
-            // now we have the file, let's get it in the right directory with the right name.
241
-            $path = apply_filters(
242
-                'FHEE__EEH_Sideloader__sideload__new_path',
243
-                $this->_upload_to . $this->_new_file_name,
244
-                $this
245
-            );
246
-            if (! EEH_File::move($temp_file, $path, true)) {
247
-                return false;
248
-            }
249
-
250
-            // set permissions
251
-            $permissions = apply_filters(
252
-                'FHEE__EEH_Sideloader__sideload__permissions_applied',
253
-                $this->_permissions,
254
-                $this
255
-            );
256
-            // verify permissions are an integer but don't actually modify the value
257
-            if (! absint($permissions)) {
258
-                EE_Error::add_error(
259
-                    esc_html__('Supplied permissions are invalid', 'event_espresso'),
260
-                    __FILE__,
261
-                    __FUNCTION__,
262
-                    __LINE__
263
-                );
264
-                return false;
265
-            }
266
-            EEH_File::chmod($path, $permissions);
267
-
268
-            // that's it.  let's allow for actions after file uploaded.
269
-            do_action('AHEE__EE_Sideloader__sideload_after', $this, $path);
270
-            return true;
271
-        } catch (Exception $exception) {
272
-            EE_Error::add_error($exception->getMessage(), __FILE__, __FUNCTION__, __LINE__);
273
-            return false;
274
-        }
275
-    }
276
-
277
-
278
-    /**
279
-     * returns TRUE if there IS an error, FALSE if there is NO ERROR
280
-     *
281
-     * @param array|WP_Error $response
282
-     * @return bool
283
-     * @throws RuntimeException
284
-     */
285
-    private function isResponseError($response): bool
286
-    {
287
-        if (! is_wp_error($response)) {
288
-            return false;
289
-        }
290
-        if (defined('WP_DEBUG') && WP_DEBUG) {
291
-            EE_Error::add_error(
292
-                sprintf(
293
-                    esc_html__(
294
-                        'The following error occurred while attempting to download the file from "%1$s":',
295
-                        'event_espresso'
296
-                    ),
297
-                    $this->_download_from,
298
-                    $response->get_error_message()
299
-                ),
300
-                __FILE__,
301
-                __FUNCTION__,
302
-                __LINE__
303
-            );
304
-        }
305
-        return true;
306
-    }
307
-
308
-
309
-    /**
310
-     * returns TRUE if there IS an error, FALSE if there is NO ERROR
311
-     *
312
-     * @param array $response
313
-     * @return bool
314
-     * @throws RuntimeException
315
-     */
316
-    private function isDownloadError(array $response): bool
317
-    {
318
-        $response_code = wp_remote_retrieve_response_code($response);
319
-        if ($response_code === 200) {
320
-            return false;
321
-        }
322
-        if (defined('WP_DEBUG') && WP_DEBUG && ! defined('EE_TESTS_DIR')) {
323
-            if ($response_code === 404) {
324
-                EE_Error::add_attention(
325
-                    sprintf(
326
-                        esc_html__(
327
-                            'Attempted to download a file from "%1$s" but encountered a "404 File Not Found" error.',
328
-                            'event_espresso'
329
-                        ),
330
-                        $this->_download_from
331
-                    ),
332
-                    __FILE__,
333
-                    __FUNCTION__,
334
-                    __LINE__
335
-                );
336
-            } else {
337
-                EE_Error::add_error(
338
-                    sprintf(
339
-                        esc_html__(
340
-                            'Unable to download the file. Either the path given is incorrect, or something else happened. Here is the path given: %s',
341
-                            'event_espresso'
342
-                        ),
343
-                        $this->_download_from
344
-                    ),
345
-                    __FILE__,
346
-                    __FUNCTION__,
347
-                    __LINE__
348
-                );
349
-            }
350
-        }
351
-        return true;
352
-    }
16
+	/**
17
+	 * @since   4.1.0
18
+	 * @var     string
19
+	 */
20
+	private $_upload_to;
21
+
22
+	/**
23
+	 * @since   4.10.5.p
24
+	 * @var     string
25
+	 */
26
+	private $_download_from;
27
+
28
+	/**
29
+	 * @since   4.1.0
30
+	 * @var     int|string
31
+	 */
32
+	private $_permissions;
33
+
34
+	/**
35
+	 * @since   4.1.0
36
+	 * @var     string
37
+	 */
38
+	private $_new_file_name;
39
+
40
+
41
+	/**
42
+	 * constructor allows the user to set the properties on the sideloader on construction.
43
+	 * However, there are also setters for doing so.
44
+	 *
45
+	 * @param array $init array fo initializing the sideloader if keys match the properties.
46
+	 * @since 4.1.0
47
+	 */
48
+	public function __construct(array $init = [])
49
+	{
50
+		$this->_init($init);
51
+	}
52
+
53
+
54
+	/**
55
+	 * sets the properties for class either to defaults or using incoming initialization array
56
+	 *
57
+	 * @param array $props values passed to setters
58
+	 * @return void
59
+	 * @since 4.1.0
60
+	 */
61
+	private function _init(array $props)
62
+	{
63
+		$props += [
64
+			'_upload_to'     => $this->_get_wp_uploads_dir(),
65
+			'_download_from' => '',
66
+			'_permissions'   => 0644,
67
+			'_new_file_name' => 'EE_Sideloader_' . uniqid() . '.default',
68
+		];
69
+
70
+		$this->set_upload_to($props['_upload_to']);
71
+		$this->set_download_from($props['_download_from']);
72
+		$this->set_permissions($props['_permissions']);
73
+		$this->set_new_file_name($props['_new_file_name']);
74
+
75
+		// make sure we include the required wp file for needed functions
76
+		require_once(ABSPATH . 'wp-admin/includes/file.php');
77
+	}
78
+
79
+
80
+	// utilities
81
+
82
+
83
+	/**
84
+	 * @return string
85
+	 * @since 4.1.0
86
+	 */
87
+	private function _get_wp_uploads_dir(): string
88
+	{
89
+		$uploads = wp_upload_dir();
90
+		return $uploads['basedir'];
91
+	}
92
+
93
+	// setters
94
+
95
+
96
+	/**
97
+	 * sets the _upload_to property to the directory to upload to.
98
+	 *
99
+	 * @param string $upload_to_folder
100
+	 * @return void
101
+	 * @since 4.1.0
102
+	 */
103
+	public function set_upload_to(string $upload_to_folder)
104
+	{
105
+		$this->_upload_to = trailingslashit($upload_to_folder);
106
+	}
107
+
108
+
109
+	/**
110
+	 * sets the _download_from property to the location we should download the file from.
111
+	 *
112
+	 * @param string $download_from The full path to the file we should sideload.
113
+	 * @return void
114
+	 * @since 4.10.5.p
115
+	 */
116
+	public function set_download_from(string $download_from)
117
+	{
118
+		$this->_download_from = $download_from;
119
+	}
120
+
121
+
122
+	/**
123
+	 * sets the _permissions property used on the sideloaded file.
124
+	 *
125
+	 * @param int|string $permissions
126
+	 * @return void
127
+	 * @since 4.1.0
128
+	 */
129
+	public function set_permissions($permissions)
130
+	{
131
+		$this->_permissions = $permissions;
132
+	}
133
+
134
+
135
+	/**
136
+	 * sets the _new_file_name property used on the sideloaded file.
137
+	 *
138
+	 * @param string $new_file_name
139
+	 * @return void
140
+	 * @since 4.1.0
141
+	 */
142
+	public function set_new_file_name(string $new_file_name)
143
+	{
144
+		$this->_new_file_name = $new_file_name;
145
+	}
146
+
147
+	// getters
148
+
149
+
150
+	/**
151
+	 * @return string
152
+	 * @since 4.1.0
153
+	 */
154
+	public function get_upload_to(): string
155
+	{
156
+		return $this->_upload_to;
157
+	}
158
+
159
+
160
+	/**
161
+	 * @return string
162
+	 * @since 4.10.5.p
163
+	 */
164
+	public function get_download_from(): string
165
+	{
166
+		return $this->_download_from;
167
+	}
168
+
169
+
170
+	/**
171
+	 * @return int|string
172
+	 * @since 4.1.0
173
+	 */
174
+	public function get_permissions()
175
+	{
176
+		return $this->_permissions;
177
+	}
178
+
179
+
180
+	/**
181
+	 * @return string
182
+	 * @since 4.1.0
183
+	 */
184
+	public function get_new_file_name(): string
185
+	{
186
+		return $this->_new_file_name;
187
+	}
188
+
189
+
190
+	// upload methods
191
+
192
+
193
+	/**
194
+	 * Downloads the file using the WordPress HTTP API.
195
+	 *
196
+	 * @return bool
197
+	 * @since 4.1.0
198
+	 */
199
+	public function sideload(): bool
200
+	{
201
+		try {
202
+			// setup temp dir
203
+			$temp_file = wp_tempnam($this->_download_from);
204
+
205
+			if (! $temp_file) {
206
+				throw new RuntimeException(
207
+					esc_html__(
208
+						'Something went wrong with the upload.  Unable to create a tmp file for the uploaded file on the server',
209
+						'event_espresso'
210
+					)
211
+				);
212
+			}
213
+
214
+			do_action('AHEE__EEH_Sideloader__sideload__before', $this, $temp_file);
215
+
216
+			$wp_remote_args = apply_filters(
217
+				'FHEE__EEH_Sideloader__sideload__wp_remote_args',
218
+				['timeout' => 500, 'stream' => true, 'filename' => $temp_file],
219
+				$this,
220
+				$temp_file
221
+			);
222
+
223
+			$response = wp_safe_remote_get($this->_download_from, $wp_remote_args);
224
+
225
+			if ($this->isResponseError($response) || $this->isDownloadError($response)) {
226
+				EEH_File::delete($temp_file);
227
+				return false;
228
+			}
229
+
230
+			// possible md5 check
231
+			$content_md5 = wp_remote_retrieve_header($response, 'content-md5');
232
+			if ($content_md5) {
233
+				$md5_check = verify_file_md5($temp_file, $content_md5);
234
+				if ($this->isResponseError($md5_check)) {
235
+					EEH_File::delete($temp_file);
236
+					return false;
237
+				}
238
+			}
239
+
240
+			// now we have the file, let's get it in the right directory with the right name.
241
+			$path = apply_filters(
242
+				'FHEE__EEH_Sideloader__sideload__new_path',
243
+				$this->_upload_to . $this->_new_file_name,
244
+				$this
245
+			);
246
+			if (! EEH_File::move($temp_file, $path, true)) {
247
+				return false;
248
+			}
249
+
250
+			// set permissions
251
+			$permissions = apply_filters(
252
+				'FHEE__EEH_Sideloader__sideload__permissions_applied',
253
+				$this->_permissions,
254
+				$this
255
+			);
256
+			// verify permissions are an integer but don't actually modify the value
257
+			if (! absint($permissions)) {
258
+				EE_Error::add_error(
259
+					esc_html__('Supplied permissions are invalid', 'event_espresso'),
260
+					__FILE__,
261
+					__FUNCTION__,
262
+					__LINE__
263
+				);
264
+				return false;
265
+			}
266
+			EEH_File::chmod($path, $permissions);
267
+
268
+			// that's it.  let's allow for actions after file uploaded.
269
+			do_action('AHEE__EE_Sideloader__sideload_after', $this, $path);
270
+			return true;
271
+		} catch (Exception $exception) {
272
+			EE_Error::add_error($exception->getMessage(), __FILE__, __FUNCTION__, __LINE__);
273
+			return false;
274
+		}
275
+	}
276
+
277
+
278
+	/**
279
+	 * returns TRUE if there IS an error, FALSE if there is NO ERROR
280
+	 *
281
+	 * @param array|WP_Error $response
282
+	 * @return bool
283
+	 * @throws RuntimeException
284
+	 */
285
+	private function isResponseError($response): bool
286
+	{
287
+		if (! is_wp_error($response)) {
288
+			return false;
289
+		}
290
+		if (defined('WP_DEBUG') && WP_DEBUG) {
291
+			EE_Error::add_error(
292
+				sprintf(
293
+					esc_html__(
294
+						'The following error occurred while attempting to download the file from "%1$s":',
295
+						'event_espresso'
296
+					),
297
+					$this->_download_from,
298
+					$response->get_error_message()
299
+				),
300
+				__FILE__,
301
+				__FUNCTION__,
302
+				__LINE__
303
+			);
304
+		}
305
+		return true;
306
+	}
307
+
308
+
309
+	/**
310
+	 * returns TRUE if there IS an error, FALSE if there is NO ERROR
311
+	 *
312
+	 * @param array $response
313
+	 * @return bool
314
+	 * @throws RuntimeException
315
+	 */
316
+	private function isDownloadError(array $response): bool
317
+	{
318
+		$response_code = wp_remote_retrieve_response_code($response);
319
+		if ($response_code === 200) {
320
+			return false;
321
+		}
322
+		if (defined('WP_DEBUG') && WP_DEBUG && ! defined('EE_TESTS_DIR')) {
323
+			if ($response_code === 404) {
324
+				EE_Error::add_attention(
325
+					sprintf(
326
+						esc_html__(
327
+							'Attempted to download a file from "%1$s" but encountered a "404 File Not Found" error.',
328
+							'event_espresso'
329
+						),
330
+						$this->_download_from
331
+					),
332
+					__FILE__,
333
+					__FUNCTION__,
334
+					__LINE__
335
+				);
336
+			} else {
337
+				EE_Error::add_error(
338
+					sprintf(
339
+						esc_html__(
340
+							'Unable to download the file. Either the path given is incorrect, or something else happened. Here is the path given: %s',
341
+							'event_espresso'
342
+						),
343
+						$this->_download_from
344
+					),
345
+					__FILE__,
346
+					__FUNCTION__,
347
+					__LINE__
348
+				);
349
+			}
350
+		}
351
+		return true;
352
+	}
353 353
 }
Please login to merge, or discard this patch.
core/EE_Maintenance_Mode.core.php 2 patches
Indentation   +512 added lines, -512 removed lines patch added patch discarded remove patch
@@ -19,524 +19,524 @@
 block discarded – undo
19 19
  */
20 20
 class EE_Maintenance_Mode implements ResettableInterface
21 21
 {
22
-    /**
23
-     * constants available to client code for interpreting the values of EE_Maintenance_Mode::level().
24
-     * STATUS_OFF means the site is NOT in maintenance mode (so everything's normal)
25
-     */
26
-    public const STATUS_OFF = 0;
27
-
28
-
29
-    /**
30
-     * STATUS_PUBLIC_ONLY means that the site's frontend EE code should be completely disabled
31
-     * but the admin backend should be running as normal. Maybe an admin can view the frontend though
32
-     */
33
-    public const STATUS_PUBLIC_ONLY = 1;
34
-
35
-    /**
36
-     * STATUS_FULL_SITE means the frontend AND EE backend code are disabled. The only system running
37
-     * is the maintenance mode stuff, which will require users to update all addons, and then finish running all
38
-     * migration scripts before taking the site out of maintenance mode
39
-     */
40
-    public const STATUS_FULL_SITE = 2;
41
-
42
-    /**
43
-     * the name of the option which stores the current level of maintenance mode
44
-     */
45
-    private const OPTION_NAME = 'ee_maintenance_mode';
46
-
47
-
48
-    protected LoaderInterface $loader;
49
-
50
-    private RequestInterface $request;
51
-
52
-    private static ?EE_Maintenance_Mode $_instance = null;
53
-
54
-    /**
55
-     * @var int
56
-     * @since 5.0.12.p
57
-     */
58
-    private int $status;
59
-
60
-    /**
61
-     * @var int
62
-     * @since 5.0.12.p
63
-     */
64
-    private int $admin_status;
65
-
66
-    /**
67
-     * true if current_user_can('administrator')
68
-     *
69
-     * @var bool
70
-     * @since 5.0.12.p
71
-     */
72
-    private bool $current_user_is_admin;
73
-
74
-    /**
75
-     * used to control updates to the WP options setting in the database
76
-     *
77
-     * @var bool
78
-     * @since 5.0.12.p
79
-     */
80
-    private bool $update_db;
81
-
82
-
83
-    /**
84
-     * @singleton method used to instantiate class object
85
-     * @param LoaderInterface|null  $loader
86
-     * @param RequestInterface|null $request
87
-     * @return EE_Maintenance_Mode|null
88
-     */
89
-    public static function instance(
90
-        ?LoaderInterface $loader = null,
91
-        ?RequestInterface $request = null
92
-    ): ?EE_Maintenance_Mode {
93
-        // check if class object is instantiated
94
-        if (! self::$_instance instanceof EE_Maintenance_Mode) {
95
-            self::$_instance = new EE_Maintenance_Mode($loader, $request);
96
-        }
97
-        return self::$_instance;
98
-    }
99
-
100
-
101
-    /**
102
-     * Resets maintenance mode (mostly just re-checks whether we should be in maintenance mode)
103
-     *
104
-     * @return EE_Maintenance_Mode|null
105
-     * @throws EE_Error
106
-     */
107
-    public static function reset(): ?EE_Maintenance_Mode
108
-    {
109
-        self::instance()->set_maintenance_mode_if_db_old();
110
-        self::instance()->initialize();
111
-        return self::instance();
112
-    }
113
-
114
-
115
-    /**
116
-     *private constructor to prevent direct creation
117
-     */
118
-    private function __construct(LoaderInterface $loader, RequestInterface $request)
119
-    {
120
-        $this->loader                = $loader;
121
-        $this->request               = $request;
122
-        $this->initialize();
123
-
124
-        // if M-Mode level 2 is engaged, we still need basic assets loaded
125
-        add_action('wp_enqueue_scripts', [$this, 'load_assets_required_for_m_mode']);
126
-        // shut 'er down for maintenance ?
127
-        add_filter('the_content', [$this, 'the_content'], 2);
128
-        // redirect ee menus to maintenance page
129
-        add_action('admin_page_access_denied', [$this, 'redirect_to_maintenance']);
130
-        // add powered by EE msg
131
-        add_action('shutdown', [$this, 'display_maintenance_mode_notice']);
132
-    }
133
-
134
-
135
-    private function initialize(): void
136
-    {
137
-        $this->current_user_is_admin = current_user_can('administrator');
138
-        // now make sure the status is set correctly everywhere
139
-        // (but don't update the db else we'll get into an infinite loop of updates)
140
-        $this->update_db = false;
141
-        $this->set_maintenance_level($this->loadStatusFromDatabase());
142
-        $this->update_db = true;
143
-    }
144
-
145
-
146
-    private function loadStatusFromDatabase(): int
147
-    {
148
-        return (int) get_option(EE_Maintenance_Mode::OPTION_NAME, EE_Maintenance_Mode::STATUS_OFF);
149
-    }
150
-
151
-
152
-    /**
153
-     * changes the maintenance mode level to reflect whether the current user is an admin or not.
154
-     * Determines whether we're in maintenance mode and what level. However, while the site
155
-     * is in level 1 maintenance, and an admin visits the frontend, this function makes it appear
156
-     * to them as if the site isn't in maintenance mode.
157
-     *      EE_Maintenance_Mode::STATUS_OFF => not in maintenance mode (in normal mode)
158
-     *      EE_Maintenance_Mode::STATUS_PUBLIC_ONLY=> frontend-only maintenance mode
159
-     *      EE_Maintenance_Mode::STATUS_FULL_SITE => frontend and backend maintenance mode
160
-     *
161
-     * @param int $status
162
-     * @return void
163
-     * @since 5.0.12.p
164
-     */
165
-    private function setAdminStatus(int $status)
166
-    {
167
-        if (
168
-            $status === EE_Maintenance_Mode::STATUS_PUBLIC_ONLY
169
-            && $this->current_user_is_admin
170
-            && ($this->request->isAjax() || ! $this->request->isAdmin())
171
-        ) {
172
-            $status = EE_Maintenance_Mode::STATUS_OFF;
173
-        }
174
-        $this->admin_status = $status;
175
-    }
176
-
177
-
178
-    public function real_level(): int
179
-    {
180
-        return $this->status;
181
-    }
182
-
183
-
184
-    /**
185
-     * @return int
186
-     */
187
-    public function level(): int
188
-    {
189
-        return $this->admin_status;
190
-    }
191
-
192
-
193
-    /**
194
-     * Determines if we need to put EE in maintenance mode because the database needs updating
195
-     *
196
-     * @return boolean true if DB is old and maintenance mode was triggered; false otherwise
197
-     * @throws EE_Error
198
-     */
199
-    public function set_maintenance_mode_if_db_old(): bool
200
-    {
201
-        /** @var EE_Data_Migration_Manager $data_migration_manager */
202
-        $data_migration_manager = $this->loader->getShared(EE_Data_Migration_Manager::class );
203
-        $scripts_that_should_run = $data_migration_manager->check_for_applicable_data_migration_scripts();
204
-        if (! empty($scripts_that_should_run)) { //  && $this->status !== EE_Maintenance_Mode::STATUS_FULL_SITE
205
-            $this->activateFullSiteMaintenanceMode();
206
-            return true;
207
-        }
208
-        if ($this->status === EE_Maintenance_Mode::STATUS_FULL_SITE) {
209
-            // we also want to handle the opposite: if the site is mm2, but there aren't any migrations to run
210
-            // then we shouldn't be in mm2. (Maybe an addon got deactivated?)
211
-            $this->deactivateMaintenanceMode();
212
-        }
213
-        return false;
214
-    }
215
-
216
-
217
-    /**
218
-     * Updates the maintenance level on the site
219
-     *
220
-     * @param int $level
221
-     * @return void
222
-     */
223
-    public function set_maintenance_level(int $level): void
224
-    {
225
-        switch ($level) {
226
-            case EE_Maintenance_Mode::STATUS_OFF:
227
-                $this->deactivateMaintenanceMode();
228
-                return;
229
-            case EE_Maintenance_Mode::STATUS_PUBLIC_ONLY:
230
-                $this->activatePublicOnlyMaintenanceMode();
231
-                return;
232
-            case EE_Maintenance_Mode::STATUS_FULL_SITE:
233
-                $this->activateFullSiteMaintenanceMode();
234
-                return;
235
-        }
236
-        throw new DomainException(
237
-            sprintf(
238
-                esc_html__(
239
-                    '"%1$s" is not valid a EE maintenance mode level. Please choose from one of the following: %2$s',
240
-                    'event_espresso'
241
-                ),
242
-                $level,
243
-                'EE_Maintenance_Mode::STATUS_OFF, EE_Maintenance_Mode::STATUS_PUBLIC_ONLY, EE_Maintenance_Mode::STATUS_FULL_SITE',
244
-            )
245
-        );
246
-    }
247
-
248
-
249
-    /**
250
-     * sets database status to online
251
-     * sets maintenance mode status to public only, unless current user is an admin, then maintenance mode is disabled
252
-     *
253
-     * @return void
254
-     * @since 5.0.12.p
255
-     */
256
-    public function activatePublicOnlyMaintenanceMode()
257
-    {
258
-        DbStatus::setOnline();
259
-        // disable maintenance mode for admins, otherwise enable public only maintenance mode
260
-        if ($this->current_user_is_admin) {
261
-            MaintenanceStatus::disableMaintenanceMode();
262
-        } else {
263
-            MaintenanceStatus::setPublicOnlyMaintenanceMode();
264
-        }
265
-        $this->updateMaintenaceModeStatus(EE_Maintenance_Mode::STATUS_PUBLIC_ONLY);
266
-    }
267
-
268
-
269
-    /**
270
-     * sets database status to offline
271
-     * sets maintenance mode status to full site
272
-     *
273
-     * @return void
274
-     * @since 5.0.12.p
275
-     */
276
-    public function activateFullSiteMaintenanceMode()
277
-    {
278
-        DbStatus::setOffline();
279
-        MaintenanceStatus::setFullSiteMaintenanceMode();
280
-        $this->updateMaintenaceModeStatus(EE_Maintenance_Mode::STATUS_FULL_SITE);
281
-    }
282
-
283
-
284
-    /**
285
-     * sets database status to online
286
-     * turns maintenance mode off
287
-     *
288
-     * @return void
289
-     * @since 5.0.12.p
290
-     */
291
-    public function deactivateMaintenanceMode()
292
-    {
293
-        DbStatus::setOnline();
294
-        MaintenanceStatus::disableMaintenanceMode();
295
-        $this->updateMaintenaceModeStatus(EE_Maintenance_Mode::STATUS_OFF);
296
-    }
297
-
298
-
299
-    private function updateMaintenaceModeStatus(int $status)
300
-    {
301
-        $this->status = $status;
302
-        $this->setAdminStatus($status);
303
-        if (! $this->update_db) {
304
-            return;
305
-        }
306
-        do_action('AHEE__EE_Maintenance_Mode__set_maintenance_level', $status);
307
-        update_option(EE_Maintenance_Mode::OPTION_NAME, $status);
308
-    }
309
-
310
-
311
-    /**
312
-     * returns TRUE if M-Mode is engaged and the current request is not for the admin
313
-     *
314
-     * @return bool
315
-     */
316
-    public static function disable_frontend_for_maintenance(): bool
317
-    {
318
-        return ! is_admin() && MaintenanceStatus::isNotDisabled();
319
-    }
320
-
321
-
322
-    /**
323
-     * @return void
324
-     */
325
-    public function load_assets_required_for_m_mode(): void
326
-    {
327
-        if (
328
-            $this->status === EE_Maintenance_Mode::STATUS_FULL_SITE
329
-            && ! wp_script_is('espresso_core')
330
-        ) {
331
-            wp_register_style(
332
-                'espresso_default',
333
-                EE_GLOBAL_ASSETS_URL . 'css/espresso_default.css',
334
-                ['dashicons'],
335
-                EVENT_ESPRESSO_VERSION
336
-            );
337
-            wp_enqueue_style('espresso_default');
338
-            wp_register_script(
339
-                'espresso_core',
340
-                EE_GLOBAL_ASSETS_URL . 'scripts/espresso_core.js',
341
-                ['jquery'],
342
-                EVENT_ESPRESSO_VERSION,
343
-                true
344
-            );
345
-            wp_enqueue_script('espresso_core');
346
-        }
347
-    }
348
-
349
-
350
-    /**
351
-     * replacement EE CPT template that displays message notifying site visitors
352
-     * that EE has been temporarily placed into maintenance mode
353
-     * does NOT get called on non-EE-CPT requests
354
-     *
355
-     * @return    string
356
-     */
357
-    public static function template_include(): string
358
-    {
359
-        // shut 'er down for maintenance ? then don't use any of our templates for our endpoints
360
-        return get_template_directory() . '/index.php';
361
-    }
362
-
363
-
364
-    /**
365
-     * displays message notifying site visitors that EE has been temporarily
366
-     * placed into maintenance mode when post_type != EE CPT
367
-     *
368
-     * @param string $the_content
369
-     * @return string
370
-     */
371
-    public function the_content(string $the_content): string
372
-    {
373
-        // check if M-mode is engaged and for EE shortcode
374
-        if ($this->admin_status && strpos($the_content, '[ESPRESSO_') !== false) {
375
-            // this can eventually be moved to a template, or edited via admin. But for now...
376
-            $the_content = sprintf(
377
-                esc_html__(
378
-                    '%sMaintenance Mode%sEvent Registration has been temporarily closed while system maintenance is being performed. We\'re sorry for any inconveniences this may have caused. Please try back again later.%s',
379
-                    'event_espresso'
380
-                ),
381
-                '<h3>',
382
-                '</h3><p>',
383
-                '</p>'
384
-            );
385
-        }
386
-        return $the_content;
387
-    }
388
-
389
-
390
-    /**
391
-     * displays message on frontend of site notifying admin that EE has been temporarily placed into maintenance mode
392
-     */
393
-    public function display_maintenance_mode_notice()
394
-    {
395
-        if (
396
-            ! $this->current_user_is_admin
397
-            || $this->status === EE_Maintenance_Mode::STATUS_OFF
398
-            || $this->request->isAdmin()
399
-            || $this->request->isAjax()
400
-            || ! did_action('AHEE__EE_System__load_core_configuration__complete')
401
-        ) {
402
-            return;
403
-        }
404
-        /** @var CurrentPage $current_page */
405
-        $current_page = $this->loader->getShared(CurrentPage::class);
406
-        if ($current_page->isEspressoPage()) {
407
-            printf(
408
-                esc_html__(
409
-                    '%sclose%sEvent Registration is currently disabled because Event Espresso has been placed into Maintenance Mode. To change Maintenance Mode settings, click here %sEE Maintenance Mode Admin Page%s',
410
-                    'event_espresso'
411
-                ),
412
-                '<div id="ee-m-mode-admin-notice-dv" class="ee-really-important-notice-dv"><a class="close-espresso-notice" title="',
413
-                '"><span class="dashicons dashicons-no"></span></a><p>',
414
-                ' &raquo; <a href="' . add_query_arg(
415
-                    ['page' => 'espresso_maintenance_settings'],
416
-                    admin_url('admin.php')
417
-                ) . '">',
418
-                '</a></p></div>'
419
-            );
420
-        }
421
-    }
422
-    // espresso-notices important-notice ee-attention
423
-
424
-
425
-    /**
426
-     * Redirects EE admin menu requests to the maintenance page
427
-     */
428
-    public function redirect_to_maintenance()
429
-    {
430
-        global $pagenow;
431
-        $page = $this->request->getRequestParam('page', '', DataType::STRING);
432
-        if (
433
-            $pagenow == 'admin.php'
434
-            && $page !== 'espresso_maintenance_settings'
435
-            && strpos($page, 'espresso_') !== false
436
-            && $this->status == EE_Maintenance_Mode::STATUS_FULL_SITE
437
-        ) {
438
-            EEH_URL::safeRedirectAndExit('admin.php?page=espresso_maintenance_settings');
439
-        }
440
-    }
441
-
442
-
443
-    /**
444
-     * override magic methods
445
-     */
446
-    final public function __destruct()
447
-    {
448
-    }
449
-
450
-
451
-    final public function __call($a, $b)
452
-    {
453
-    }
454
-
455
-
456
-    final public function __get($a)
457
-    {
458
-    }
459
-
460
-
461
-    final public function __set($a, $b)
462
-    {
463
-    }
464
-
465
-
466
-    final public function __isset($a)
467
-    {
468
-    }
469
-
470
-
471
-    final public function __unset($a)
472
-    {
473
-    }
474
-
475
-
476
-    final public function __sleep()
477
-    {
478
-        return [];
479
-    }
480
-
481
-
482
-    final public function __wakeup()
483
-    {
484
-    }
485
-
486
-
487
-    final public function __invoke()
488
-    {
489
-    }
490
-
491
-
492
-    final public static function __set_state($a = null)
493
-    {
494
-        return EE_Maintenance_Mode::instance();
495
-    }
496
-
497
-
498
-    final public function __clone()
499
-    {
500
-    }
501
-
502
-
503
-    final public static function __callStatic($a, $b)
504
-    {
505
-    }
22
+	/**
23
+	 * constants available to client code for interpreting the values of EE_Maintenance_Mode::level().
24
+	 * STATUS_OFF means the site is NOT in maintenance mode (so everything's normal)
25
+	 */
26
+	public const STATUS_OFF = 0;
27
+
28
+
29
+	/**
30
+	 * STATUS_PUBLIC_ONLY means that the site's frontend EE code should be completely disabled
31
+	 * but the admin backend should be running as normal. Maybe an admin can view the frontend though
32
+	 */
33
+	public const STATUS_PUBLIC_ONLY = 1;
34
+
35
+	/**
36
+	 * STATUS_FULL_SITE means the frontend AND EE backend code are disabled. The only system running
37
+	 * is the maintenance mode stuff, which will require users to update all addons, and then finish running all
38
+	 * migration scripts before taking the site out of maintenance mode
39
+	 */
40
+	public const STATUS_FULL_SITE = 2;
41
+
42
+	/**
43
+	 * the name of the option which stores the current level of maintenance mode
44
+	 */
45
+	private const OPTION_NAME = 'ee_maintenance_mode';
46
+
47
+
48
+	protected LoaderInterface $loader;
49
+
50
+	private RequestInterface $request;
51
+
52
+	private static ?EE_Maintenance_Mode $_instance = null;
53
+
54
+	/**
55
+	 * @var int
56
+	 * @since 5.0.12.p
57
+	 */
58
+	private int $status;
59
+
60
+	/**
61
+	 * @var int
62
+	 * @since 5.0.12.p
63
+	 */
64
+	private int $admin_status;
65
+
66
+	/**
67
+	 * true if current_user_can('administrator')
68
+	 *
69
+	 * @var bool
70
+	 * @since 5.0.12.p
71
+	 */
72
+	private bool $current_user_is_admin;
73
+
74
+	/**
75
+	 * used to control updates to the WP options setting in the database
76
+	 *
77
+	 * @var bool
78
+	 * @since 5.0.12.p
79
+	 */
80
+	private bool $update_db;
81
+
82
+
83
+	/**
84
+	 * @singleton method used to instantiate class object
85
+	 * @param LoaderInterface|null  $loader
86
+	 * @param RequestInterface|null $request
87
+	 * @return EE_Maintenance_Mode|null
88
+	 */
89
+	public static function instance(
90
+		?LoaderInterface $loader = null,
91
+		?RequestInterface $request = null
92
+	): ?EE_Maintenance_Mode {
93
+		// check if class object is instantiated
94
+		if (! self::$_instance instanceof EE_Maintenance_Mode) {
95
+			self::$_instance = new EE_Maintenance_Mode($loader, $request);
96
+		}
97
+		return self::$_instance;
98
+	}
99
+
100
+
101
+	/**
102
+	 * Resets maintenance mode (mostly just re-checks whether we should be in maintenance mode)
103
+	 *
104
+	 * @return EE_Maintenance_Mode|null
105
+	 * @throws EE_Error
106
+	 */
107
+	public static function reset(): ?EE_Maintenance_Mode
108
+	{
109
+		self::instance()->set_maintenance_mode_if_db_old();
110
+		self::instance()->initialize();
111
+		return self::instance();
112
+	}
113
+
114
+
115
+	/**
116
+	 *private constructor to prevent direct creation
117
+	 */
118
+	private function __construct(LoaderInterface $loader, RequestInterface $request)
119
+	{
120
+		$this->loader                = $loader;
121
+		$this->request               = $request;
122
+		$this->initialize();
123
+
124
+		// if M-Mode level 2 is engaged, we still need basic assets loaded
125
+		add_action('wp_enqueue_scripts', [$this, 'load_assets_required_for_m_mode']);
126
+		// shut 'er down for maintenance ?
127
+		add_filter('the_content', [$this, 'the_content'], 2);
128
+		// redirect ee menus to maintenance page
129
+		add_action('admin_page_access_denied', [$this, 'redirect_to_maintenance']);
130
+		// add powered by EE msg
131
+		add_action('shutdown', [$this, 'display_maintenance_mode_notice']);
132
+	}
133
+
134
+
135
+	private function initialize(): void
136
+	{
137
+		$this->current_user_is_admin = current_user_can('administrator');
138
+		// now make sure the status is set correctly everywhere
139
+		// (but don't update the db else we'll get into an infinite loop of updates)
140
+		$this->update_db = false;
141
+		$this->set_maintenance_level($this->loadStatusFromDatabase());
142
+		$this->update_db = true;
143
+	}
144
+
145
+
146
+	private function loadStatusFromDatabase(): int
147
+	{
148
+		return (int) get_option(EE_Maintenance_Mode::OPTION_NAME, EE_Maintenance_Mode::STATUS_OFF);
149
+	}
150
+
151
+
152
+	/**
153
+	 * changes the maintenance mode level to reflect whether the current user is an admin or not.
154
+	 * Determines whether we're in maintenance mode and what level. However, while the site
155
+	 * is in level 1 maintenance, and an admin visits the frontend, this function makes it appear
156
+	 * to them as if the site isn't in maintenance mode.
157
+	 *      EE_Maintenance_Mode::STATUS_OFF => not in maintenance mode (in normal mode)
158
+	 *      EE_Maintenance_Mode::STATUS_PUBLIC_ONLY=> frontend-only maintenance mode
159
+	 *      EE_Maintenance_Mode::STATUS_FULL_SITE => frontend and backend maintenance mode
160
+	 *
161
+	 * @param int $status
162
+	 * @return void
163
+	 * @since 5.0.12.p
164
+	 */
165
+	private function setAdminStatus(int $status)
166
+	{
167
+		if (
168
+			$status === EE_Maintenance_Mode::STATUS_PUBLIC_ONLY
169
+			&& $this->current_user_is_admin
170
+			&& ($this->request->isAjax() || ! $this->request->isAdmin())
171
+		) {
172
+			$status = EE_Maintenance_Mode::STATUS_OFF;
173
+		}
174
+		$this->admin_status = $status;
175
+	}
176
+
177
+
178
+	public function real_level(): int
179
+	{
180
+		return $this->status;
181
+	}
182
+
183
+
184
+	/**
185
+	 * @return int
186
+	 */
187
+	public function level(): int
188
+	{
189
+		return $this->admin_status;
190
+	}
191
+
192
+
193
+	/**
194
+	 * Determines if we need to put EE in maintenance mode because the database needs updating
195
+	 *
196
+	 * @return boolean true if DB is old and maintenance mode was triggered; false otherwise
197
+	 * @throws EE_Error
198
+	 */
199
+	public function set_maintenance_mode_if_db_old(): bool
200
+	{
201
+		/** @var EE_Data_Migration_Manager $data_migration_manager */
202
+		$data_migration_manager = $this->loader->getShared(EE_Data_Migration_Manager::class );
203
+		$scripts_that_should_run = $data_migration_manager->check_for_applicable_data_migration_scripts();
204
+		if (! empty($scripts_that_should_run)) { //  && $this->status !== EE_Maintenance_Mode::STATUS_FULL_SITE
205
+			$this->activateFullSiteMaintenanceMode();
206
+			return true;
207
+		}
208
+		if ($this->status === EE_Maintenance_Mode::STATUS_FULL_SITE) {
209
+			// we also want to handle the opposite: if the site is mm2, but there aren't any migrations to run
210
+			// then we shouldn't be in mm2. (Maybe an addon got deactivated?)
211
+			$this->deactivateMaintenanceMode();
212
+		}
213
+		return false;
214
+	}
215
+
216
+
217
+	/**
218
+	 * Updates the maintenance level on the site
219
+	 *
220
+	 * @param int $level
221
+	 * @return void
222
+	 */
223
+	public function set_maintenance_level(int $level): void
224
+	{
225
+		switch ($level) {
226
+			case EE_Maintenance_Mode::STATUS_OFF:
227
+				$this->deactivateMaintenanceMode();
228
+				return;
229
+			case EE_Maintenance_Mode::STATUS_PUBLIC_ONLY:
230
+				$this->activatePublicOnlyMaintenanceMode();
231
+				return;
232
+			case EE_Maintenance_Mode::STATUS_FULL_SITE:
233
+				$this->activateFullSiteMaintenanceMode();
234
+				return;
235
+		}
236
+		throw new DomainException(
237
+			sprintf(
238
+				esc_html__(
239
+					'"%1$s" is not valid a EE maintenance mode level. Please choose from one of the following: %2$s',
240
+					'event_espresso'
241
+				),
242
+				$level,
243
+				'EE_Maintenance_Mode::STATUS_OFF, EE_Maintenance_Mode::STATUS_PUBLIC_ONLY, EE_Maintenance_Mode::STATUS_FULL_SITE',
244
+			)
245
+		);
246
+	}
247
+
248
+
249
+	/**
250
+	 * sets database status to online
251
+	 * sets maintenance mode status to public only, unless current user is an admin, then maintenance mode is disabled
252
+	 *
253
+	 * @return void
254
+	 * @since 5.0.12.p
255
+	 */
256
+	public function activatePublicOnlyMaintenanceMode()
257
+	{
258
+		DbStatus::setOnline();
259
+		// disable maintenance mode for admins, otherwise enable public only maintenance mode
260
+		if ($this->current_user_is_admin) {
261
+			MaintenanceStatus::disableMaintenanceMode();
262
+		} else {
263
+			MaintenanceStatus::setPublicOnlyMaintenanceMode();
264
+		}
265
+		$this->updateMaintenaceModeStatus(EE_Maintenance_Mode::STATUS_PUBLIC_ONLY);
266
+	}
267
+
268
+
269
+	/**
270
+	 * sets database status to offline
271
+	 * sets maintenance mode status to full site
272
+	 *
273
+	 * @return void
274
+	 * @since 5.0.12.p
275
+	 */
276
+	public function activateFullSiteMaintenanceMode()
277
+	{
278
+		DbStatus::setOffline();
279
+		MaintenanceStatus::setFullSiteMaintenanceMode();
280
+		$this->updateMaintenaceModeStatus(EE_Maintenance_Mode::STATUS_FULL_SITE);
281
+	}
282
+
283
+
284
+	/**
285
+	 * sets database status to online
286
+	 * turns maintenance mode off
287
+	 *
288
+	 * @return void
289
+	 * @since 5.0.12.p
290
+	 */
291
+	public function deactivateMaintenanceMode()
292
+	{
293
+		DbStatus::setOnline();
294
+		MaintenanceStatus::disableMaintenanceMode();
295
+		$this->updateMaintenaceModeStatus(EE_Maintenance_Mode::STATUS_OFF);
296
+	}
297
+
298
+
299
+	private function updateMaintenaceModeStatus(int $status)
300
+	{
301
+		$this->status = $status;
302
+		$this->setAdminStatus($status);
303
+		if (! $this->update_db) {
304
+			return;
305
+		}
306
+		do_action('AHEE__EE_Maintenance_Mode__set_maintenance_level', $status);
307
+		update_option(EE_Maintenance_Mode::OPTION_NAME, $status);
308
+	}
309
+
310
+
311
+	/**
312
+	 * returns TRUE if M-Mode is engaged and the current request is not for the admin
313
+	 *
314
+	 * @return bool
315
+	 */
316
+	public static function disable_frontend_for_maintenance(): bool
317
+	{
318
+		return ! is_admin() && MaintenanceStatus::isNotDisabled();
319
+	}
320
+
321
+
322
+	/**
323
+	 * @return void
324
+	 */
325
+	public function load_assets_required_for_m_mode(): void
326
+	{
327
+		if (
328
+			$this->status === EE_Maintenance_Mode::STATUS_FULL_SITE
329
+			&& ! wp_script_is('espresso_core')
330
+		) {
331
+			wp_register_style(
332
+				'espresso_default',
333
+				EE_GLOBAL_ASSETS_URL . 'css/espresso_default.css',
334
+				['dashicons'],
335
+				EVENT_ESPRESSO_VERSION
336
+			);
337
+			wp_enqueue_style('espresso_default');
338
+			wp_register_script(
339
+				'espresso_core',
340
+				EE_GLOBAL_ASSETS_URL . 'scripts/espresso_core.js',
341
+				['jquery'],
342
+				EVENT_ESPRESSO_VERSION,
343
+				true
344
+			);
345
+			wp_enqueue_script('espresso_core');
346
+		}
347
+	}
348
+
349
+
350
+	/**
351
+	 * replacement EE CPT template that displays message notifying site visitors
352
+	 * that EE has been temporarily placed into maintenance mode
353
+	 * does NOT get called on non-EE-CPT requests
354
+	 *
355
+	 * @return    string
356
+	 */
357
+	public static function template_include(): string
358
+	{
359
+		// shut 'er down for maintenance ? then don't use any of our templates for our endpoints
360
+		return get_template_directory() . '/index.php';
361
+	}
362
+
363
+
364
+	/**
365
+	 * displays message notifying site visitors that EE has been temporarily
366
+	 * placed into maintenance mode when post_type != EE CPT
367
+	 *
368
+	 * @param string $the_content
369
+	 * @return string
370
+	 */
371
+	public function the_content(string $the_content): string
372
+	{
373
+		// check if M-mode is engaged and for EE shortcode
374
+		if ($this->admin_status && strpos($the_content, '[ESPRESSO_') !== false) {
375
+			// this can eventually be moved to a template, or edited via admin. But for now...
376
+			$the_content = sprintf(
377
+				esc_html__(
378
+					'%sMaintenance Mode%sEvent Registration has been temporarily closed while system maintenance is being performed. We\'re sorry for any inconveniences this may have caused. Please try back again later.%s',
379
+					'event_espresso'
380
+				),
381
+				'<h3>',
382
+				'</h3><p>',
383
+				'</p>'
384
+			);
385
+		}
386
+		return $the_content;
387
+	}
388
+
389
+
390
+	/**
391
+	 * displays message on frontend of site notifying admin that EE has been temporarily placed into maintenance mode
392
+	 */
393
+	public function display_maintenance_mode_notice()
394
+	{
395
+		if (
396
+			! $this->current_user_is_admin
397
+			|| $this->status === EE_Maintenance_Mode::STATUS_OFF
398
+			|| $this->request->isAdmin()
399
+			|| $this->request->isAjax()
400
+			|| ! did_action('AHEE__EE_System__load_core_configuration__complete')
401
+		) {
402
+			return;
403
+		}
404
+		/** @var CurrentPage $current_page */
405
+		$current_page = $this->loader->getShared(CurrentPage::class);
406
+		if ($current_page->isEspressoPage()) {
407
+			printf(
408
+				esc_html__(
409
+					'%sclose%sEvent Registration is currently disabled because Event Espresso has been placed into Maintenance Mode. To change Maintenance Mode settings, click here %sEE Maintenance Mode Admin Page%s',
410
+					'event_espresso'
411
+				),
412
+				'<div id="ee-m-mode-admin-notice-dv" class="ee-really-important-notice-dv"><a class="close-espresso-notice" title="',
413
+				'"><span class="dashicons dashicons-no"></span></a><p>',
414
+				' &raquo; <a href="' . add_query_arg(
415
+					['page' => 'espresso_maintenance_settings'],
416
+					admin_url('admin.php')
417
+				) . '">',
418
+				'</a></p></div>'
419
+			);
420
+		}
421
+	}
422
+	// espresso-notices important-notice ee-attention
423
+
424
+
425
+	/**
426
+	 * Redirects EE admin menu requests to the maintenance page
427
+	 */
428
+	public function redirect_to_maintenance()
429
+	{
430
+		global $pagenow;
431
+		$page = $this->request->getRequestParam('page', '', DataType::STRING);
432
+		if (
433
+			$pagenow == 'admin.php'
434
+			&& $page !== 'espresso_maintenance_settings'
435
+			&& strpos($page, 'espresso_') !== false
436
+			&& $this->status == EE_Maintenance_Mode::STATUS_FULL_SITE
437
+		) {
438
+			EEH_URL::safeRedirectAndExit('admin.php?page=espresso_maintenance_settings');
439
+		}
440
+	}
441
+
442
+
443
+	/**
444
+	 * override magic methods
445
+	 */
446
+	final public function __destruct()
447
+	{
448
+	}
449
+
450
+
451
+	final public function __call($a, $b)
452
+	{
453
+	}
454
+
455
+
456
+	final public function __get($a)
457
+	{
458
+	}
459
+
460
+
461
+	final public function __set($a, $b)
462
+	{
463
+	}
464
+
465
+
466
+	final public function __isset($a)
467
+	{
468
+	}
469
+
470
+
471
+	final public function __unset($a)
472
+	{
473
+	}
474
+
475
+
476
+	final public function __sleep()
477
+	{
478
+		return [];
479
+	}
480
+
481
+
482
+	final public function __wakeup()
483
+	{
484
+	}
485
+
486
+
487
+	final public function __invoke()
488
+	{
489
+	}
490
+
491
+
492
+	final public static function __set_state($a = null)
493
+	{
494
+		return EE_Maintenance_Mode::instance();
495
+	}
496
+
497
+
498
+	final public function __clone()
499
+	{
500
+	}
501
+
502
+
503
+	final public static function __callStatic($a, $b)
504
+	{
505
+	}
506 506
 
507 507
 
508
-    /************************ @DEPRECATED ********************** */
508
+	/************************ @DEPRECATED ********************** */
509 509
 
510
-    /**
511
-     * @depecated 5.0.12.p
512
-     */
513
-    const level_0_not_in_maintenance = 0;
510
+	/**
511
+	 * @depecated 5.0.12.p
512
+	 */
513
+	const level_0_not_in_maintenance = 0;
514 514
 
515
-    /**
516
-     * @depecated 5.0.12.p
517
-     */
518
-    const level_1_frontend_only_maintenance = 1;
515
+	/**
516
+	 * @depecated 5.0.12.p
517
+	 */
518
+	const level_1_frontend_only_maintenance = 1;
519 519
 
520
-    /**
521
-     * @depecated 5.0.12.p
522
-     */
523
-    const level_2_complete_maintenance = 2;
520
+	/**
521
+	 * @depecated 5.0.12.p
522
+	 */
523
+	const level_2_complete_maintenance = 2;
524 524
 
525
-    /**
526
-     * @depecated 5.0.12.p
527
-     */
528
-    const option_name_maintenance_mode = 'ee_maintenance_mode';
525
+	/**
526
+	 * @depecated 5.0.12.p
527
+	 */
528
+	const option_name_maintenance_mode = 'ee_maintenance_mode';
529 529
 
530 530
 
531
-    /**
532
-     * Returns whether the models reportedly are able to run queries or not
533
-     * (ie, if the system thinks their tables are present and up-to-date).
534
-     *
535
-     * @return boolean
536
-     * @depecated 5.0.12.p
537
-     */
538
-    public function models_can_query(): bool
539
-    {
540
-        return DbStatus::isOnline();
541
-    }
531
+	/**
532
+	 * Returns whether the models reportedly are able to run queries or not
533
+	 * (ie, if the system thinks their tables are present and up-to-date).
534
+	 *
535
+	 * @return boolean
536
+	 * @depecated 5.0.12.p
537
+	 */
538
+	public function models_can_query(): bool
539
+	{
540
+		return DbStatus::isOnline();
541
+	}
542 542
 }
Please login to merge, or discard this patch.
Spacing   +9 added lines, -9 removed lines patch added patch discarded remove patch
@@ -91,7 +91,7 @@  discard block
 block discarded – undo
91 91
         ?RequestInterface $request = null
92 92
     ): ?EE_Maintenance_Mode {
93 93
         // check if class object is instantiated
94
-        if (! self::$_instance instanceof EE_Maintenance_Mode) {
94
+        if ( ! self::$_instance instanceof EE_Maintenance_Mode) {
95 95
             self::$_instance = new EE_Maintenance_Mode($loader, $request);
96 96
         }
97 97
         return self::$_instance;
@@ -199,9 +199,9 @@  discard block
 block discarded – undo
199 199
     public function set_maintenance_mode_if_db_old(): bool
200 200
     {
201 201
         /** @var EE_Data_Migration_Manager $data_migration_manager */
202
-        $data_migration_manager = $this->loader->getShared(EE_Data_Migration_Manager::class );
202
+        $data_migration_manager = $this->loader->getShared(EE_Data_Migration_Manager::class);
203 203
         $scripts_that_should_run = $data_migration_manager->check_for_applicable_data_migration_scripts();
204
-        if (! empty($scripts_that_should_run)) { //  && $this->status !== EE_Maintenance_Mode::STATUS_FULL_SITE
204
+        if ( ! empty($scripts_that_should_run)) { //  && $this->status !== EE_Maintenance_Mode::STATUS_FULL_SITE
205 205
             $this->activateFullSiteMaintenanceMode();
206 206
             return true;
207 207
         }
@@ -300,7 +300,7 @@  discard block
 block discarded – undo
300 300
     {
301 301
         $this->status = $status;
302 302
         $this->setAdminStatus($status);
303
-        if (! $this->update_db) {
303
+        if ( ! $this->update_db) {
304 304
             return;
305 305
         }
306 306
         do_action('AHEE__EE_Maintenance_Mode__set_maintenance_level', $status);
@@ -330,14 +330,14 @@  discard block
 block discarded – undo
330 330
         ) {
331 331
             wp_register_style(
332 332
                 'espresso_default',
333
-                EE_GLOBAL_ASSETS_URL . 'css/espresso_default.css',
333
+                EE_GLOBAL_ASSETS_URL.'css/espresso_default.css',
334 334
                 ['dashicons'],
335 335
                 EVENT_ESPRESSO_VERSION
336 336
             );
337 337
             wp_enqueue_style('espresso_default');
338 338
             wp_register_script(
339 339
                 'espresso_core',
340
-                EE_GLOBAL_ASSETS_URL . 'scripts/espresso_core.js',
340
+                EE_GLOBAL_ASSETS_URL.'scripts/espresso_core.js',
341 341
                 ['jquery'],
342 342
                 EVENT_ESPRESSO_VERSION,
343 343
                 true
@@ -357,7 +357,7 @@  discard block
 block discarded – undo
357 357
     public static function template_include(): string
358 358
     {
359 359
         // shut 'er down for maintenance ? then don't use any of our templates for our endpoints
360
-        return get_template_directory() . '/index.php';
360
+        return get_template_directory().'/index.php';
361 361
     }
362 362
 
363 363
 
@@ -411,10 +411,10 @@  discard block
 block discarded – undo
411 411
                 ),
412 412
                 '<div id="ee-m-mode-admin-notice-dv" class="ee-really-important-notice-dv"><a class="close-espresso-notice" title="',
413 413
                 '"><span class="dashicons dashicons-no"></span></a><p>',
414
-                ' &raquo; <a href="' . add_query_arg(
414
+                ' &raquo; <a href="'.add_query_arg(
415 415
                     ['page' => 'espresso_maintenance_settings'],
416 416
                     admin_url('admin.php')
417
-                ) . '">',
417
+                ).'">',
418 418
                 '</a></p></div>'
419 419
             );
420 420
         }
Please login to merge, or discard this patch.
core/libraries/shortcodes/EE_Event_Author_Shortcodes.lib.php 2 patches
Indentation   +106 added lines, -106 removed lines patch added patch discarded remove patch
@@ -19,121 +19,121 @@
 block discarded – undo
19 19
  */
20 20
 class EE_Event_Author_Shortcodes extends EE_Shortcodes
21 21
 {
22
-    protected function _init_props()
23
-    {
24
-        $this->label       = esc_html__('Event Author Details Shortcodes', 'event_espresso');
25
-        $this->description = esc_html__('All shortcodes specific to event_author data', 'event_espresso');
26
-        $this->_shortcodes = [
27
-            '[EVENT_AUTHOR_FNAME]'           => esc_html__(
28
-                'Parses to the first name of the event author.',
29
-                'event_espresso'
30
-            ),
31
-            '[EVENT_AUTHOR_LNAME]'           => esc_html__(
32
-                'Parses to the last name of the event author.',
33
-                'event_espresso'
34
-            ),
35
-            '[EVENT_AUTHOR_FORMATTED_EMAIL]' => esc_html__(
36
-                'Parses to a formatted email address of the event author (fname lname &lt;[email protected]&gt;).  <strong>NOTE:</strong> If the event author has not filled out their WordPress user profile then the organization name will be used as the "From" name.',
37
-                'event_espresso'
38
-            ),
39
-            '[EVENT_AUTHOR_EMAIL]'           => esc_html__(
40
-                'Parses to the unformatted email address of the event author',
41
-                'event_espresso'
42
-            ),
43
-        ];
44
-    }
22
+	protected function _init_props()
23
+	{
24
+		$this->label       = esc_html__('Event Author Details Shortcodes', 'event_espresso');
25
+		$this->description = esc_html__('All shortcodes specific to event_author data', 'event_espresso');
26
+		$this->_shortcodes = [
27
+			'[EVENT_AUTHOR_FNAME]'           => esc_html__(
28
+				'Parses to the first name of the event author.',
29
+				'event_espresso'
30
+			),
31
+			'[EVENT_AUTHOR_LNAME]'           => esc_html__(
32
+				'Parses to the last name of the event author.',
33
+				'event_espresso'
34
+			),
35
+			'[EVENT_AUTHOR_FORMATTED_EMAIL]' => esc_html__(
36
+				'Parses to a formatted email address of the event author (fname lname &lt;[email protected]&gt;).  <strong>NOTE:</strong> If the event author has not filled out their WordPress user profile then the organization name will be used as the "From" name.',
37
+				'event_espresso'
38
+			),
39
+			'[EVENT_AUTHOR_EMAIL]'           => esc_html__(
40
+				'Parses to the unformatted email address of the event author',
41
+				'event_espresso'
42
+			),
43
+		];
44
+	}
45 45
 
46 46
 
47
-    /**
48
-     * @param string $shortcode
49
-     * @throws EE_Error
50
-     * @throws ReflectionException
51
-     */
52
-    protected function _parser($shortcode)
53
-    {
54
-        // make sure we end up with a copy of the EE_Messages_Addressee object
55
-        $recipient = $this->_data instanceof EE_Messages_Addressee ? $this->_data : null;
56
-        $recipient = ! $recipient instanceof EE_Messages_Addressee
57
-            && is_array($this->_data)
58
-            && isset($this->_data['data'])
59
-            && $this->_data['data'] instanceof EE_Messages_Addressee
60
-                ? $this->_data['data']
61
-                : $recipient;
62
-        $recipient = ! $recipient instanceof EE_Messages_Addressee
63
-            && ! empty($this->_extra_data['data'])
64
-            && $this->_extra_data['data'] instanceof EE_Messages_Addressee
65
-                ? $this->_extra_data['data']
66
-                : $recipient;
47
+	/**
48
+	 * @param string $shortcode
49
+	 * @throws EE_Error
50
+	 * @throws ReflectionException
51
+	 */
52
+	protected function _parser($shortcode)
53
+	{
54
+		// make sure we end up with a copy of the EE_Messages_Addressee object
55
+		$recipient = $this->_data instanceof EE_Messages_Addressee ? $this->_data : null;
56
+		$recipient = ! $recipient instanceof EE_Messages_Addressee
57
+			&& is_array($this->_data)
58
+			&& isset($this->_data['data'])
59
+			&& $this->_data['data'] instanceof EE_Messages_Addressee
60
+				? $this->_data['data']
61
+				: $recipient;
62
+		$recipient = ! $recipient instanceof EE_Messages_Addressee
63
+			&& ! empty($this->_extra_data['data'])
64
+			&& $this->_extra_data['data'] instanceof EE_Messages_Addressee
65
+				? $this->_extra_data['data']
66
+				: $recipient;
67 67
 
68
-        // now it's possible that $recipient is not an instance of EE_Messages_Addressee in which case we need to see if $this->_data is an instance of $event.
69
-        $event = $this->_data instanceof EE_Event ? $this->_data : null;
68
+		// now it's possible that $recipient is not an instance of EE_Messages_Addressee in which case we need to see if $this->_data is an instance of $event.
69
+		$event = $this->_data instanceof EE_Event ? $this->_data : null;
70 70
 
71
-        if (! $recipient instanceof EE_Messages_Addressee && ! $event instanceof EE_Event) {
72
-            return '';
73
-        }
71
+		if (! $recipient instanceof EE_Messages_Addressee && ! $event instanceof EE_Event) {
72
+			return '';
73
+		}
74 74
 
75
-        switch ($shortcode) {
76
-            case '[EVENT_AUTHOR_FNAME]':
77
-                $fname = ! empty($recipient) ? $recipient->fname : null;
78
-                if (empty($fname) && ! empty($event)) {
79
-                    $user  = $this->_get_author_for_event($event);
80
-                    $fname = $user->first_name;
81
-                }
82
-                return $fname;
75
+		switch ($shortcode) {
76
+			case '[EVENT_AUTHOR_FNAME]':
77
+				$fname = ! empty($recipient) ? $recipient->fname : null;
78
+				if (empty($fname) && ! empty($event)) {
79
+					$user  = $this->_get_author_for_event($event);
80
+					$fname = $user->first_name;
81
+				}
82
+				return $fname;
83 83
 
84
-            case '[EVENT_AUTHOR_LNAME]':
85
-                $lname = ! empty($recipient) ? $recipient->lname : null;
86
-                if (empty($lname) && ! empty($event)) {
87
-                    $user  = $this->_get_author_for_event($event);
88
-                    $lname = $user->last_name;
89
-                }
90
-                return $lname;
84
+			case '[EVENT_AUTHOR_LNAME]':
85
+				$lname = ! empty($recipient) ? $recipient->lname : null;
86
+				if (empty($lname) && ! empty($event)) {
87
+					$user  = $this->_get_author_for_event($event);
88
+					$lname = $user->last_name;
89
+				}
90
+				return $lname;
91 91
 
92
-            case '[EVENT_AUTHOR_FORMATTED_EMAIL]':
93
-                if (! empty($recipient)) {
94
-                    $email = ! empty($recipient->fname)
95
-                        ? $recipient->fname . ' ' . $recipient->lname . '<' . $recipient->admin_email . '>'
96
-                        : EE_Registry::instance()->CFG->organization->get_pretty(
97
-                            'name'
98
-                        ) . '<' . $recipient->admin_email . '>';
99
-                } else {
100
-                    $email = null;
101
-                }
102
-                if (empty($email) && ! empty($event)) {
103
-                    $user  = $this->_get_author_for_event($event);
104
-                    $email = ! empty($user->first_name)
105
-                        ? $user->first_name . ' ' . $user->last_name . '<' . $user->user_email . '>'
106
-                        : EE_Registry::instance()->CFG->organization->get_pretty(
107
-                            'name'
108
-                        ) . '<' . $user->user_email . '>';
109
-                }
110
-                return $email;
92
+			case '[EVENT_AUTHOR_FORMATTED_EMAIL]':
93
+				if (! empty($recipient)) {
94
+					$email = ! empty($recipient->fname)
95
+						? $recipient->fname . ' ' . $recipient->lname . '<' . $recipient->admin_email . '>'
96
+						: EE_Registry::instance()->CFG->organization->get_pretty(
97
+							'name'
98
+						) . '<' . $recipient->admin_email . '>';
99
+				} else {
100
+					$email = null;
101
+				}
102
+				if (empty($email) && ! empty($event)) {
103
+					$user  = $this->_get_author_for_event($event);
104
+					$email = ! empty($user->first_name)
105
+						? $user->first_name . ' ' . $user->last_name . '<' . $user->user_email . '>'
106
+						: EE_Registry::instance()->CFG->organization->get_pretty(
107
+							'name'
108
+						) . '<' . $user->user_email . '>';
109
+				}
110
+				return $email;
111 111
 
112
-            case '[EVENT_AUTHOR_EMAIL]':
113
-                $email = ! empty($recipient) ? $recipient->admin_email : null;
114
-                if (empty($email) && ! empty($event)) {
115
-                    $user  = $this->_get_author_for_event($event);
116
-                    $email = $user instanceof WP_User ? $user->user_email : '';
117
-                }
118
-                return $email;
112
+			case '[EVENT_AUTHOR_EMAIL]':
113
+				$email = ! empty($recipient) ? $recipient->admin_email : null;
114
+				if (empty($email) && ! empty($event)) {
115
+					$user  = $this->_get_author_for_event($event);
116
+					$email = $user instanceof WP_User ? $user->user_email : '';
117
+				}
118
+				return $email;
119 119
 
120
-            default:
121
-                return '';
122
-        }
123
-    }
120
+			default:
121
+				return '';
122
+		}
123
+	}
124 124
 
125 125
 
126
-    /**
127
-     * Helper method to return the user object for the author of the given EE_Event
128
-     *
129
-     * @param EE_Event $event
130
-     *
131
-     * @return WP_User|false
132
-     * @throws EE_Error
133
-     * @throws ReflectionException
134
-     */
135
-    private function _get_author_for_event(EE_Event $event)
136
-    {
137
-        return get_userdata($event->wp_user());
138
-    }
126
+	/**
127
+	 * Helper method to return the user object for the author of the given EE_Event
128
+	 *
129
+	 * @param EE_Event $event
130
+	 *
131
+	 * @return WP_User|false
132
+	 * @throws EE_Error
133
+	 * @throws ReflectionException
134
+	 */
135
+	private function _get_author_for_event(EE_Event $event)
136
+	{
137
+		return get_userdata($event->wp_user());
138
+	}
139 139
 }
Please login to merge, or discard this patch.
Spacing   +6 added lines, -6 removed lines patch added patch discarded remove patch
@@ -68,7 +68,7 @@  discard block
 block discarded – undo
68 68
         // now it's possible that $recipient is not an instance of EE_Messages_Addressee in which case we need to see if $this->_data is an instance of $event.
69 69
         $event = $this->_data instanceof EE_Event ? $this->_data : null;
70 70
 
71
-        if (! $recipient instanceof EE_Messages_Addressee && ! $event instanceof EE_Event) {
71
+        if ( ! $recipient instanceof EE_Messages_Addressee && ! $event instanceof EE_Event) {
72 72
             return '';
73 73
         }
74 74
 
@@ -90,22 +90,22 @@  discard block
 block discarded – undo
90 90
                 return $lname;
91 91
 
92 92
             case '[EVENT_AUTHOR_FORMATTED_EMAIL]':
93
-                if (! empty($recipient)) {
93
+                if ( ! empty($recipient)) {
94 94
                     $email = ! empty($recipient->fname)
95
-                        ? $recipient->fname . ' ' . $recipient->lname . '<' . $recipient->admin_email . '>'
95
+                        ? $recipient->fname.' '.$recipient->lname.'<'.$recipient->admin_email.'>'
96 96
                         : EE_Registry::instance()->CFG->organization->get_pretty(
97 97
                             'name'
98
-                        ) . '<' . $recipient->admin_email . '>';
98
+                        ).'<'.$recipient->admin_email.'>';
99 99
                 } else {
100 100
                     $email = null;
101 101
                 }
102 102
                 if (empty($email) && ! empty($event)) {
103 103
                     $user  = $this->_get_author_for_event($event);
104 104
                     $email = ! empty($user->first_name)
105
-                        ? $user->first_name . ' ' . $user->last_name . '<' . $user->user_email . '>'
105
+                        ? $user->first_name.' '.$user->last_name.'<'.$user->user_email.'>'
106 106
                         : EE_Registry::instance()->CFG->organization->get_pretty(
107 107
                             'name'
108
-                        ) . '<' . $user->user_email . '>';
108
+                        ).'<'.$user->user_email.'>';
109 109
                 }
110 110
                 return $email;
111 111
 
Please login to merge, or discard this patch.
core/libraries/rest_api/ModelDataTranslator.php 2 patches
Indentation   +660 added lines, -660 removed lines patch added patch discarded remove patch
@@ -39,664 +39,664 @@
 block discarded – undo
39 39
  */
40 40
 class ModelDataTranslator
41 41
 {
42
-    /**
43
-     * We used to use -1 for infinity in the rest api, but that's ambiguous for
44
-     * fields that COULD contain -1; so we use null
45
-     */
46
-    const EE_INF_IN_REST = null;
47
-
48
-
49
-    /**
50
-     * Prepares a possible array of input values from JSON for use by the models
51
-     *
52
-     * @param EE_Model_Field_Base $field_obj
53
-     * @param mixed               $original_value_maybe_array
54
-     * @param string              $requested_version
55
-     * @param string|null         $timezone_string treat values as being in this timezone
56
-     * @return mixed
57
-     * @throws RestException
58
-     * @throws EE_Error
59
-     */
60
-    public static function prepareFieldValuesFromJson(
61
-        EE_Model_Field_Base $field_obj,
62
-        $original_value_maybe_array,
63
-        string $requested_version,
64
-        ?string $timezone_string = 'UTC'
65
-    ) {
66
-        if (
67
-            is_array($original_value_maybe_array)
68
-            && ! $field_obj instanceof EE_Serialized_Text_Field
69
-        ) {
70
-            $new_value_maybe_array = [];
71
-            foreach ($original_value_maybe_array as $array_key => $array_item) {
72
-                $new_value_maybe_array[ $array_key ] = ModelDataTranslator::prepareFieldValueFromJson(
73
-                    $field_obj,
74
-                    $array_item,
75
-                    $requested_version,
76
-                    $timezone_string
77
-                );
78
-            }
79
-        } else {
80
-            $new_value_maybe_array = ModelDataTranslator::prepareFieldValueFromJson(
81
-                $field_obj,
82
-                $original_value_maybe_array,
83
-                $requested_version,
84
-                $timezone_string
85
-            );
86
-        }
87
-        return $new_value_maybe_array;
88
-    }
89
-
90
-
91
-    /**
92
-     * Prepares an array of field values FOR use in JSON/REST API
93
-     *
94
-     * @param EE_Model_Field_Base $field_obj
95
-     * @param mixed               $original_value_maybe_array
96
-     * @param string              $request_version (eg 4.8.36)
97
-     * @return array|int|string
98
-     * @throws EE_Error
99
-     * @throws EE_Error
100
-     */
101
-    public static function prepareFieldValuesForJson(
102
-        EE_Model_Field_Base $field_obj,
103
-        $original_value_maybe_array,
104
-        string $request_version
105
-    ) {
106
-        if (is_array($original_value_maybe_array)) {
107
-            $new_value = [];
108
-            foreach ($original_value_maybe_array as $key => $value) {
109
-                $new_value[ $key ] = ModelDataTranslator::prepareFieldValuesForJson(
110
-                    $field_obj,
111
-                    $value,
112
-                    $request_version
113
-                );
114
-            }
115
-        } else {
116
-            $new_value = ModelDataTranslator::prepareFieldValueForJson(
117
-                $field_obj,
118
-                $original_value_maybe_array,
119
-                $request_version
120
-            );
121
-        }
122
-        return $new_value;
123
-    }
124
-
125
-
126
-    /**
127
-     * Prepares incoming data from the json or request parameters for the models'
128
-     * "$query_params".
129
-     *
130
-     * @param EE_Model_Field_Base $field_obj
131
-     * @param mixed               $original_value
132
-     * @param string              $requested_version
133
-     * @param string|null         $timezone_string treat values as being in this timezone
134
-     * @return mixed
135
-     * @throws RestException
136
-     * @throws DomainException
137
-     * @throws EE_Error
138
-     * @throws Exception
139
-     */
140
-    public static function prepareFieldValueFromJson(
141
-        EE_Model_Field_Base $field_obj,
142
-        $original_value,
143
-        string $requested_version,
144
-        ?string $timezone_string = 'UTC'
145
-    ) {
146
-        // check if they accidentally submitted an error value. If so throw an exception
147
-        if (
148
-            is_array($original_value)
149
-            && isset($original_value['error_code'], $original_value['error_message'])
150
-        ) {
151
-            throw new RestException(
152
-                'rest_submitted_error_value',
153
-                sprintf(
154
-                    esc_html__(
155
-                        'You tried to submit a JSON error object as a value for %1$s. That\'s not allowed.',
156
-                        'event_espresso'
157
-                    ),
158
-                    $field_obj->get_name()
159
-                ),
160
-                [
161
-                    'status' => 400,
162
-                ]
163
-            );
164
-        }
165
-        // double-check for serialized PHP. We never accept serialized PHP. No way Jose.
166
-        ModelDataTranslator::throwExceptionIfContainsSerializedData($original_value);
167
-        $timezone_string = $timezone_string !== ''
168
-            ? $timezone_string
169
-            : get_option('timezone_string', '');
170
-        // walk through the submitted data and double-check for serialized PHP. We never accept serialized PHP. No
171
-        // way Jose.
172
-        ModelDataTranslator::throwExceptionIfContainsSerializedData($original_value);
173
-        $new_value = $original_value;
174
-        if (
175
-            $field_obj instanceof EE_Infinite_Integer_Field
176
-            && in_array($original_value, [null, ''], true)
177
-        ) {
178
-            $new_value = EE_INF;
179
-        } elseif ($field_obj instanceof EE_Datetime_Field) {
180
-            if (! (empty($original_value) && $field_obj->is_nullable())) {
181
-                $new_value = rest_parse_date(
182
-                    self::getTimestampWithTimezoneOffset($original_value, $field_obj, $timezone_string)
183
-                );
184
-                if ($new_value === false) {
185
-                    throw new RestException(
186
-                        'invalid_format_for_timestamp',
187
-                        sprintf(
188
-                            esc_html__(
189
-                                'Timestamps received on a request as the value for Date and Time fields must be in %1$s/%2$s format.  The timestamp provided (%3$s) is not that format.',
190
-                                'event_espresso'
191
-                            ),
192
-                            'RFC3339',
193
-                            'ISO8601',
194
-                            $original_value
195
-                        ),
196
-                        [
197
-                            'status' => 400,
198
-                        ]
199
-                    );
200
-                }
201
-            }
202
-        } elseif ($field_obj instanceof EE_Boolean_Field) {
203
-            // Interpreted the strings "false", "true", "on", "off" appropriately.
204
-            $new_value = filter_var($original_value, FILTER_VALIDATE_BOOLEAN);
205
-        }
206
-        return $new_value;
207
-    }
208
-
209
-
210
-    /**
211
-     * This checks if the incoming timestamp has timezone information already on it and if it doesn't,
212
-     * then adds timezone information via details obtained from the host site.
213
-     *
214
-     * @param string|null       $original_timestamp
215
-     * @param EE_Datetime_Field $datetime_field
216
-     * @param string|null       $timezone_string
217
-     * @return string
218
-     * @throws DomainException
219
-     * @throws Exception
220
-     */
221
-    private static function getTimestampWithTimezoneOffset(
222
-        ?string $original_timestamp,
223
-        EE_Datetime_Field $datetime_field,
224
-        ?string $timezone_string
225
-    ): ?string {
226
-        // already have timezone information?
227
-        if (preg_match('/Z|([+-])(\d{2}:\d{2})/', (string) $original_timestamp)) {
228
-            // yes, we're ignoring the timezone.
229
-            return $original_timestamp;
230
-        }
231
-        // need to append timezone
232
-        [$offset_sign, $offset_secs] = self::parseTimezoneOffset(
233
-            $datetime_field->get_timezone_offset(
234
-                new DateTimeZone($timezone_string),
235
-                $original_timestamp
236
-            )
237
-        );
238
-        $offset_string =
239
-            str_pad(
240
-                floor($offset_secs / HOUR_IN_SECONDS),
241
-                2,
242
-                '0',
243
-                STR_PAD_LEFT
244
-            )
245
-            . ':'
246
-            . str_pad(
247
-                ($offset_secs % HOUR_IN_SECONDS) / MINUTE_IN_SECONDS,
248
-                2,
249
-                '0',
250
-                STR_PAD_LEFT
251
-            );
252
-        return $original_timestamp . $offset_sign . $offset_string;
253
-    }
254
-
255
-
256
-    /**
257
-     * Throws an exception if $data is a serialized PHP string (or somehow an actually PHP object, although I don't
258
-     * think that can happen). If $data is an array, recurses into its keys and values
259
-     *
260
-     * @param mixed $data
261
-     * @return void
262
-     * @throws RestException
263
-     */
264
-    public static function throwExceptionIfContainsSerializedData($data)
265
-    {
266
-        if (is_array($data)) {
267
-            foreach ($data as $key => $value) {
268
-                ModelDataTranslator::throwExceptionIfContainsSerializedData($key);
269
-                ModelDataTranslator::throwExceptionIfContainsSerializedData($value);
270
-            }
271
-        } else {
272
-            if (is_serialized($data) || is_object($data)) {
273
-                throw new RestException(
274
-                    'serialized_data_submission_prohibited',
275
-                    esc_html__(
276
-                    // @codingStandardsIgnoreStart
277
-                        'You tried to submit a string of serialized text. Serialized PHP is prohibited over the EE4 REST API.',
278
-                        // @codingStandardsIgnoreEnd
279
-                        'event_espresso'
280
-                    )
281
-                );
282
-            }
283
-        }
284
-    }
285
-
286
-
287
-    /**
288
-     * determines what's going on with them timezone strings
289
-     *
290
-     * @param int|string|null $timezone_offset
291
-     * @return array
292
-     */
293
-    private static function parseTimezoneOffset($timezone_offset): array
294
-    {
295
-        $first_char = substr((string) $timezone_offset, 0, 1);
296
-        if ($first_char === '+' || $first_char === '-') {
297
-            $offset_sign = $first_char;
298
-            $offset_secs = substr((string) $timezone_offset, 1);
299
-        } else {
300
-            $offset_sign = '+';
301
-            $offset_secs = $timezone_offset;
302
-        }
303
-        return [$offset_sign, $offset_secs];
304
-    }
305
-
306
-
307
-    /**
308
-     * Prepares a field's value for display in the API
309
-     *
310
-     * @param EE_Model_Field_Base|null $field_obj
311
-     * @param mixed                    $original_value
312
-     * @param string                   $requested_version
313
-     * @return mixed
314
-     * @throws EE_Error
315
-     * @throws EE_Error
316
-     * @throws Exception
317
-     */
318
-    public static function prepareFieldValueForJson(
319
-        ?EE_Model_Field_Base $field_obj,
320
-        $original_value,
321
-        string $requested_version
322
-    ) {
323
-        if ($original_value === EE_INF) {
324
-            $new_value = ModelDataTranslator::EE_INF_IN_REST;
325
-        } elseif ($field_obj instanceof EE_Datetime_Field) {
326
-            if (is_string($original_value)) {
327
-                // did they submit a string of a unix timestamp?
328
-                if (is_numeric($original_value)) {
329
-                    $datetime_obj = new DateTime();
330
-                    $datetime_obj->setTimestamp((int) $original_value);
331
-                } else {
332
-                    // first, check if it's a MySQL timestamp in GMT
333
-                    $datetime_obj = DateTime::createFromFormat('Y-m-d H:i:s', $original_value);
334
-                }
335
-                if (! $datetime_obj instanceof DateTime) {
336
-                    // so it's not a unix timestamp or a MySQL timestamp. Maybe it's in the field's date/time format?
337
-                    $datetime_obj = $field_obj->prepare_for_set($original_value);
338
-                }
339
-                $original_value = $datetime_obj;
340
-            }
341
-            if ($original_value instanceof DateTime) {
342
-                $new_value = $original_value->format('Y-m-d H:i:s');
343
-            } elseif (is_int($original_value) || is_float($original_value)) {
344
-                $new_value = date('Y-m-d H:i:s', $original_value);
345
-            } elseif ($original_value === null || $original_value === '') {
346
-                $new_value = null;
347
-            } else {
348
-                // so it's not a datetime object, unix timestamp (as string or int),
349
-                // MySQL timestamp, or even a string in the field object's format. So no idea what it is
350
-                throw new EE_Error(
351
-                    sprintf(
352
-                        esc_html__(
353
-                        // @codingStandardsIgnoreStart
354
-                            'The value "%1$s" for the field "%2$s" on model "%3$s" could not be understood. It should be a PHP DateTime, unix timestamp, MySQL date, or string in the format "%4$s".',
355
-                            // @codingStandardsIgnoreEnd
356
-                            'event_espresso'
357
-                        ),
358
-                        $original_value,
359
-                        $field_obj->get_name(),
360
-                        $field_obj->get_model_name(),
361
-                        $field_obj->get_time_format() . ' ' . $field_obj->get_time_format()
362
-                    )
363
-                );
364
-            }
365
-            if ($new_value !== null) {
366
-                $new_value = mysql2date('Y-m-d\TH:i:s', $new_value, false);
367
-            }
368
-        } else {
369
-            $new_value = $original_value;
370
-        }
371
-        // are we about to send an object? just don't. We have no good way to represent it in JSON.
372
-        // can't just check using is_object() because that missed PHP incomplete objects
373
-        if (! ModelDataTranslator::isRepresentableInJson($new_value)) {
374
-            $new_value = [
375
-                'error_code'    => 'php_object_not_return',
376
-                'error_message' => esc_html__(
377
-                    'The value of this field in the database is a PHP object, which can\'t be represented in JSON.',
378
-                    'event_espresso'
379
-                ),
380
-            ];
381
-        }
382
-        return apply_filters(
383
-            'FHEE__EventEspresso\core\libraries\rest_api\Model_Data_Translator__prepare_field_for_rest_api',
384
-            $new_value,
385
-            $field_obj,
386
-            $original_value,
387
-            $requested_version
388
-        );
389
-    }
390
-
391
-
392
-    /**
393
-     * Prepares condition-query-parameters (like what's in where and having) from
394
-     * the format expected in the API to use in the models
395
-     *
396
-     * @param array    $inputted_query_params_of_this_type
397
-     * @param EEM_Base $model
398
-     * @param string   $requested_version
399
-     * @param boolean  $writing whether this data will be written to the DB, or if we're just building a query.
400
-     *                          If we're writing to the DB, we don't expect any operators, or any logic query
401
-     *                          parameters, and we also won't accept serialized data unless the current user has
402
-     *                          unfiltered_html.
403
-     * @return array
404
-     * @throws DomainException
405
-     * @throws EE_Error
406
-     * @throws RestException
407
-     * @throws InvalidDataTypeException
408
-     * @throws InvalidInterfaceException
409
-     * @throws InvalidArgumentException
410
-     */
411
-    public static function prepareConditionsQueryParamsForModels(
412
-        array $inputted_query_params_of_this_type,
413
-        EEM_Base $model,
414
-        string $requested_version,
415
-        bool $writing = false
416
-    ): array {
417
-        $query_param_for_models = [];
418
-        $context                = new RestIncomingQueryParamContext($model, $requested_version, $writing);
419
-        foreach ($inputted_query_params_of_this_type as $query_param_key => $query_param_value) {
420
-            $query_param_meta = new RestIncomingQueryParamMetadata($query_param_key, $query_param_value, $context);
421
-            if ($query_param_meta->getField() instanceof EE_Model_Field_Base) {
422
-                $translated_value = $query_param_meta->determineConditionsQueryParameterValue();
423
-                if (
424
-                    (isset($query_param_for_models[ $query_param_meta->getQueryParamKey() ])
425
-                        && $query_param_meta->isGmtField())
426
-                    || $translated_value === null
427
-                ) {
428
-                    // they have already provided a non-gmt field, ignore the gmt one. That's what WP core
429
-                    // currently does (they might change it though). See https://core.trac.wordpress.org/ticket/39954
430
-                    // OR we couldn't create a translated value from their input
431
-                    continue;
432
-                }
433
-                $query_param_for_models[ $query_param_meta->getQueryParamKey() ] = $translated_value;
434
-            } else {
435
-                $nested_query_params = $query_param_meta->determineNestedConditionQueryParameters();
436
-                if ($nested_query_params) {
437
-                    $query_param_for_models[ $query_param_meta->getQueryParamKey() ] = $nested_query_params;
438
-                }
439
-            }
440
-        }
441
-        return $query_param_for_models;
442
-    }
443
-
444
-
445
-    /**
446
-     * Mostly checks if the last 4 characters are "_gmt", indicating it's a
447
-     * gmt date field name
448
-     *
449
-     * @param string $field_name
450
-     * @return boolean
451
-     */
452
-    public static function isGmtDateFieldName(string $field_name): bool
453
-    {
454
-        $field_name = ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey($field_name);
455
-        return substr($field_name, -4, 4) === '_gmt';
456
-    }
457
-
458
-
459
-    /**
460
-     * Removes the last "_gmt" part of a field name (and if there is no "_gmt" at the end, leave it alone)
461
-     *
462
-     * @param string $field_name
463
-     * @return string
464
-     */
465
-    public static function removeGmtFromFieldName(string $field_name): string
466
-    {
467
-        if (! ModelDataTranslator::isGmtDateFieldName($field_name)) {
468
-            return $field_name;
469
-        }
470
-        $query_param_sans_stars              =
471
-            ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey(
472
-                $field_name
473
-            );
474
-        $query_param_sans_gmt_and_sans_stars = substr(
475
-            $query_param_sans_stars,
476
-            0,
477
-            strrpos(
478
-                $field_name,
479
-                '_gmt'
480
-            )
481
-        );
482
-        return str_replace($query_param_sans_stars, $query_param_sans_gmt_and_sans_stars, $field_name);
483
-    }
484
-
485
-
486
-    /**
487
-     * Takes a field name from the REST API and prepares it for the model querying
488
-     *
489
-     * @param string $field_name
490
-     * @return string
491
-     */
492
-    public static function prepareFieldNameFromJson(string $field_name): string
493
-    {
494
-        if (ModelDataTranslator::isGmtDateFieldName($field_name)) {
495
-            return ModelDataTranslator::removeGmtFromFieldName($field_name);
496
-        }
497
-        return $field_name;
498
-    }
499
-
500
-
501
-    /**
502
-     * Takes array of field names from REST API and prepares for models
503
-     *
504
-     * @param array $field_names
505
-     * @return array of field names (possibly include model prefixes)
506
-     */
507
-    public static function prepareFieldNamesFromJson(array $field_names): array
508
-    {
509
-        $new_array = [];
510
-        foreach ($field_names as $key => $field_name) {
511
-            $new_array[ $key ] = ModelDataTranslator::prepareFieldNameFromJson($field_name);
512
-        }
513
-        return $new_array;
514
-    }
515
-
516
-
517
-    /**
518
-     * Takes array where array keys are field names (possibly with model path prefixes)
519
-     * from the REST API and prepares them for model querying
520
-     *
521
-     * @param array $field_names_as_keys
522
-     * @return array
523
-     */
524
-    public static function prepareFieldNamesInArrayKeysFromJson(array $field_names_as_keys): array
525
-    {
526
-        $new_array = [];
527
-        foreach ($field_names_as_keys as $field_name => $value) {
528
-            $new_array[ ModelDataTranslator::prepareFieldNameFromJson($field_name) ] = $value;
529
-        }
530
-        return $new_array;
531
-    }
532
-
533
-
534
-    /**
535
-     * Prepares an array of model query params for use in the REST API
536
-     *
537
-     * @param array       $model_query_params
538
-     * @param EEM_Base    $model
539
-     * @param string|null $requested_version eg "4.8.36". If null is provided, defaults to the latest release of the EE4
540
-     *                                       REST API
541
-     * @return array which can be passed into the EE4 REST API when querying a model resource
542
-     * @throws EE_Error
543
-     * @throws ReflectionException
544
-     */
545
-    public static function prepareQueryParamsForRestApi(
546
-        array $model_query_params,
547
-        EEM_Base $model,
548
-        ?string $requested_version = ''
549
-    ): array {
550
-        if (! $requested_version) {
551
-            $requested_version = EED_Core_Rest_Api::latest_rest_api_version();
552
-        }
553
-        $rest_query_params = $model_query_params;
554
-        if (isset($model_query_params[0])) {
555
-            $rest_query_params['where'] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
556
-                $model_query_params[0],
557
-                $model,
558
-                $requested_version
559
-            );
560
-            unset($rest_query_params[0]);
561
-        }
562
-        if (isset($model_query_params['having'])) {
563
-            $rest_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
564
-                $model_query_params['having'],
565
-                $model,
566
-                $requested_version
567
-            );
568
-        }
569
-        return apply_filters(
570
-            'FHEE__EventEspresso\core\libraries\rest_api\Model_Data_Translator__prepare_query_params_for_rest_api',
571
-            $rest_query_params,
572
-            $model_query_params,
573
-            $model,
574
-            $requested_version
575
-        );
576
-    }
577
-
578
-
579
-    /**
580
-     * Prepares all the sub-conditions query parameters (eg having or where conditions) for use in the rest api
581
-     *
582
-     * @param array    $inputted_query_params_of_this_type eg like the "where" or "having" conditions query params
583
-     * @param EEM_Base $model
584
-     * @param string   $requested_version                  eg "4.8.36"
585
-     * @return array ready for use in the rest api query params
586
-     * @throws EE_Error
587
-     * @throws RestException if somehow a PHP object were in the query param's values
588
-     * @throws ReflectionException (which would be really unusual)
589
-     * @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions
590
-     */
591
-    public static function prepareConditionsQueryParamsForRestApi(
592
-        array $inputted_query_params_of_this_type,
593
-        EEM_Base $model,
594
-        string $requested_version
595
-    ): array {
596
-        $query_param_for_models = [];
597
-        foreach ($inputted_query_params_of_this_type as $query_param_key => $query_param_value) {
598
-            $field = ModelDataTranslator::deduceFieldFromQueryParam(
599
-                ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey($query_param_key),
600
-                $model
601
-            );
602
-            if ($field instanceof EE_Model_Field_Base) {
603
-                // did they specify an operator?
604
-                if (is_array($query_param_value)) {
605
-                    $op               = $query_param_value[0];
606
-                    $translated_value = [$op];
607
-                    if (isset($query_param_value[1])) {
608
-                        $value               = $query_param_value[1];
609
-                        $translated_value[1] = ModelDataTranslator::prepareFieldValuesForJson(
610
-                            $field,
611
-                            $value,
612
-                            $requested_version
613
-                        );
614
-                    }
615
-                } else {
616
-                    $translated_value = ModelDataTranslator::prepareFieldValueForJson(
617
-                        $field,
618
-                        $query_param_value,
619
-                        $requested_version
620
-                    );
621
-                }
622
-                $query_param_for_models[ $query_param_key ] = $translated_value;
623
-            } else {
624
-                // so it's not for a field, assume it's a logic query param key
625
-                $query_param_for_models[ $query_param_key ] =
626
-                    ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
627
-                        $query_param_value,
628
-                        $model,
629
-                        $requested_version
630
-                    );
631
-            }
632
-        }
633
-        return $query_param_for_models;
634
-    }
635
-
636
-
637
-    /**
638
-     * @param $condition_query_param_key
639
-     * @return string
640
-     */
641
-    public static function removeStarsAndAnythingAfterFromConditionQueryParamKey($condition_query_param_key): string
642
-    {
643
-        $pos_of_star = strpos($condition_query_param_key, '*');
644
-        if ($pos_of_star === false) {
645
-            return $condition_query_param_key;
646
-        }
647
-        return substr($condition_query_param_key, 0, $pos_of_star);
648
-    }
649
-
650
-
651
-    /**
652
-     * Takes the input parameter and finds the model field that it indicates.
653
-     *
654
-     * @param string   $query_param_name like Registration.Transaction.TXN_ID, Event.Datetime.start_time, or REG_ID
655
-     * @param EEM_Base $model
656
-     * @return EE_Model_Field_Base
657
-     * @throws EE_Error
658
-     * @throws ReflectionException
659
-     */
660
-    public static function deduceFieldFromQueryParam(string $query_param_name, EEM_Base $model): ?EE_Model_Field_Base
661
-    {
662
-        // ok, now proceed with deducing which part is the model's name, and which is the field's name
663
-        // which will help us find the database table and column
664
-        $query_param_parts = explode('.', $query_param_name);
665
-        if (empty($query_param_parts)) {
666
-            throw new EE_Error(
667
-                sprintf(
668
-                    esc_html__(
669
-                        '_extract_column_name is empty when trying to extract column and table name from %s',
670
-                        'event_espresso'
671
-                    ),
672
-                    $query_param_name
673
-                )
674
-            );
675
-        }
676
-        $number_of_parts       = count($query_param_parts);
677
-        $last_query_param_part = $query_param_parts[ count($query_param_parts) - 1 ];
678
-        $field_name            = $last_query_param_part;
679
-        if ($number_of_parts !== 1) {
680
-            // the last part is the column name, and there are only 2parts. therefore...
681
-            $model = EE_Registry::instance()->load_model($query_param_parts[ $number_of_parts - 2 ]);
682
-        }
683
-        try {
684
-            return $model->field_settings_for($field_name, false);
685
-        } catch (EE_Error $e) {
686
-            return null;
687
-        }
688
-    }
689
-
690
-
691
-    /**
692
-     * Returns true if $data can be easily represented in JSON.
693
-     * Basically, objects and resources can't be represented in JSON easily.
694
-     *
695
-     * @param mixed $data
696
-     * @return bool
697
-     */
698
-    protected static function isRepresentableInJson($data): bool
699
-    {
700
-        return is_scalar($data) || is_array($data) || is_null($data);
701
-    }
42
+	/**
43
+	 * We used to use -1 for infinity in the rest api, but that's ambiguous for
44
+	 * fields that COULD contain -1; so we use null
45
+	 */
46
+	const EE_INF_IN_REST = null;
47
+
48
+
49
+	/**
50
+	 * Prepares a possible array of input values from JSON for use by the models
51
+	 *
52
+	 * @param EE_Model_Field_Base $field_obj
53
+	 * @param mixed               $original_value_maybe_array
54
+	 * @param string              $requested_version
55
+	 * @param string|null         $timezone_string treat values as being in this timezone
56
+	 * @return mixed
57
+	 * @throws RestException
58
+	 * @throws EE_Error
59
+	 */
60
+	public static function prepareFieldValuesFromJson(
61
+		EE_Model_Field_Base $field_obj,
62
+		$original_value_maybe_array,
63
+		string $requested_version,
64
+		?string $timezone_string = 'UTC'
65
+	) {
66
+		if (
67
+			is_array($original_value_maybe_array)
68
+			&& ! $field_obj instanceof EE_Serialized_Text_Field
69
+		) {
70
+			$new_value_maybe_array = [];
71
+			foreach ($original_value_maybe_array as $array_key => $array_item) {
72
+				$new_value_maybe_array[ $array_key ] = ModelDataTranslator::prepareFieldValueFromJson(
73
+					$field_obj,
74
+					$array_item,
75
+					$requested_version,
76
+					$timezone_string
77
+				);
78
+			}
79
+		} else {
80
+			$new_value_maybe_array = ModelDataTranslator::prepareFieldValueFromJson(
81
+				$field_obj,
82
+				$original_value_maybe_array,
83
+				$requested_version,
84
+				$timezone_string
85
+			);
86
+		}
87
+		return $new_value_maybe_array;
88
+	}
89
+
90
+
91
+	/**
92
+	 * Prepares an array of field values FOR use in JSON/REST API
93
+	 *
94
+	 * @param EE_Model_Field_Base $field_obj
95
+	 * @param mixed               $original_value_maybe_array
96
+	 * @param string              $request_version (eg 4.8.36)
97
+	 * @return array|int|string
98
+	 * @throws EE_Error
99
+	 * @throws EE_Error
100
+	 */
101
+	public static function prepareFieldValuesForJson(
102
+		EE_Model_Field_Base $field_obj,
103
+		$original_value_maybe_array,
104
+		string $request_version
105
+	) {
106
+		if (is_array($original_value_maybe_array)) {
107
+			$new_value = [];
108
+			foreach ($original_value_maybe_array as $key => $value) {
109
+				$new_value[ $key ] = ModelDataTranslator::prepareFieldValuesForJson(
110
+					$field_obj,
111
+					$value,
112
+					$request_version
113
+				);
114
+			}
115
+		} else {
116
+			$new_value = ModelDataTranslator::prepareFieldValueForJson(
117
+				$field_obj,
118
+				$original_value_maybe_array,
119
+				$request_version
120
+			);
121
+		}
122
+		return $new_value;
123
+	}
124
+
125
+
126
+	/**
127
+	 * Prepares incoming data from the json or request parameters for the models'
128
+	 * "$query_params".
129
+	 *
130
+	 * @param EE_Model_Field_Base $field_obj
131
+	 * @param mixed               $original_value
132
+	 * @param string              $requested_version
133
+	 * @param string|null         $timezone_string treat values as being in this timezone
134
+	 * @return mixed
135
+	 * @throws RestException
136
+	 * @throws DomainException
137
+	 * @throws EE_Error
138
+	 * @throws Exception
139
+	 */
140
+	public static function prepareFieldValueFromJson(
141
+		EE_Model_Field_Base $field_obj,
142
+		$original_value,
143
+		string $requested_version,
144
+		?string $timezone_string = 'UTC'
145
+	) {
146
+		// check if they accidentally submitted an error value. If so throw an exception
147
+		if (
148
+			is_array($original_value)
149
+			&& isset($original_value['error_code'], $original_value['error_message'])
150
+		) {
151
+			throw new RestException(
152
+				'rest_submitted_error_value',
153
+				sprintf(
154
+					esc_html__(
155
+						'You tried to submit a JSON error object as a value for %1$s. That\'s not allowed.',
156
+						'event_espresso'
157
+					),
158
+					$field_obj->get_name()
159
+				),
160
+				[
161
+					'status' => 400,
162
+				]
163
+			);
164
+		}
165
+		// double-check for serialized PHP. We never accept serialized PHP. No way Jose.
166
+		ModelDataTranslator::throwExceptionIfContainsSerializedData($original_value);
167
+		$timezone_string = $timezone_string !== ''
168
+			? $timezone_string
169
+			: get_option('timezone_string', '');
170
+		// walk through the submitted data and double-check for serialized PHP. We never accept serialized PHP. No
171
+		// way Jose.
172
+		ModelDataTranslator::throwExceptionIfContainsSerializedData($original_value);
173
+		$new_value = $original_value;
174
+		if (
175
+			$field_obj instanceof EE_Infinite_Integer_Field
176
+			&& in_array($original_value, [null, ''], true)
177
+		) {
178
+			$new_value = EE_INF;
179
+		} elseif ($field_obj instanceof EE_Datetime_Field) {
180
+			if (! (empty($original_value) && $field_obj->is_nullable())) {
181
+				$new_value = rest_parse_date(
182
+					self::getTimestampWithTimezoneOffset($original_value, $field_obj, $timezone_string)
183
+				);
184
+				if ($new_value === false) {
185
+					throw new RestException(
186
+						'invalid_format_for_timestamp',
187
+						sprintf(
188
+							esc_html__(
189
+								'Timestamps received on a request as the value for Date and Time fields must be in %1$s/%2$s format.  The timestamp provided (%3$s) is not that format.',
190
+								'event_espresso'
191
+							),
192
+							'RFC3339',
193
+							'ISO8601',
194
+							$original_value
195
+						),
196
+						[
197
+							'status' => 400,
198
+						]
199
+					);
200
+				}
201
+			}
202
+		} elseif ($field_obj instanceof EE_Boolean_Field) {
203
+			// Interpreted the strings "false", "true", "on", "off" appropriately.
204
+			$new_value = filter_var($original_value, FILTER_VALIDATE_BOOLEAN);
205
+		}
206
+		return $new_value;
207
+	}
208
+
209
+
210
+	/**
211
+	 * This checks if the incoming timestamp has timezone information already on it and if it doesn't,
212
+	 * then adds timezone information via details obtained from the host site.
213
+	 *
214
+	 * @param string|null       $original_timestamp
215
+	 * @param EE_Datetime_Field $datetime_field
216
+	 * @param string|null       $timezone_string
217
+	 * @return string
218
+	 * @throws DomainException
219
+	 * @throws Exception
220
+	 */
221
+	private static function getTimestampWithTimezoneOffset(
222
+		?string $original_timestamp,
223
+		EE_Datetime_Field $datetime_field,
224
+		?string $timezone_string
225
+	): ?string {
226
+		// already have timezone information?
227
+		if (preg_match('/Z|([+-])(\d{2}:\d{2})/', (string) $original_timestamp)) {
228
+			// yes, we're ignoring the timezone.
229
+			return $original_timestamp;
230
+		}
231
+		// need to append timezone
232
+		[$offset_sign, $offset_secs] = self::parseTimezoneOffset(
233
+			$datetime_field->get_timezone_offset(
234
+				new DateTimeZone($timezone_string),
235
+				$original_timestamp
236
+			)
237
+		);
238
+		$offset_string =
239
+			str_pad(
240
+				floor($offset_secs / HOUR_IN_SECONDS),
241
+				2,
242
+				'0',
243
+				STR_PAD_LEFT
244
+			)
245
+			. ':'
246
+			. str_pad(
247
+				($offset_secs % HOUR_IN_SECONDS) / MINUTE_IN_SECONDS,
248
+				2,
249
+				'0',
250
+				STR_PAD_LEFT
251
+			);
252
+		return $original_timestamp . $offset_sign . $offset_string;
253
+	}
254
+
255
+
256
+	/**
257
+	 * Throws an exception if $data is a serialized PHP string (or somehow an actually PHP object, although I don't
258
+	 * think that can happen). If $data is an array, recurses into its keys and values
259
+	 *
260
+	 * @param mixed $data
261
+	 * @return void
262
+	 * @throws RestException
263
+	 */
264
+	public static function throwExceptionIfContainsSerializedData($data)
265
+	{
266
+		if (is_array($data)) {
267
+			foreach ($data as $key => $value) {
268
+				ModelDataTranslator::throwExceptionIfContainsSerializedData($key);
269
+				ModelDataTranslator::throwExceptionIfContainsSerializedData($value);
270
+			}
271
+		} else {
272
+			if (is_serialized($data) || is_object($data)) {
273
+				throw new RestException(
274
+					'serialized_data_submission_prohibited',
275
+					esc_html__(
276
+					// @codingStandardsIgnoreStart
277
+						'You tried to submit a string of serialized text. Serialized PHP is prohibited over the EE4 REST API.',
278
+						// @codingStandardsIgnoreEnd
279
+						'event_espresso'
280
+					)
281
+				);
282
+			}
283
+		}
284
+	}
285
+
286
+
287
+	/**
288
+	 * determines what's going on with them timezone strings
289
+	 *
290
+	 * @param int|string|null $timezone_offset
291
+	 * @return array
292
+	 */
293
+	private static function parseTimezoneOffset($timezone_offset): array
294
+	{
295
+		$first_char = substr((string) $timezone_offset, 0, 1);
296
+		if ($first_char === '+' || $first_char === '-') {
297
+			$offset_sign = $first_char;
298
+			$offset_secs = substr((string) $timezone_offset, 1);
299
+		} else {
300
+			$offset_sign = '+';
301
+			$offset_secs = $timezone_offset;
302
+		}
303
+		return [$offset_sign, $offset_secs];
304
+	}
305
+
306
+
307
+	/**
308
+	 * Prepares a field's value for display in the API
309
+	 *
310
+	 * @param EE_Model_Field_Base|null $field_obj
311
+	 * @param mixed                    $original_value
312
+	 * @param string                   $requested_version
313
+	 * @return mixed
314
+	 * @throws EE_Error
315
+	 * @throws EE_Error
316
+	 * @throws Exception
317
+	 */
318
+	public static function prepareFieldValueForJson(
319
+		?EE_Model_Field_Base $field_obj,
320
+		$original_value,
321
+		string $requested_version
322
+	) {
323
+		if ($original_value === EE_INF) {
324
+			$new_value = ModelDataTranslator::EE_INF_IN_REST;
325
+		} elseif ($field_obj instanceof EE_Datetime_Field) {
326
+			if (is_string($original_value)) {
327
+				// did they submit a string of a unix timestamp?
328
+				if (is_numeric($original_value)) {
329
+					$datetime_obj = new DateTime();
330
+					$datetime_obj->setTimestamp((int) $original_value);
331
+				} else {
332
+					// first, check if it's a MySQL timestamp in GMT
333
+					$datetime_obj = DateTime::createFromFormat('Y-m-d H:i:s', $original_value);
334
+				}
335
+				if (! $datetime_obj instanceof DateTime) {
336
+					// so it's not a unix timestamp or a MySQL timestamp. Maybe it's in the field's date/time format?
337
+					$datetime_obj = $field_obj->prepare_for_set($original_value);
338
+				}
339
+				$original_value = $datetime_obj;
340
+			}
341
+			if ($original_value instanceof DateTime) {
342
+				$new_value = $original_value->format('Y-m-d H:i:s');
343
+			} elseif (is_int($original_value) || is_float($original_value)) {
344
+				$new_value = date('Y-m-d H:i:s', $original_value);
345
+			} elseif ($original_value === null || $original_value === '') {
346
+				$new_value = null;
347
+			} else {
348
+				// so it's not a datetime object, unix timestamp (as string or int),
349
+				// MySQL timestamp, or even a string in the field object's format. So no idea what it is
350
+				throw new EE_Error(
351
+					sprintf(
352
+						esc_html__(
353
+						// @codingStandardsIgnoreStart
354
+							'The value "%1$s" for the field "%2$s" on model "%3$s" could not be understood. It should be a PHP DateTime, unix timestamp, MySQL date, or string in the format "%4$s".',
355
+							// @codingStandardsIgnoreEnd
356
+							'event_espresso'
357
+						),
358
+						$original_value,
359
+						$field_obj->get_name(),
360
+						$field_obj->get_model_name(),
361
+						$field_obj->get_time_format() . ' ' . $field_obj->get_time_format()
362
+					)
363
+				);
364
+			}
365
+			if ($new_value !== null) {
366
+				$new_value = mysql2date('Y-m-d\TH:i:s', $new_value, false);
367
+			}
368
+		} else {
369
+			$new_value = $original_value;
370
+		}
371
+		// are we about to send an object? just don't. We have no good way to represent it in JSON.
372
+		// can't just check using is_object() because that missed PHP incomplete objects
373
+		if (! ModelDataTranslator::isRepresentableInJson($new_value)) {
374
+			$new_value = [
375
+				'error_code'    => 'php_object_not_return',
376
+				'error_message' => esc_html__(
377
+					'The value of this field in the database is a PHP object, which can\'t be represented in JSON.',
378
+					'event_espresso'
379
+				),
380
+			];
381
+		}
382
+		return apply_filters(
383
+			'FHEE__EventEspresso\core\libraries\rest_api\Model_Data_Translator__prepare_field_for_rest_api',
384
+			$new_value,
385
+			$field_obj,
386
+			$original_value,
387
+			$requested_version
388
+		);
389
+	}
390
+
391
+
392
+	/**
393
+	 * Prepares condition-query-parameters (like what's in where and having) from
394
+	 * the format expected in the API to use in the models
395
+	 *
396
+	 * @param array    $inputted_query_params_of_this_type
397
+	 * @param EEM_Base $model
398
+	 * @param string   $requested_version
399
+	 * @param boolean  $writing whether this data will be written to the DB, or if we're just building a query.
400
+	 *                          If we're writing to the DB, we don't expect any operators, or any logic query
401
+	 *                          parameters, and we also won't accept serialized data unless the current user has
402
+	 *                          unfiltered_html.
403
+	 * @return array
404
+	 * @throws DomainException
405
+	 * @throws EE_Error
406
+	 * @throws RestException
407
+	 * @throws InvalidDataTypeException
408
+	 * @throws InvalidInterfaceException
409
+	 * @throws InvalidArgumentException
410
+	 */
411
+	public static function prepareConditionsQueryParamsForModels(
412
+		array $inputted_query_params_of_this_type,
413
+		EEM_Base $model,
414
+		string $requested_version,
415
+		bool $writing = false
416
+	): array {
417
+		$query_param_for_models = [];
418
+		$context                = new RestIncomingQueryParamContext($model, $requested_version, $writing);
419
+		foreach ($inputted_query_params_of_this_type as $query_param_key => $query_param_value) {
420
+			$query_param_meta = new RestIncomingQueryParamMetadata($query_param_key, $query_param_value, $context);
421
+			if ($query_param_meta->getField() instanceof EE_Model_Field_Base) {
422
+				$translated_value = $query_param_meta->determineConditionsQueryParameterValue();
423
+				if (
424
+					(isset($query_param_for_models[ $query_param_meta->getQueryParamKey() ])
425
+						&& $query_param_meta->isGmtField())
426
+					|| $translated_value === null
427
+				) {
428
+					// they have already provided a non-gmt field, ignore the gmt one. That's what WP core
429
+					// currently does (they might change it though). See https://core.trac.wordpress.org/ticket/39954
430
+					// OR we couldn't create a translated value from their input
431
+					continue;
432
+				}
433
+				$query_param_for_models[ $query_param_meta->getQueryParamKey() ] = $translated_value;
434
+			} else {
435
+				$nested_query_params = $query_param_meta->determineNestedConditionQueryParameters();
436
+				if ($nested_query_params) {
437
+					$query_param_for_models[ $query_param_meta->getQueryParamKey() ] = $nested_query_params;
438
+				}
439
+			}
440
+		}
441
+		return $query_param_for_models;
442
+	}
443
+
444
+
445
+	/**
446
+	 * Mostly checks if the last 4 characters are "_gmt", indicating it's a
447
+	 * gmt date field name
448
+	 *
449
+	 * @param string $field_name
450
+	 * @return boolean
451
+	 */
452
+	public static function isGmtDateFieldName(string $field_name): bool
453
+	{
454
+		$field_name = ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey($field_name);
455
+		return substr($field_name, -4, 4) === '_gmt';
456
+	}
457
+
458
+
459
+	/**
460
+	 * Removes the last "_gmt" part of a field name (and if there is no "_gmt" at the end, leave it alone)
461
+	 *
462
+	 * @param string $field_name
463
+	 * @return string
464
+	 */
465
+	public static function removeGmtFromFieldName(string $field_name): string
466
+	{
467
+		if (! ModelDataTranslator::isGmtDateFieldName($field_name)) {
468
+			return $field_name;
469
+		}
470
+		$query_param_sans_stars              =
471
+			ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey(
472
+				$field_name
473
+			);
474
+		$query_param_sans_gmt_and_sans_stars = substr(
475
+			$query_param_sans_stars,
476
+			0,
477
+			strrpos(
478
+				$field_name,
479
+				'_gmt'
480
+			)
481
+		);
482
+		return str_replace($query_param_sans_stars, $query_param_sans_gmt_and_sans_stars, $field_name);
483
+	}
484
+
485
+
486
+	/**
487
+	 * Takes a field name from the REST API and prepares it for the model querying
488
+	 *
489
+	 * @param string $field_name
490
+	 * @return string
491
+	 */
492
+	public static function prepareFieldNameFromJson(string $field_name): string
493
+	{
494
+		if (ModelDataTranslator::isGmtDateFieldName($field_name)) {
495
+			return ModelDataTranslator::removeGmtFromFieldName($field_name);
496
+		}
497
+		return $field_name;
498
+	}
499
+
500
+
501
+	/**
502
+	 * Takes array of field names from REST API and prepares for models
503
+	 *
504
+	 * @param array $field_names
505
+	 * @return array of field names (possibly include model prefixes)
506
+	 */
507
+	public static function prepareFieldNamesFromJson(array $field_names): array
508
+	{
509
+		$new_array = [];
510
+		foreach ($field_names as $key => $field_name) {
511
+			$new_array[ $key ] = ModelDataTranslator::prepareFieldNameFromJson($field_name);
512
+		}
513
+		return $new_array;
514
+	}
515
+
516
+
517
+	/**
518
+	 * Takes array where array keys are field names (possibly with model path prefixes)
519
+	 * from the REST API and prepares them for model querying
520
+	 *
521
+	 * @param array $field_names_as_keys
522
+	 * @return array
523
+	 */
524
+	public static function prepareFieldNamesInArrayKeysFromJson(array $field_names_as_keys): array
525
+	{
526
+		$new_array = [];
527
+		foreach ($field_names_as_keys as $field_name => $value) {
528
+			$new_array[ ModelDataTranslator::prepareFieldNameFromJson($field_name) ] = $value;
529
+		}
530
+		return $new_array;
531
+	}
532
+
533
+
534
+	/**
535
+	 * Prepares an array of model query params for use in the REST API
536
+	 *
537
+	 * @param array       $model_query_params
538
+	 * @param EEM_Base    $model
539
+	 * @param string|null $requested_version eg "4.8.36". If null is provided, defaults to the latest release of the EE4
540
+	 *                                       REST API
541
+	 * @return array which can be passed into the EE4 REST API when querying a model resource
542
+	 * @throws EE_Error
543
+	 * @throws ReflectionException
544
+	 */
545
+	public static function prepareQueryParamsForRestApi(
546
+		array $model_query_params,
547
+		EEM_Base $model,
548
+		?string $requested_version = ''
549
+	): array {
550
+		if (! $requested_version) {
551
+			$requested_version = EED_Core_Rest_Api::latest_rest_api_version();
552
+		}
553
+		$rest_query_params = $model_query_params;
554
+		if (isset($model_query_params[0])) {
555
+			$rest_query_params['where'] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
556
+				$model_query_params[0],
557
+				$model,
558
+				$requested_version
559
+			);
560
+			unset($rest_query_params[0]);
561
+		}
562
+		if (isset($model_query_params['having'])) {
563
+			$rest_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
564
+				$model_query_params['having'],
565
+				$model,
566
+				$requested_version
567
+			);
568
+		}
569
+		return apply_filters(
570
+			'FHEE__EventEspresso\core\libraries\rest_api\Model_Data_Translator__prepare_query_params_for_rest_api',
571
+			$rest_query_params,
572
+			$model_query_params,
573
+			$model,
574
+			$requested_version
575
+		);
576
+	}
577
+
578
+
579
+	/**
580
+	 * Prepares all the sub-conditions query parameters (eg having or where conditions) for use in the rest api
581
+	 *
582
+	 * @param array    $inputted_query_params_of_this_type eg like the "where" or "having" conditions query params
583
+	 * @param EEM_Base $model
584
+	 * @param string   $requested_version                  eg "4.8.36"
585
+	 * @return array ready for use in the rest api query params
586
+	 * @throws EE_Error
587
+	 * @throws RestException if somehow a PHP object were in the query param's values
588
+	 * @throws ReflectionException (which would be really unusual)
589
+	 * @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions
590
+	 */
591
+	public static function prepareConditionsQueryParamsForRestApi(
592
+		array $inputted_query_params_of_this_type,
593
+		EEM_Base $model,
594
+		string $requested_version
595
+	): array {
596
+		$query_param_for_models = [];
597
+		foreach ($inputted_query_params_of_this_type as $query_param_key => $query_param_value) {
598
+			$field = ModelDataTranslator::deduceFieldFromQueryParam(
599
+				ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey($query_param_key),
600
+				$model
601
+			);
602
+			if ($field instanceof EE_Model_Field_Base) {
603
+				// did they specify an operator?
604
+				if (is_array($query_param_value)) {
605
+					$op               = $query_param_value[0];
606
+					$translated_value = [$op];
607
+					if (isset($query_param_value[1])) {
608
+						$value               = $query_param_value[1];
609
+						$translated_value[1] = ModelDataTranslator::prepareFieldValuesForJson(
610
+							$field,
611
+							$value,
612
+							$requested_version
613
+						);
614
+					}
615
+				} else {
616
+					$translated_value = ModelDataTranslator::prepareFieldValueForJson(
617
+						$field,
618
+						$query_param_value,
619
+						$requested_version
620
+					);
621
+				}
622
+				$query_param_for_models[ $query_param_key ] = $translated_value;
623
+			} else {
624
+				// so it's not for a field, assume it's a logic query param key
625
+				$query_param_for_models[ $query_param_key ] =
626
+					ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
627
+						$query_param_value,
628
+						$model,
629
+						$requested_version
630
+					);
631
+			}
632
+		}
633
+		return $query_param_for_models;
634
+	}
635
+
636
+
637
+	/**
638
+	 * @param $condition_query_param_key
639
+	 * @return string
640
+	 */
641
+	public static function removeStarsAndAnythingAfterFromConditionQueryParamKey($condition_query_param_key): string
642
+	{
643
+		$pos_of_star = strpos($condition_query_param_key, '*');
644
+		if ($pos_of_star === false) {
645
+			return $condition_query_param_key;
646
+		}
647
+		return substr($condition_query_param_key, 0, $pos_of_star);
648
+	}
649
+
650
+
651
+	/**
652
+	 * Takes the input parameter and finds the model field that it indicates.
653
+	 *
654
+	 * @param string   $query_param_name like Registration.Transaction.TXN_ID, Event.Datetime.start_time, or REG_ID
655
+	 * @param EEM_Base $model
656
+	 * @return EE_Model_Field_Base
657
+	 * @throws EE_Error
658
+	 * @throws ReflectionException
659
+	 */
660
+	public static function deduceFieldFromQueryParam(string $query_param_name, EEM_Base $model): ?EE_Model_Field_Base
661
+	{
662
+		// ok, now proceed with deducing which part is the model's name, and which is the field's name
663
+		// which will help us find the database table and column
664
+		$query_param_parts = explode('.', $query_param_name);
665
+		if (empty($query_param_parts)) {
666
+			throw new EE_Error(
667
+				sprintf(
668
+					esc_html__(
669
+						'_extract_column_name is empty when trying to extract column and table name from %s',
670
+						'event_espresso'
671
+					),
672
+					$query_param_name
673
+				)
674
+			);
675
+		}
676
+		$number_of_parts       = count($query_param_parts);
677
+		$last_query_param_part = $query_param_parts[ count($query_param_parts) - 1 ];
678
+		$field_name            = $last_query_param_part;
679
+		if ($number_of_parts !== 1) {
680
+			// the last part is the column name, and there are only 2parts. therefore...
681
+			$model = EE_Registry::instance()->load_model($query_param_parts[ $number_of_parts - 2 ]);
682
+		}
683
+		try {
684
+			return $model->field_settings_for($field_name, false);
685
+		} catch (EE_Error $e) {
686
+			return null;
687
+		}
688
+	}
689
+
690
+
691
+	/**
692
+	 * Returns true if $data can be easily represented in JSON.
693
+	 * Basically, objects and resources can't be represented in JSON easily.
694
+	 *
695
+	 * @param mixed $data
696
+	 * @return bool
697
+	 */
698
+	protected static function isRepresentableInJson($data): bool
699
+	{
700
+		return is_scalar($data) || is_array($data) || is_null($data);
701
+	}
702 702
 }
Please login to merge, or discard this patch.
Spacing   +19 added lines, -19 removed lines patch added patch discarded remove patch
@@ -69,7 +69,7 @@  discard block
 block discarded – undo
69 69
         ) {
70 70
             $new_value_maybe_array = [];
71 71
             foreach ($original_value_maybe_array as $array_key => $array_item) {
72
-                $new_value_maybe_array[ $array_key ] = ModelDataTranslator::prepareFieldValueFromJson(
72
+                $new_value_maybe_array[$array_key] = ModelDataTranslator::prepareFieldValueFromJson(
73 73
                     $field_obj,
74 74
                     $array_item,
75 75
                     $requested_version,
@@ -106,7 +106,7 @@  discard block
 block discarded – undo
106 106
         if (is_array($original_value_maybe_array)) {
107 107
             $new_value = [];
108 108
             foreach ($original_value_maybe_array as $key => $value) {
109
-                $new_value[ $key ] = ModelDataTranslator::prepareFieldValuesForJson(
109
+                $new_value[$key] = ModelDataTranslator::prepareFieldValuesForJson(
110 110
                     $field_obj,
111 111
                     $value,
112 112
                     $request_version
@@ -177,7 +177,7 @@  discard block
 block discarded – undo
177 177
         ) {
178 178
             $new_value = EE_INF;
179 179
         } elseif ($field_obj instanceof EE_Datetime_Field) {
180
-            if (! (empty($original_value) && $field_obj->is_nullable())) {
180
+            if ( ! (empty($original_value) && $field_obj->is_nullable())) {
181 181
                 $new_value = rest_parse_date(
182 182
                     self::getTimestampWithTimezoneOffset($original_value, $field_obj, $timezone_string)
183 183
                 );
@@ -249,7 +249,7 @@  discard block
 block discarded – undo
249 249
                 '0',
250 250
                 STR_PAD_LEFT
251 251
             );
252
-        return $original_timestamp . $offset_sign . $offset_string;
252
+        return $original_timestamp.$offset_sign.$offset_string;
253 253
     }
254 254
 
255 255
 
@@ -332,7 +332,7 @@  discard block
 block discarded – undo
332 332
                     // first, check if it's a MySQL timestamp in GMT
333 333
                     $datetime_obj = DateTime::createFromFormat('Y-m-d H:i:s', $original_value);
334 334
                 }
335
-                if (! $datetime_obj instanceof DateTime) {
335
+                if ( ! $datetime_obj instanceof DateTime) {
336 336
                     // so it's not a unix timestamp or a MySQL timestamp. Maybe it's in the field's date/time format?
337 337
                     $datetime_obj = $field_obj->prepare_for_set($original_value);
338 338
                 }
@@ -358,7 +358,7 @@  discard block
 block discarded – undo
358 358
                         $original_value,
359 359
                         $field_obj->get_name(),
360 360
                         $field_obj->get_model_name(),
361
-                        $field_obj->get_time_format() . ' ' . $field_obj->get_time_format()
361
+                        $field_obj->get_time_format().' '.$field_obj->get_time_format()
362 362
                     )
363 363
                 );
364 364
             }
@@ -370,7 +370,7 @@  discard block
 block discarded – undo
370 370
         }
371 371
         // are we about to send an object? just don't. We have no good way to represent it in JSON.
372 372
         // can't just check using is_object() because that missed PHP incomplete objects
373
-        if (! ModelDataTranslator::isRepresentableInJson($new_value)) {
373
+        if ( ! ModelDataTranslator::isRepresentableInJson($new_value)) {
374 374
             $new_value = [
375 375
                 'error_code'    => 'php_object_not_return',
376 376
                 'error_message' => esc_html__(
@@ -421,7 +421,7 @@  discard block
 block discarded – undo
421 421
             if ($query_param_meta->getField() instanceof EE_Model_Field_Base) {
422 422
                 $translated_value = $query_param_meta->determineConditionsQueryParameterValue();
423 423
                 if (
424
-                    (isset($query_param_for_models[ $query_param_meta->getQueryParamKey() ])
424
+                    (isset($query_param_for_models[$query_param_meta->getQueryParamKey()])
425 425
                         && $query_param_meta->isGmtField())
426 426
                     || $translated_value === null
427 427
                 ) {
@@ -430,11 +430,11 @@  discard block
 block discarded – undo
430 430
                     // OR we couldn't create a translated value from their input
431 431
                     continue;
432 432
                 }
433
-                $query_param_for_models[ $query_param_meta->getQueryParamKey() ] = $translated_value;
433
+                $query_param_for_models[$query_param_meta->getQueryParamKey()] = $translated_value;
434 434
             } else {
435 435
                 $nested_query_params = $query_param_meta->determineNestedConditionQueryParameters();
436 436
                 if ($nested_query_params) {
437
-                    $query_param_for_models[ $query_param_meta->getQueryParamKey() ] = $nested_query_params;
437
+                    $query_param_for_models[$query_param_meta->getQueryParamKey()] = $nested_query_params;
438 438
                 }
439 439
             }
440 440
         }
@@ -464,10 +464,10 @@  discard block
 block discarded – undo
464 464
      */
465 465
     public static function removeGmtFromFieldName(string $field_name): string
466 466
     {
467
-        if (! ModelDataTranslator::isGmtDateFieldName($field_name)) {
467
+        if ( ! ModelDataTranslator::isGmtDateFieldName($field_name)) {
468 468
             return $field_name;
469 469
         }
470
-        $query_param_sans_stars              =
470
+        $query_param_sans_stars =
471 471
             ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey(
472 472
                 $field_name
473 473
             );
@@ -508,7 +508,7 @@  discard block
 block discarded – undo
508 508
     {
509 509
         $new_array = [];
510 510
         foreach ($field_names as $key => $field_name) {
511
-            $new_array[ $key ] = ModelDataTranslator::prepareFieldNameFromJson($field_name);
511
+            $new_array[$key] = ModelDataTranslator::prepareFieldNameFromJson($field_name);
512 512
         }
513 513
         return $new_array;
514 514
     }
@@ -525,7 +525,7 @@  discard block
 block discarded – undo
525 525
     {
526 526
         $new_array = [];
527 527
         foreach ($field_names_as_keys as $field_name => $value) {
528
-            $new_array[ ModelDataTranslator::prepareFieldNameFromJson($field_name) ] = $value;
528
+            $new_array[ModelDataTranslator::prepareFieldNameFromJson($field_name)] = $value;
529 529
         }
530 530
         return $new_array;
531 531
     }
@@ -547,7 +547,7 @@  discard block
 block discarded – undo
547 547
         EEM_Base $model,
548 548
         ?string $requested_version = ''
549 549
     ): array {
550
-        if (! $requested_version) {
550
+        if ( ! $requested_version) {
551 551
             $requested_version = EED_Core_Rest_Api::latest_rest_api_version();
552 552
         }
553 553
         $rest_query_params = $model_query_params;
@@ -619,10 +619,10 @@  discard block
 block discarded – undo
619 619
                         $requested_version
620 620
                     );
621 621
                 }
622
-                $query_param_for_models[ $query_param_key ] = $translated_value;
622
+                $query_param_for_models[$query_param_key] = $translated_value;
623 623
             } else {
624 624
                 // so it's not for a field, assume it's a logic query param key
625
-                $query_param_for_models[ $query_param_key ] =
625
+                $query_param_for_models[$query_param_key] =
626 626
                     ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
627 627
                         $query_param_value,
628 628
                         $model,
@@ -674,11 +674,11 @@  discard block
 block discarded – undo
674 674
             );
675 675
         }
676 676
         $number_of_parts       = count($query_param_parts);
677
-        $last_query_param_part = $query_param_parts[ count($query_param_parts) - 1 ];
677
+        $last_query_param_part = $query_param_parts[count($query_param_parts) - 1];
678 678
         $field_name            = $last_query_param_part;
679 679
         if ($number_of_parts !== 1) {
680 680
             // the last part is the column name, and there are only 2parts. therefore...
681
-            $model = EE_Registry::instance()->load_model($query_param_parts[ $number_of_parts - 2 ]);
681
+            $model = EE_Registry::instance()->load_model($query_param_parts[$number_of_parts - 2]);
682 682
         }
683 683
         try {
684 684
             return $model->field_settings_for($field_name, false);
Please login to merge, or discard this patch.
core/libraries/rest_api/controllers/Base.php 2 patches
Indentation   +313 added lines, -313 removed lines patch added patch discarded remove patch
@@ -21,317 +21,317 @@
 block discarded – undo
21 21
  */
22 22
 class Base
23 23
 {
24
-    public const HEADER_PREFIX_FOR_EE = 'X-EE-';
25
-
26
-    public const HEADER_PREFIX_FOR_WP = 'X-WP-';
27
-
28
-    /**
29
-     * Contains debug info we'll send back in the response headers
30
-     *
31
-     * @var array
32
-     */
33
-    protected array $debug_info = [];
34
-
35
-    /**
36
-     * Indicates whether the API is in debug mode
37
-     *
38
-     * @var boolean
39
-     */
40
-    protected bool $debug_mode = false;
41
-
42
-    /**
43
-     * Indicates the version that was requested
44
-     *
45
-     * @var string
46
-     */
47
-    protected string $requested_version = '';
48
-
49
-    /**
50
-     * flat array of headers to send in the response
51
-     *
52
-     * @var array
53
-     */
54
-    protected array $response_headers = [];
55
-
56
-
57
-    public function __construct()
58
-    {
59
-        $this->debug_mode = EED_Core_Rest_Api::debugMode();
60
-        // we are handling a REST request. Don't show a fancy HTML error message is any error comes up
61
-        add_filter('FHEE__EE_Error__get_error__show_normal_exceptions', '__return_true');
62
-    }
63
-
64
-
65
-    /**
66
-     * Sets the version the user requested
67
-     *
68
-     * @param string $version eg '4.8'
69
-     */
70
-    public function setRequestedVersion(string $version)
71
-    {
72
-        $this->requested_version = $version;
73
-    }
74
-
75
-
76
-    /**
77
-     * Sets some debug info that we'll send back in headers
78
-     *
79
-     * @param string       $key
80
-     * @param string|array $info
81
-     */
82
-    protected function setDebugInfo(string $key, $info)
83
-    {
84
-        $this->debug_info[ $key ] = $info;
85
-    }
86
-
87
-
88
-    /**
89
-     * Sets headers for the response
90
-     *
91
-     * @param string       $header_key    , excluding the "X-EE-" part
92
-     * @param array|string $value         if an array, multiple headers will be added, one
93
-     *                                    for each key in the array
94
-     * @param boolean      $use_ee_prefix whether to use the EE prefix on the header, or fallback to
95
-     *                                    the standard WP one
96
-     */
97
-    protected function setResponseHeader(string $header_key, $value, bool $use_ee_prefix = true)
98
-    {
99
-        if (is_array($value)) {
100
-            foreach ($value as $value_key => $value_value) {
101
-                $this->setResponseHeader($header_key . '[' . $value_key . ']', $value_value);
102
-            }
103
-        } else {
104
-            $prefix                                          =
105
-                $use_ee_prefix ? Base::HEADER_PREFIX_FOR_EE : Base::HEADER_PREFIX_FOR_WP;
106
-            $this->response_headers[ $prefix . $header_key ] = $value;
107
-        }
108
-    }
109
-
110
-
111
-    /**
112
-     * Returns a flat array of headers to be added to the response
113
-     *
114
-     * @return array
115
-     */
116
-    protected function getResponseHeaders(): array
117
-    {
118
-        return apply_filters(
119
-            'FHEE__EventEspresso\core\libraries\rest_api\controllers\Base___get_response_headers',
120
-            $this->response_headers,
121
-            $this,
122
-            $this->requested_version
123
-        );
124
-    }
125
-
126
-
127
-    /**
128
-     * Adds error notices from EE_Error onto the provided \WP_Error
129
-     *
130
-     * @param WP_Error $wp_error_response
131
-     * @return WP_Error
132
-     */
133
-    protected function addEeErrorsToResponse(WP_Error $wp_error_response): WP_Error
134
-    {
135
-        $notices_during_checkin = EE_Error::get_raw_notices();
136
-        if (! empty($notices_during_checkin['errors'])) {
137
-            foreach ($notices_during_checkin['errors'] as $error_code => $error_message) {
138
-                $wp_error_response->add(
139
-                    sanitize_key($error_code),
140
-                    strip_tags($error_message)
141
-                );
142
-            }
143
-        }
144
-        return $wp_error_response;
145
-    }
146
-
147
-
148
-    /**
149
-     * Sends a response, but also makes sure to attach headers that
150
-     * are handy for debugging.
151
-     * Specifically, we assume folks will want to know what exactly was the DB query that got run,
152
-     * what exactly was the Models query that got run, what capabilities came into play, what fields were omitted from
153
-     * the response, others?
154
-     *
155
-     * @param array|WP_Error|Exception|RestException $response
156
-     * @return WP_REST_Response
157
-     */
158
-    public function sendResponse($response): WP_REST_Response
159
-    {
160
-        if ($response instanceof RestException) {
161
-            $response = new WP_Error($response->getStringCode(), $response->getMessage(), $response->getData());
162
-        }
163
-        if ($response instanceof Exception) {
164
-            $code     = $response->getCode() ?: 'error_occurred';
165
-            $response = new WP_Error($code, $response->getMessage());
166
-        }
167
-        if ($response instanceof WP_Error) {
168
-            $response      = $this->addEeErrorsToResponse($response);
169
-            $rest_response = $this->createRestResponseFromWpError($response);
170
-        } else {
171
-            $rest_response = new WP_REST_Response($response, 200);
172
-        }
173
-        $headers = [];
174
-        if ($this->debug_mode) {
175
-            foreach ($this->debug_info as $debug_key => $debug_info) {
176
-                if (is_array($debug_info)) {
177
-                    $debug_info = wp_json_encode($debug_info);
178
-                }
179
-                $debug_key             = ucwords(str_replace("\x20", '-', $debug_key));
180
-                $debug_key             = Base::HEADER_PREFIX_FOR_EE . '4-Debug-' . $debug_key;
181
-                $headers[ $debug_key ] = $debug_info;
182
-            }
183
-        }
184
-        $headers = array_merge(
185
-            $headers,
186
-            $this->getResponseHeaders(),
187
-            $this->getHeadersFromEeNotices()
188
-        );
189
-        $rest_response->set_headers($headers);
190
-        return $rest_response;
191
-    }
192
-
193
-
194
-    /**
195
-     * Converts the \WP_Error into `WP_REST_Response.
196
-     * Mostly this is just a copy-and-paste from \WP_REST_Server::error_to_response
197
-     * (which is protected)
198
-     *
199
-     * @param WP_Error $wp_error
200
-     * @return WP_REST_Response
201
-     */
202
-    protected function createRestResponseFromWpError(WP_Error $wp_error): WP_REST_Response
203
-    {
204
-        $error_data = $wp_error->get_error_data();
205
-        if (is_array($error_data) && isset($error_data['status'])) {
206
-            $status = $error_data['status'];
207
-        } else {
208
-            $status = 500;
209
-        }
210
-        $errors = [];
211
-        foreach ((array) $wp_error->errors as $code => $messages) {
212
-            foreach ((array) $messages as $message) {
213
-                $errors[] = [
214
-                    'code'    => $code,
215
-                    'message' => $message,
216
-                    'data'    => $wp_error->get_error_data($code),
217
-                ];
218
-            }
219
-        }
220
-        $data = $errors[0] ?? [];
221
-        if (count($errors) > 1) {
222
-            // Remove the primary error.
223
-            array_shift($errors);
224
-            $data['additional_errors'] = $errors;
225
-        }
226
-        return new WP_REST_Response($data, $status);
227
-    }
228
-
229
-
230
-    /**
231
-     * Array of headers derived from EE success, attention, and error messages
232
-     *
233
-     * @return array
234
-     */
235
-    protected function getHeadersFromEeNotices(): array
236
-    {
237
-        $headers = [];
238
-        $notices = EE_Error::get_raw_notices();
239
-        foreach ($notices as $notice_type => $sub_notices) {
240
-            if (! is_array($sub_notices)) {
241
-                continue;
242
-            }
243
-            foreach ($sub_notices as $notice_code => $sub_notice) {
244
-                $headers[ 'X-EE4-Notices-'
245
-                . EEH_Inflector::humanize($notice_type)
246
-                . '['
247
-                . $notice_code
248
-                . ']' ] = strip_tags((string) $sub_notice);
249
-            }
250
-        }
251
-        return apply_filters(
252
-            'FHEE__EventEspresso\core\libraries\rest_api\controllers\Base___get_headers_from_ee_notices__return',
253
-            $headers,
254
-            $this->requested_version,
255
-            $notices
256
-        );
257
-    }
258
-
259
-
260
-    /**
261
-     * Finds which version of the API was requested given the route, and returns it.
262
-     * eg in a request to "mysite.com/wp-json/ee/v4.8.29/events/123" this would return
263
-     * "4.8.29".
264
-     * We should know hte requested version in this model though, so if no route is
265
-     * provided just use what we set earlier
266
-     *
267
-     * @param string|null $route
268
-     * @return string
269
-     * @throws EE_Error
270
-     */
271
-    public function getRequestedVersion(?string $route = null): string
272
-    {
273
-        if ($route === null) {
274
-            return $this->requested_version;
275
-        }
276
-        $matches = $this->parseRoute(
277
-            $route,
278
-            '~' . EED_Core_Rest_Api::ee_api_namespace_for_regex . '~',
279
-            ['version']
280
-        );
281
-        return $matches['version'] ?? EED_Core_Rest_Api::latest_rest_api_version();
282
-    }
283
-
284
-
285
-    /**
286
-     * Applies the regex to the route, then creates an array using the values of
287
-     * $match_keys as keys (but ignores the full pattern match). Returns the array of matches.
288
-     * For example, if you call
289
-     * parse_route( '/ee/v4.8/events', '~\/ee\/v([^/]*)\/(.*)~', array( 'version', 'model' ) )
290
-     * it will return array( 'version' => '4.8', 'model' => 'events' )
291
-     *
292
-     * @param string $route
293
-     * @param string $regex
294
-     * @param array  $match_keys EXCLUDING matching the entire regex
295
-     * @return array where  $match_keys are the keys (the first value of $match_keys
296
-     *                           becomes the first key of the return value, etc. Eg passing in $match_keys of
297
-     *                           array( 'model', 'id' ), will, if the regex is successful, will return
298
-     *                           array( 'model' => 'foo', 'id' => 'bar' )
299
-     * @throws EE_Error if it couldn't be parsed
300
-     */
301
-    public function parseRoute(string $route, string $regex, array $match_keys): array
302
-    {
303
-        $indexed_matches = [];
304
-        $success         = preg_match($regex, $route, $matches);
305
-        if (is_array($matches)) {
306
-            // skip the overall regex match. Who cares
307
-            for ($i = 1; $i <= count($match_keys); $i++) {
308
-                if (! isset($matches[ $i ])) {
309
-                    $success = false;
310
-                } else {
311
-                    $indexed_matches[ $match_keys[ $i - 1 ] ] = $matches[ $i ];
312
-                }
313
-            }
314
-        }
315
-        if (! $success) {
316
-            throw new EE_Error(
317
-                esc_html__('We could not parse the URL. Please contact Event Espresso Support', 'event_espresso'),
318
-                'endpoint_parsing_error'
319
-            );
320
-        }
321
-        return $indexed_matches;
322
-    }
323
-
324
-
325
-    /**
326
-     * Gets the body's params (either from JSON or parsed body), which EXCLUDES the GET params and URL params
327
-     *
328
-     * @param WP_REST_Request $request
329
-     * @return array
330
-     */
331
-    protected function getBodyParams(WP_REST_Request $request): array
332
-    {
333
-        $body_params = (array) $request->get_body_params();
334
-        $json_params = (array) $request->get_json_params();
335
-        return [...$body_params, ...$json_params];
336
-    }
24
+	public const HEADER_PREFIX_FOR_EE = 'X-EE-';
25
+
26
+	public const HEADER_PREFIX_FOR_WP = 'X-WP-';
27
+
28
+	/**
29
+	 * Contains debug info we'll send back in the response headers
30
+	 *
31
+	 * @var array
32
+	 */
33
+	protected array $debug_info = [];
34
+
35
+	/**
36
+	 * Indicates whether the API is in debug mode
37
+	 *
38
+	 * @var boolean
39
+	 */
40
+	protected bool $debug_mode = false;
41
+
42
+	/**
43
+	 * Indicates the version that was requested
44
+	 *
45
+	 * @var string
46
+	 */
47
+	protected string $requested_version = '';
48
+
49
+	/**
50
+	 * flat array of headers to send in the response
51
+	 *
52
+	 * @var array
53
+	 */
54
+	protected array $response_headers = [];
55
+
56
+
57
+	public function __construct()
58
+	{
59
+		$this->debug_mode = EED_Core_Rest_Api::debugMode();
60
+		// we are handling a REST request. Don't show a fancy HTML error message is any error comes up
61
+		add_filter('FHEE__EE_Error__get_error__show_normal_exceptions', '__return_true');
62
+	}
63
+
64
+
65
+	/**
66
+	 * Sets the version the user requested
67
+	 *
68
+	 * @param string $version eg '4.8'
69
+	 */
70
+	public function setRequestedVersion(string $version)
71
+	{
72
+		$this->requested_version = $version;
73
+	}
74
+
75
+
76
+	/**
77
+	 * Sets some debug info that we'll send back in headers
78
+	 *
79
+	 * @param string       $key
80
+	 * @param string|array $info
81
+	 */
82
+	protected function setDebugInfo(string $key, $info)
83
+	{
84
+		$this->debug_info[ $key ] = $info;
85
+	}
86
+
87
+
88
+	/**
89
+	 * Sets headers for the response
90
+	 *
91
+	 * @param string       $header_key    , excluding the "X-EE-" part
92
+	 * @param array|string $value         if an array, multiple headers will be added, one
93
+	 *                                    for each key in the array
94
+	 * @param boolean      $use_ee_prefix whether to use the EE prefix on the header, or fallback to
95
+	 *                                    the standard WP one
96
+	 */
97
+	protected function setResponseHeader(string $header_key, $value, bool $use_ee_prefix = true)
98
+	{
99
+		if (is_array($value)) {
100
+			foreach ($value as $value_key => $value_value) {
101
+				$this->setResponseHeader($header_key . '[' . $value_key . ']', $value_value);
102
+			}
103
+		} else {
104
+			$prefix                                          =
105
+				$use_ee_prefix ? Base::HEADER_PREFIX_FOR_EE : Base::HEADER_PREFIX_FOR_WP;
106
+			$this->response_headers[ $prefix . $header_key ] = $value;
107
+		}
108
+	}
109
+
110
+
111
+	/**
112
+	 * Returns a flat array of headers to be added to the response
113
+	 *
114
+	 * @return array
115
+	 */
116
+	protected function getResponseHeaders(): array
117
+	{
118
+		return apply_filters(
119
+			'FHEE__EventEspresso\core\libraries\rest_api\controllers\Base___get_response_headers',
120
+			$this->response_headers,
121
+			$this,
122
+			$this->requested_version
123
+		);
124
+	}
125
+
126
+
127
+	/**
128
+	 * Adds error notices from EE_Error onto the provided \WP_Error
129
+	 *
130
+	 * @param WP_Error $wp_error_response
131
+	 * @return WP_Error
132
+	 */
133
+	protected function addEeErrorsToResponse(WP_Error $wp_error_response): WP_Error
134
+	{
135
+		$notices_during_checkin = EE_Error::get_raw_notices();
136
+		if (! empty($notices_during_checkin['errors'])) {
137
+			foreach ($notices_during_checkin['errors'] as $error_code => $error_message) {
138
+				$wp_error_response->add(
139
+					sanitize_key($error_code),
140
+					strip_tags($error_message)
141
+				);
142
+			}
143
+		}
144
+		return $wp_error_response;
145
+	}
146
+
147
+
148
+	/**
149
+	 * Sends a response, but also makes sure to attach headers that
150
+	 * are handy for debugging.
151
+	 * Specifically, we assume folks will want to know what exactly was the DB query that got run,
152
+	 * what exactly was the Models query that got run, what capabilities came into play, what fields were omitted from
153
+	 * the response, others?
154
+	 *
155
+	 * @param array|WP_Error|Exception|RestException $response
156
+	 * @return WP_REST_Response
157
+	 */
158
+	public function sendResponse($response): WP_REST_Response
159
+	{
160
+		if ($response instanceof RestException) {
161
+			$response = new WP_Error($response->getStringCode(), $response->getMessage(), $response->getData());
162
+		}
163
+		if ($response instanceof Exception) {
164
+			$code     = $response->getCode() ?: 'error_occurred';
165
+			$response = new WP_Error($code, $response->getMessage());
166
+		}
167
+		if ($response instanceof WP_Error) {
168
+			$response      = $this->addEeErrorsToResponse($response);
169
+			$rest_response = $this->createRestResponseFromWpError($response);
170
+		} else {
171
+			$rest_response = new WP_REST_Response($response, 200);
172
+		}
173
+		$headers = [];
174
+		if ($this->debug_mode) {
175
+			foreach ($this->debug_info as $debug_key => $debug_info) {
176
+				if (is_array($debug_info)) {
177
+					$debug_info = wp_json_encode($debug_info);
178
+				}
179
+				$debug_key             = ucwords(str_replace("\x20", '-', $debug_key));
180
+				$debug_key             = Base::HEADER_PREFIX_FOR_EE . '4-Debug-' . $debug_key;
181
+				$headers[ $debug_key ] = $debug_info;
182
+			}
183
+		}
184
+		$headers = array_merge(
185
+			$headers,
186
+			$this->getResponseHeaders(),
187
+			$this->getHeadersFromEeNotices()
188
+		);
189
+		$rest_response->set_headers($headers);
190
+		return $rest_response;
191
+	}
192
+
193
+
194
+	/**
195
+	 * Converts the \WP_Error into `WP_REST_Response.
196
+	 * Mostly this is just a copy-and-paste from \WP_REST_Server::error_to_response
197
+	 * (which is protected)
198
+	 *
199
+	 * @param WP_Error $wp_error
200
+	 * @return WP_REST_Response
201
+	 */
202
+	protected function createRestResponseFromWpError(WP_Error $wp_error): WP_REST_Response
203
+	{
204
+		$error_data = $wp_error->get_error_data();
205
+		if (is_array($error_data) && isset($error_data['status'])) {
206
+			$status = $error_data['status'];
207
+		} else {
208
+			$status = 500;
209
+		}
210
+		$errors = [];
211
+		foreach ((array) $wp_error->errors as $code => $messages) {
212
+			foreach ((array) $messages as $message) {
213
+				$errors[] = [
214
+					'code'    => $code,
215
+					'message' => $message,
216
+					'data'    => $wp_error->get_error_data($code),
217
+				];
218
+			}
219
+		}
220
+		$data = $errors[0] ?? [];
221
+		if (count($errors) > 1) {
222
+			// Remove the primary error.
223
+			array_shift($errors);
224
+			$data['additional_errors'] = $errors;
225
+		}
226
+		return new WP_REST_Response($data, $status);
227
+	}
228
+
229
+
230
+	/**
231
+	 * Array of headers derived from EE success, attention, and error messages
232
+	 *
233
+	 * @return array
234
+	 */
235
+	protected function getHeadersFromEeNotices(): array
236
+	{
237
+		$headers = [];
238
+		$notices = EE_Error::get_raw_notices();
239
+		foreach ($notices as $notice_type => $sub_notices) {
240
+			if (! is_array($sub_notices)) {
241
+				continue;
242
+			}
243
+			foreach ($sub_notices as $notice_code => $sub_notice) {
244
+				$headers[ 'X-EE4-Notices-'
245
+				. EEH_Inflector::humanize($notice_type)
246
+				. '['
247
+				. $notice_code
248
+				. ']' ] = strip_tags((string) $sub_notice);
249
+			}
250
+		}
251
+		return apply_filters(
252
+			'FHEE__EventEspresso\core\libraries\rest_api\controllers\Base___get_headers_from_ee_notices__return',
253
+			$headers,
254
+			$this->requested_version,
255
+			$notices
256
+		);
257
+	}
258
+
259
+
260
+	/**
261
+	 * Finds which version of the API was requested given the route, and returns it.
262
+	 * eg in a request to "mysite.com/wp-json/ee/v4.8.29/events/123" this would return
263
+	 * "4.8.29".
264
+	 * We should know hte requested version in this model though, so if no route is
265
+	 * provided just use what we set earlier
266
+	 *
267
+	 * @param string|null $route
268
+	 * @return string
269
+	 * @throws EE_Error
270
+	 */
271
+	public function getRequestedVersion(?string $route = null): string
272
+	{
273
+		if ($route === null) {
274
+			return $this->requested_version;
275
+		}
276
+		$matches = $this->parseRoute(
277
+			$route,
278
+			'~' . EED_Core_Rest_Api::ee_api_namespace_for_regex . '~',
279
+			['version']
280
+		);
281
+		return $matches['version'] ?? EED_Core_Rest_Api::latest_rest_api_version();
282
+	}
283
+
284
+
285
+	/**
286
+	 * Applies the regex to the route, then creates an array using the values of
287
+	 * $match_keys as keys (but ignores the full pattern match). Returns the array of matches.
288
+	 * For example, if you call
289
+	 * parse_route( '/ee/v4.8/events', '~\/ee\/v([^/]*)\/(.*)~', array( 'version', 'model' ) )
290
+	 * it will return array( 'version' => '4.8', 'model' => 'events' )
291
+	 *
292
+	 * @param string $route
293
+	 * @param string $regex
294
+	 * @param array  $match_keys EXCLUDING matching the entire regex
295
+	 * @return array where  $match_keys are the keys (the first value of $match_keys
296
+	 *                           becomes the first key of the return value, etc. Eg passing in $match_keys of
297
+	 *                           array( 'model', 'id' ), will, if the regex is successful, will return
298
+	 *                           array( 'model' => 'foo', 'id' => 'bar' )
299
+	 * @throws EE_Error if it couldn't be parsed
300
+	 */
301
+	public function parseRoute(string $route, string $regex, array $match_keys): array
302
+	{
303
+		$indexed_matches = [];
304
+		$success         = preg_match($regex, $route, $matches);
305
+		if (is_array($matches)) {
306
+			// skip the overall regex match. Who cares
307
+			for ($i = 1; $i <= count($match_keys); $i++) {
308
+				if (! isset($matches[ $i ])) {
309
+					$success = false;
310
+				} else {
311
+					$indexed_matches[ $match_keys[ $i - 1 ] ] = $matches[ $i ];
312
+				}
313
+			}
314
+		}
315
+		if (! $success) {
316
+			throw new EE_Error(
317
+				esc_html__('We could not parse the URL. Please contact Event Espresso Support', 'event_espresso'),
318
+				'endpoint_parsing_error'
319
+			);
320
+		}
321
+		return $indexed_matches;
322
+	}
323
+
324
+
325
+	/**
326
+	 * Gets the body's params (either from JSON or parsed body), which EXCLUDES the GET params and URL params
327
+	 *
328
+	 * @param WP_REST_Request $request
329
+	 * @return array
330
+	 */
331
+	protected function getBodyParams(WP_REST_Request $request): array
332
+	{
333
+		$body_params = (array) $request->get_body_params();
334
+		$json_params = (array) $request->get_json_params();
335
+		return [...$body_params, ...$json_params];
336
+	}
337 337
 }
Please login to merge, or discard this patch.
Spacing   +13 added lines, -13 removed lines patch added patch discarded remove patch
@@ -81,7 +81,7 @@  discard block
 block discarded – undo
81 81
      */
82 82
     protected function setDebugInfo(string $key, $info)
83 83
     {
84
-        $this->debug_info[ $key ] = $info;
84
+        $this->debug_info[$key] = $info;
85 85
     }
86 86
 
87 87
 
@@ -98,12 +98,12 @@  discard block
 block discarded – undo
98 98
     {
99 99
         if (is_array($value)) {
100 100
             foreach ($value as $value_key => $value_value) {
101
-                $this->setResponseHeader($header_key . '[' . $value_key . ']', $value_value);
101
+                $this->setResponseHeader($header_key.'['.$value_key.']', $value_value);
102 102
             }
103 103
         } else {
104 104
             $prefix                                          =
105 105
                 $use_ee_prefix ? Base::HEADER_PREFIX_FOR_EE : Base::HEADER_PREFIX_FOR_WP;
106
-            $this->response_headers[ $prefix . $header_key ] = $value;
106
+            $this->response_headers[$prefix.$header_key] = $value;
107 107
         }
108 108
     }
109 109
 
@@ -133,7 +133,7 @@  discard block
 block discarded – undo
133 133
     protected function addEeErrorsToResponse(WP_Error $wp_error_response): WP_Error
134 134
     {
135 135
         $notices_during_checkin = EE_Error::get_raw_notices();
136
-        if (! empty($notices_during_checkin['errors'])) {
136
+        if ( ! empty($notices_during_checkin['errors'])) {
137 137
             foreach ($notices_during_checkin['errors'] as $error_code => $error_message) {
138 138
                 $wp_error_response->add(
139 139
                     sanitize_key($error_code),
@@ -177,8 +177,8 @@  discard block
 block discarded – undo
177 177
                     $debug_info = wp_json_encode($debug_info);
178 178
                 }
179 179
                 $debug_key             = ucwords(str_replace("\x20", '-', $debug_key));
180
-                $debug_key             = Base::HEADER_PREFIX_FOR_EE . '4-Debug-' . $debug_key;
181
-                $headers[ $debug_key ] = $debug_info;
180
+                $debug_key             = Base::HEADER_PREFIX_FOR_EE.'4-Debug-'.$debug_key;
181
+                $headers[$debug_key] = $debug_info;
182 182
             }
183 183
         }
184 184
         $headers = array_merge(
@@ -237,15 +237,15 @@  discard block
 block discarded – undo
237 237
         $headers = [];
238 238
         $notices = EE_Error::get_raw_notices();
239 239
         foreach ($notices as $notice_type => $sub_notices) {
240
-            if (! is_array($sub_notices)) {
240
+            if ( ! is_array($sub_notices)) {
241 241
                 continue;
242 242
             }
243 243
             foreach ($sub_notices as $notice_code => $sub_notice) {
244
-                $headers[ 'X-EE4-Notices-'
244
+                $headers['X-EE4-Notices-'
245 245
                 . EEH_Inflector::humanize($notice_type)
246 246
                 . '['
247 247
                 . $notice_code
248
-                . ']' ] = strip_tags((string) $sub_notice);
248
+                . ']'] = strip_tags((string) $sub_notice);
249 249
             }
250 250
         }
251 251
         return apply_filters(
@@ -275,7 +275,7 @@  discard block
 block discarded – undo
275 275
         }
276 276
         $matches = $this->parseRoute(
277 277
             $route,
278
-            '~' . EED_Core_Rest_Api::ee_api_namespace_for_regex . '~',
278
+            '~'.EED_Core_Rest_Api::ee_api_namespace_for_regex.'~',
279 279
             ['version']
280 280
         );
281 281
         return $matches['version'] ?? EED_Core_Rest_Api::latest_rest_api_version();
@@ -305,14 +305,14 @@  discard block
 block discarded – undo
305 305
         if (is_array($matches)) {
306 306
             // skip the overall regex match. Who cares
307 307
             for ($i = 1; $i <= count($match_keys); $i++) {
308
-                if (! isset($matches[ $i ])) {
308
+                if ( ! isset($matches[$i])) {
309 309
                     $success = false;
310 310
                 } else {
311
-                    $indexed_matches[ $match_keys[ $i - 1 ] ] = $matches[ $i ];
311
+                    $indexed_matches[$match_keys[$i - 1]] = $matches[$i];
312 312
                 }
313 313
             }
314 314
         }
315
-        if (! $success) {
315
+        if ( ! $success) {
316 316
             throw new EE_Error(
317 317
                 esc_html__('We could not parse the URL. Please contact Event Espresso Support', 'event_espresso'),
318 318
                 'endpoint_parsing_error'
Please login to merge, or discard this patch.
core/libraries/rest_api/controllers/model/Read.php 1 patch
Indentation   +1686 added lines, -1686 removed lines patch added patch discarded remove patch
@@ -51,1690 +51,1690 @@
 block discarded – undo
51 51
  */
52 52
 class Read extends Base
53 53
 {
54
-    protected CalculatedModelFields $fields_calculator;
55
-
56
-
57
-    /**
58
-     * Read constructor.
59
-     *
60
-     * @param CalculatedModelFields $fields_calculator
61
-     */
62
-    public function __construct(CalculatedModelFields $fields_calculator)
63
-    {
64
-        parent::__construct();
65
-        $this->fields_calculator = $fields_calculator;
66
-    }
67
-
68
-
69
-    /**
70
-     * @return Read
71
-     */
72
-    private static function getReadController(): Read
73
-    {
74
-        return LoaderFactory::getLoader()->getNew(Read::class);
75
-    }
76
-
77
-
78
-    /**
79
-     * Handles requests to get all (or a filtered subset) of entities for a particular model
80
-     *
81
-     * @param WP_REST_Request $request
82
-     * @param string          $version
83
-     * @param string          $model_name
84
-     * @return WP_REST_Response
85
-     * @throws InvalidArgumentException
86
-     * @throws InvalidDataTypeException
87
-     * @throws InvalidInterfaceException
88
-     */
89
-    public static function handleRequestGetAll(
90
-        WP_REST_Request $request,
91
-        string $version,
92
-        string $model_name
93
-    ): WP_REST_Response {
94
-        $controller = self::getReadController();
95
-        try {
96
-            $controller->setRequestedVersion($version);
97
-            if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
98
-                return $controller->sendResponse(
99
-                    new WP_Error(
100
-                        'endpoint_parsing_error',
101
-                        sprintf(
102
-                            esc_html__(
103
-                                'There is no model for endpoint %s. Please contact event espresso support',
104
-                                'event_espresso'
105
-                            ),
106
-                            $model_name
107
-                        )
108
-                    )
109
-                );
110
-            }
111
-            return $controller->sendResponse(
112
-                $controller->getEntitiesFromModel(
113
-                    $controller->getModelVersionInfo()->loadModel($model_name),
114
-                    $request
115
-                )
116
-            );
117
-        } catch (Exception $e) {
118
-            return $controller->sendResponse($e);
119
-        }
120
-    }
121
-
122
-
123
-    /**
124
-     * Prepares and returns schema for any OPTIONS request.
125
-     *
126
-     * @param string $version    The API endpoint version being used.
127
-     * @param string $model_name Something like `Event` or `Registration`
128
-     * @return array
129
-     * @throws InvalidArgumentException
130
-     * @throws InvalidDataTypeException
131
-     * @throws InvalidInterfaceException
132
-     */
133
-    public static function handleSchemaRequest(string $version, string $model_name): array
134
-    {
135
-        $controller = self::getReadController();
136
-        try {
137
-            $controller->setRequestedVersion($version);
138
-            if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
139
-                return [];
140
-            }
141
-            // get the model for this version
142
-            $model        = $controller->getModelVersionInfo()->loadModel($model_name);
143
-            $model_schema = new JsonModelSchema(
144
-                $model,
145
-                LoaderFactory::getLoader()->getShared('EventEspresso\core\libraries\rest_api\CalculatedModelFields')
146
-            );
147
-            return $model_schema->getModelSchemaForRelations(
148
-                $controller->getModelVersionInfo()->relationSettings($model),
149
-                $controller->customizeSchemaForRestResponse(
150
-                    $model,
151
-                    $model_schema->getModelSchemaForFields(
152
-                        $controller->getModelVersionInfo()->fieldsOnModelInThisVersion($model),
153
-                        $model_schema->getInitialSchemaStructure()
154
-                    )
155
-                )
156
-            );
157
-        } catch (Exception $e) {
158
-            error_log($e->getMessage());
159
-            return [];
160
-        }
161
-    }
162
-
163
-
164
-    /**
165
-     * This loops through each field in the given schema for the model and does the following:
166
-     * - add any extra fields that are REST API specific and related to existing fields.
167
-     * - transform default values into the correct format for a REST API response.
168
-     *
169
-     * @param EEM_Base $model
170
-     * @param array    $schema
171
-     * @return array  The final schema.
172
-     * @throws EE_Error
173
-     */
174
-    public function customizeSchemaForRestResponse(EEM_Base $model, array $schema): array
175
-    {
176
-        foreach ($this->getModelVersionInfo()->fieldsOnModelInThisVersion($model) as $field_name => $field) {
177
-            $schema = $this->translateDefaultsForRestResponse(
178
-                $field_name,
179
-                $field,
180
-                $this->maybeAddExtraFieldsToSchema($field_name, $field, $schema)
181
-            );
182
-        }
183
-        return $schema;
184
-    }
185
-
186
-
187
-    /**
188
-     * This is used to ensure that the 'default' value set in the schema response is formatted correctly for the REST
189
-     * response.
190
-     *
191
-     * @param                      $field_name
192
-     * @param EE_Model_Field_Base  $field
193
-     * @param array                $schema
194
-     * @return array
195
-     * @throws RestException  if a default value has a PHP object, which we should never do
196
-     *                                  (but if we did, let's know about it ASAP, so let the exception bubble up)
197
-     * @throws EE_Error
198
-     *
199
-     */
200
-    protected function translateDefaultsForRestResponse($field_name, EE_Model_Field_Base $field, array $schema): array
201
-    {
202
-        if (isset($schema['properties'][ $field_name ]['default'])) {
203
-            if (is_array($schema['properties'][ $field_name ]['default'])) {
204
-                foreach ($schema['properties'][ $field_name ]['default'] as $default_key => $default_value) {
205
-                    if ($default_key === 'raw') {
206
-                        $schema['properties'][ $field_name ]['default'][ $default_key ] =
207
-                            ModelDataTranslator::prepareFieldValueForJson(
208
-                                $field,
209
-                                $default_value,
210
-                                $this->getModelVersionInfo()->requestedVersion()
211
-                            );
212
-                    }
213
-                }
214
-            } else {
215
-                $schema['properties'][ $field_name ]['default'] = ModelDataTranslator::prepareFieldValueForJson(
216
-                    $field,
217
-                    $schema['properties'][ $field_name ]['default'],
218
-                    $this->getModelVersionInfo()->requestedVersion()
219
-                );
220
-            }
221
-        }
222
-        return $schema;
223
-    }
224
-
225
-
226
-    /**
227
-     * Adds additional fields to the schema
228
-     * The REST API returns a GMT value field for each datetime field in the resource.  Thus the description about this
229
-     * needs to be added to the schema.
230
-     *
231
-     * @param                      $field_name
232
-     * @param EE_Model_Field_Base  $field
233
-     * @param array                $schema
234
-     * @return array
235
-     */
236
-    protected function maybeAddExtraFieldsToSchema($field_name, EE_Model_Field_Base $field, array $schema): array
237
-    {
238
-        if ($field instanceof EE_Datetime_Field) {
239
-            $schema['properties'][ $field_name . '_gmt' ] = $field->getSchema();
240
-            // modify the description
241
-            $schema['properties'][ $field_name . '_gmt' ]['description'] = sprintf(
242
-                esc_html__('%s - the value for this field is in GMT.', 'event_espresso'),
243
-                wp_specialchars_decode($field->get_nicename(), ENT_QUOTES)
244
-            );
245
-        }
246
-        return $schema;
247
-    }
248
-
249
-
250
-    /**
251
-     * Used to figure out the route from the request when a `WP_REST_Request` object is not available
252
-     *
253
-     * @return string
254
-     */
255
-    protected function getRouteFromRequest(): string
256
-    {
257
-        if (
258
-            isset($GLOBALS['wp'])
259
-            && $GLOBALS['wp'] instanceof WP
260
-            && isset($GLOBALS['wp']->query_vars['rest_route'])
261
-        ) {
262
-            return $GLOBALS['wp']->query_vars['rest_route'];
263
-        }
264
-        /** @var RequestInterface $request */
265
-        $request = LoaderFactory::getLoader()->getShared(RequestInterface::class);
266
-        return $request->serverParamIsSet('PATH_INFO')
267
-            ? $request->getServerParam('PATH_INFO')
268
-            : '/';
269
-    }
270
-
271
-
272
-    /**
273
-     * Gets a single entity related to the model indicated in the path and its id
274
-     *
275
-     * @param WP_REST_Request $request
276
-     * @param string          $version
277
-     * @param string          $model_name
278
-     * @return WP_REST_Response
279
-     * @throws InvalidDataTypeException
280
-     * @throws InvalidInterfaceException
281
-     * @throws InvalidArgumentException
282
-     */
283
-    public static function handleRequestGetOne(
284
-        WP_REST_Request $request,
285
-        string $version,
286
-        string $model_name
287
-    ): WP_REST_Response {
288
-        $controller = self::getReadController();
289
-        try {
290
-            $controller->setRequestedVersion($version);
291
-            if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
292
-                return $controller->sendResponse(
293
-                    new WP_Error(
294
-                        'endpoint_parsing_error',
295
-                        sprintf(
296
-                            esc_html__(
297
-                                'There is no model for endpoint %s. Please contact event espresso support',
298
-                                'event_espresso'
299
-                            ),
300
-                            $model_name
301
-                        )
302
-                    )
303
-                );
304
-            }
305
-            return $controller->sendResponse(
306
-                $controller->getEntityFromModel(
307
-                    $controller->getModelVersionInfo()->loadModel($model_name),
308
-                    $request
309
-                )
310
-            );
311
-        } catch (Exception $e) {
312
-            return $controller->sendResponse($e);
313
-        }
314
-    }
315
-
316
-
317
-    /**
318
-     * Gets all the related entities (or if its a belongs-to relation just the one)
319
-     * to the item with the given id
320
-     *
321
-     * @param WP_REST_Request $request
322
-     * @param string          $version
323
-     * @param string          $model_name
324
-     * @param string          $related_model_name
325
-     * @return WP_REST_Response
326
-     * @throws InvalidDataTypeException
327
-     * @throws InvalidInterfaceException
328
-     * @throws InvalidArgumentException
329
-     */
330
-    public static function handleRequestGetRelated(
331
-        WP_REST_Request $request,
332
-        string $version,
333
-        string $model_name,
334
-        string $related_model_name
335
-    ): WP_REST_Response {
336
-        $controller = self::getReadController();
337
-        try {
338
-            $controller->setRequestedVersion($version);
339
-            $main_model = $controller->validateModel($model_name);
340
-            $controller->validateModel($related_model_name);
341
-            return $controller->sendResponse(
342
-                $controller->getEntitiesFromRelation(
343
-                    $request->get_param('id'),
344
-                    $main_model->related_settings_for($related_model_name),
345
-                    $request
346
-                )
347
-            );
348
-        } catch (Exception $e) {
349
-            return $controller->sendResponse($e);
350
-        }
351
-    }
352
-
353
-
354
-    /**
355
-     * Gets a collection for the given model and filters
356
-     *
357
-     * @param EEM_Base        $model
358
-     * @param WP_REST_Request $request
359
-     * @return array
360
-     * @throws DomainException
361
-     * @throws EE_Error
362
-     * @throws InvalidArgumentException
363
-     * @throws InvalidDataTypeException
364
-     * @throws InvalidInterfaceException
365
-     * @throws ModelConfigurationException
366
-     * @throws ReflectionException
367
-     * @throws RestException
368
-     * @throws RestPasswordIncorrectException
369
-     * @throws RestPasswordRequiredException
370
-     * @throws UnexpectedEntityException
371
-     */
372
-    public function getEntitiesFromModel(EEM_Base $model, WP_REST_Request $request): array
373
-    {
374
-        $query_params = $this->createModelQueryParams($model, $request->get_params());
375
-        if (! Capabilities::currentUserHasPartialAccessTo($model, $query_params['caps'])) {
376
-            $model_name_plural = EEH_Inflector::pluralize_and_lower($model->get_this_model_name());
377
-            throw new RestException(
378
-                sprintf('rest_%s_cannot_list', $model_name_plural),
379
-                sprintf(
380
-                    esc_html__('Sorry, you are not allowed to list %1$s. Missing permissions: %2$s', 'event_espresso'),
381
-                    $model_name_plural,
382
-                    Capabilities::getMissingPermissionsString($model, $query_params['caps'])
383
-                ),
384
-                ['status' => 403]
385
-            );
386
-        }
387
-        if (! $request->get_header('no_rest_headers')) {
388
-            $this->setHeadersFromQueryParams($model, $query_params);
389
-        }
390
-        /** @type array $results */
391
-        $results      = $model->get_all_wpdb_results($query_params);
392
-        $nice_results = [];
393
-        foreach ($results as $result) {
394
-            $nice_results[] = $this->createEntityFromWpdbResult(
395
-                $model,
396
-                $result,
397
-                $request
398
-            );
399
-        }
400
-        return $nice_results;
401
-    }
402
-
403
-
404
-    /**
405
-     * Gets the collection for given relation object
406
-     * The same as Read::get_entities_from_model(), except if the relation
407
-     * is a HABTM relation, in which case it merges any non-foreign-key fields from
408
-     * the join-model-object into the results
409
-     *
410
-     * @param array                  $primary_model_query_params  query params for finding the item from which
411
-     *                                                            relations will be based
412
-     * @param EE_Model_Relation_Base $relation
413
-     * @param WP_REST_Request        $request
414
-     * @return array|null
415
-     * @throws DomainException
416
-     * @throws EE_Error
417
-     * @throws InvalidArgumentException
418
-     * @throws InvalidDataTypeException
419
-     * @throws InvalidInterfaceException
420
-     * @throws ModelConfigurationException
421
-     * @throws ReflectionException
422
-     * @throws RestException
423
-     * @throws ModelConfigurationException
424
-     * @throws UnexpectedEntityException
425
-     * @throws Exception
426
-     */
427
-    protected function getEntitiesFromRelationUsingModelQueryParams(
428
-        array $primary_model_query_params,
429
-        EE_Model_Relation_Base $relation,
430
-        WP_REST_Request $request
431
-    ): ?array {
432
-        $context       = $this->validateContext($request->get_param('caps'));
433
-        $model         = $relation->get_this_model();
434
-        $related_model = $relation->get_other_model();
435
-        if (! isset($primary_model_query_params[0])) {
436
-            $primary_model_query_params[0] = [];
437
-        }
438
-        // check if they can access the 1st model object
439
-        $primary_model_query_params = [
440
-            0       => $primary_model_query_params[0],
441
-            'limit' => 1,
442
-        ];
443
-        if ($model instanceof EEM_Soft_Delete_Base) {
444
-            $primary_model_query_params = $model->alter_query_params_so_deleted_and_undeleted_items_included(
445
-                $primary_model_query_params
446
-            );
447
-        }
448
-        $restricted_query_params          = $primary_model_query_params;
449
-        $restricted_query_params['caps']  = $context;
450
-        $restricted_query_params['limit'] = 1;
451
-        $this->setDebugInfo('main model query params', $restricted_query_params);
452
-        $this->setDebugInfo('missing caps', Capabilities::getMissingPermissionsString($related_model, $context));
453
-        $primary_model_rows = $model->get_all_wpdb_results($restricted_query_params);
454
-        $primary_model_row  = null;
455
-        if (is_array($primary_model_rows)) {
456
-            $primary_model_row = reset($primary_model_rows);
457
-        }
458
-        if (
459
-            ! (
460
-                $primary_model_row
461
-                && Capabilities::currentUserHasPartialAccessTo($related_model, $context)
462
-            )
463
-        ) {
464
-            if ($relation instanceof EE_Belongs_To_Relation) {
465
-                $related_model_name_maybe_plural = strtolower($related_model->get_this_model_name());
466
-            } else {
467
-                $related_model_name_maybe_plural = EEH_Inflector::pluralize_and_lower(
468
-                    $related_model->get_this_model_name()
469
-                );
470
-            }
471
-            throw new RestException(
472
-                sprintf('rest_%s_cannot_list', $related_model_name_maybe_plural),
473
-                sprintf(
474
-                    esc_html__(
475
-                        'Sorry, you are not allowed to list %1$s related to %2$s. Missing permissions: %3$s',
476
-                        'event_espresso'
477
-                    ),
478
-                    $related_model_name_maybe_plural,
479
-                    $relation->get_this_model()->get_this_model_name(),
480
-                    implode(
481
-                        ',',
482
-                        array_keys(
483
-                            Capabilities::getMissingPermissions($related_model, $context)
484
-                        )
485
-                    )
486
-                ),
487
-                ['status' => 403]
488
-            );
489
-        }
490
-
491
-        $this->checkPassword(
492
-            $model,
493
-            $primary_model_row,
494
-            $restricted_query_params,
495
-            $request
496
-        );
497
-        $query_params = $this->createModelQueryParams($relation->get_other_model(), $request->get_params());
498
-        foreach ($primary_model_query_params[0] as $where_condition_key => $where_condition_value) {
499
-            $query_params[0][ $relation->get_this_model()->get_this_model_name()
500
-                              . '.'
501
-                              . $where_condition_key ] = $where_condition_value;
502
-        }
503
-        $query_params['default_where_conditions'] = 'none';
504
-        $query_params['caps']                     = $context;
505
-        if (! $request->get_header('no_rest_headers')) {
506
-            $this->setHeadersFromQueryParams($relation->get_other_model(), $query_params);
507
-        }
508
-        /** @type array $results */
509
-        $results      = $relation->get_other_model()->get_all_wpdb_results($query_params);
510
-        $nice_results = [];
511
-        foreach ($results as $result) {
512
-            $nice_result = $this->createEntityFromWpdbResult(
513
-                $relation->get_other_model(),
514
-                $result,
515
-                $request
516
-            );
517
-            if ($relation instanceof EE_HABTM_Relation) {
518
-                // put the unusual stuff (properties from the HABTM relation) first, and make sure
519
-                // if there are conflicts we prefer the properties from the main model
520
-                $join_model_result = $this->createEntityFromWpdbResult(
521
-                    $relation->get_join_model(),
522
-                    $result,
523
-                    $request
524
-                );
525
-                $joined_result     = array_merge($join_model_result, $nice_result);
526
-                // but keep the meta stuff from the main model
527
-                if (isset($nice_result['meta'])) {
528
-                    $joined_result['meta'] = $nice_result['meta'];
529
-                }
530
-                $nice_result = $joined_result;
531
-            }
532
-            $nice_results[] = $nice_result;
533
-        }
534
-        if ($relation instanceof EE_Belongs_To_Relation) {
535
-            return array_shift($nice_results);
536
-        } else {
537
-            return $nice_results;
538
-        }
539
-    }
540
-
541
-
542
-    /**
543
-     * Gets the collection for given relation object
544
-     * The same as Read::get_entities_from_model(), except if the relation
545
-     * is a HABTM relation, in which case it merges any non-foreign-key fields from
546
-     * the join-model-object into the results
547
-     *
548
-     * @param int|string             $id the ID of the thing we are fetching related stuff from
549
-     * @param EE_Model_Relation_Base $relation
550
-     * @param WP_REST_Request        $request
551
-     * @return array
552
-     * @throws DomainException
553
-     * @throws EE_Error
554
-     * @throws InvalidArgumentException
555
-     * @throws InvalidDataTypeException
556
-     * @throws InvalidInterfaceException
557
-     * @throws ReflectionException
558
-     * @throws RestException
559
-     * @throws ModelConfigurationException
560
-     * @throws UnexpectedEntityException
561
-     * @throws Exception
562
-     */
563
-    public function getEntitiesFromRelation($id, EE_Model_Relation_Base $relation, WP_REST_Request $request): array
564
-    {
565
-        if (! $relation->get_this_model()->has_primary_key_field()) {
566
-            throw new EE_Error(
567
-                sprintf(
568
-                    esc_html__(
569
-                    // @codingStandardsIgnoreStart
570
-                        'Read::get_entities_from_relation should only be called from a model with a primary key, it was called from %1$s',
571
-                        // @codingStandardsIgnoreEnd
572
-                        'event_espresso'
573
-                    ),
574
-                    $relation->get_this_model()->get_this_model_name()
575
-                )
576
-            );
577
-        }
578
-        // can we edit that main item?
579
-        // if not, show nothing but an error
580
-        // otherwise, please proceed
581
-        return $this->getEntitiesFromRelationUsingModelQueryParams(
582
-            [
583
-                [
584
-                    $relation->get_this_model()->primary_key_name() => $id,
585
-                ],
586
-            ],
587
-            $relation,
588
-            $request
589
-        );
590
-    }
591
-
592
-
593
-    /**
594
-     * Sets the headers that are based on the model and query params,
595
-     * like the total records. This should only be called on the original request
596
-     * from the client, not on subsequent internal
597
-     *
598
-     * @param EEM_Base $model
599
-     * @param array    $query_params
600
-     * @return void
601
-     * @throws EE_Error
602
-     * @throws ReflectionException
603
-     */
604
-    protected function setHeadersFromQueryParams(EEM_Base $model, array $query_params)
605
-    {
606
-        $this->setDebugInfo('model query params', $query_params);
607
-        $this->setDebugInfo(
608
-            'missing caps',
609
-            Capabilities::getMissingPermissionsString($model, $query_params['caps'])
610
-        );
611
-        // normally the limit to a 2-part array, where the 2nd item is the limit
612
-        if (! isset($query_params['limit'])) {
613
-            $query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
614
-        }
615
-        if (is_array($query_params['limit'])) {
616
-            $limit_parts = $query_params['limit'];
617
-        } else {
618
-            $limit_parts = explode(',', $query_params['limit']);
619
-            if (count($limit_parts) === 1) {
620
-                $limit_parts = [0, $limit_parts[0]];
621
-            }
622
-        }
623
-        // remove the group by and having parts of the query, as those will
624
-        // make the sql query return an array of values, instead of just a single value
625
-        unset($query_params['group_by'], $query_params['having'], $query_params['limit']);
626
-        $count = $model->count($query_params, null, true);
627
-        $pages = $count / $limit_parts[1];
628
-        $this->setResponseHeader('Total', $count, false);
629
-        $this->setResponseHeader('PageSize', $limit_parts[1], false);
630
-        $this->setResponseHeader('TotalPages', ceil($pages), false);
631
-    }
632
-
633
-
634
-    /**
635
-     * Changes database results into REST API entities
636
-     *
637
-     * @param EEM_Base             $model
638
-     * @param array                $db_row     like results from $wpdb->get_results()
639
-     * @param WP_REST_Request|null $rest_request
640
-     * @param string|null          $deprecated no longer used
641
-     * @return array ready for being converted into json for sending to client
642
-     * @throws DomainException
643
-     * @throws EE_Error
644
-     * @throws InvalidArgumentException
645
-     * @throws InvalidDataTypeException
646
-     * @throws InvalidInterfaceException
647
-     * @throws ModelConfigurationException
648
-     * @throws ReflectionException
649
-     * @throws RestException
650
-     * @throws RestPasswordIncorrectException
651
-     * @throws RestPasswordRequiredException
652
-     * @throws UnexpectedEntityException
653
-     */
654
-    public function createEntityFromWpdbResult(
655
-        EEM_Base $model,
656
-        array $db_row,
657
-        ?WP_REST_Request $rest_request,
658
-        string $deprecated = null
659
-    ): array {
660
-        if (! $rest_request instanceof WP_REST_Request) {
661
-            // ok so this was called in the old style, where the 3rd arg was
662
-            // $include, and the 4th arg was $context
663
-            // now setup the request just to avoid fatal errors, although we won't be able
664
-            // to truly make use of it because it's kinda devoid of info
665
-            $rest_request = new WP_REST_Request();
666
-            $rest_request->set_param('include', $rest_request);
667
-            $rest_request->set_param('caps', $deprecated);
668
-        }
669
-        if ($rest_request->get_param('caps') === null) {
670
-            $rest_request->set_param('caps', EEM_Base::caps_read);
671
-        }
672
-        $current_user_full_access_to_entity = $model->currentUserCan(
673
-            EEM_Base::caps_read_admin,
674
-            $model->deduce_fields_n_values_from_cols_n_values($db_row)
675
-        );
676
-        $entity_array                       = $this->createBareEntityFromWpdbResults($model, $db_row);
677
-        $entity_array                       = $this->addExtraFields($model, $db_row, $entity_array);
678
-        $entity_array['_links']             = $this->getEntityLinks($model, $db_row, $entity_array);
679
-        // when it's a regular read request for a model with a password and the password wasn't provided
680
-        // remove the password protected fields
681
-        $has_protected_fields = false;
682
-        try {
683
-            $this->checkPassword(
684
-                $model,
685
-                $db_row,
686
-                $model->alter_query_params_to_restrict_by_ID(
687
-                    $model->get_index_primary_key_string(
688
-                        $model->deduce_fields_n_values_from_cols_n_values($db_row)
689
-                    )
690
-                ),
691
-                $rest_request
692
-            );
693
-        } catch (RestPasswordRequiredException $e) {
694
-            if ($model->hasPassword()) {
695
-                // just remove protected fields
696
-                $has_protected_fields = true;
697
-                $entity_array         = Capabilities::filterOutPasswordProtectedFields(
698
-                    $entity_array,
699
-                    $model,
700
-                    $this->getModelVersionInfo()
701
-                );
702
-            } else {
703
-                // that's a problem. None of this should be accessible if no password was provided
704
-                throw $e;
705
-            }
706
-        }
707
-
708
-        $entity_array['_calculated_fields'] = $this->getEntityCalculations(
709
-            $model,
710
-            $db_row,
711
-            $rest_request,
712
-            $has_protected_fields
713
-        );
714
-        $entity_array                       = apply_filters(
715
-            'FHEE__Read__create_entity_from_wpdb_results__entity_before_including_requested_models',
716
-            $entity_array,
717
-            $model,
718
-            $rest_request->get_param('caps'),
719
-            $rest_request,
720
-            $this
721
-        );
722
-        // add an empty protected property for now. If it's still around after we remove everything the request didn't
723
-        // want, we'll populate it then. k?
724
-        $entity_array['_protected'] = [];
725
-        // remove any properties the request didn't want. This way _protected won't bother mentioning them
726
-        $entity_array = $this->includeOnlyRequestedProperties($model, $rest_request, $entity_array);
727
-        $entity_array = $this->includeRequestedModels(
728
-            $model,
729
-            $rest_request,
730
-            $entity_array,
731
-            $db_row,
732
-            $has_protected_fields
733
-        );
734
-        // if they still wanted the _protected property, add it.
735
-        if (isset($entity_array['_protected'])) {
736
-            $entity_array = $this->addProtectedProperty($model, $entity_array, $has_protected_fields);
737
-        }
738
-        $entity_array = apply_filters(
739
-            'FHEE__Read__create_entity_from_wpdb_results__entity_before_inaccessible_field_removal',
740
-            $entity_array,
741
-            $model,
742
-            $rest_request->get_param('caps'),
743
-            $rest_request,
744
-            $this
745
-        );
746
-        if (! $current_user_full_access_to_entity) {
747
-            $result_without_inaccessible_fields = Capabilities::filterOutInaccessibleEntityFields(
748
-                $entity_array,
749
-                $model,
750
-                $rest_request->get_param('caps'),
751
-                $this->getModelVersionInfo()
752
-            );
753
-        } else {
754
-            $result_without_inaccessible_fields = $entity_array;
755
-        }
756
-        $this->setDebugInfo(
757
-            'inaccessible fields',
758
-            array_keys(array_diff_key((array) $entity_array, (array) $result_without_inaccessible_fields))
759
-        );
760
-        return apply_filters(
761
-            'FHEE__Read__create_entity_from_wpdb_results__entity_return',
762
-            $result_without_inaccessible_fields,
763
-            $model,
764
-            $rest_request->get_param('caps')
765
-        );
766
-    }
767
-
768
-
769
-    /**
770
-     * Returns an array describing which fields can be protected, and which actually were removed this request
771
-     *
772
-     * @param EEM_Base $model
773
-     * @param array    $results_so_far
774
-     * @param bool     $protected
775
-     * @return array results
776
-     * @throws EE_Error
777
-     * @since 4.9.74.p
778
-     */
779
-    protected function addProtectedProperty(EEM_Base $model, array $results_so_far, bool $protected): array
780
-    {
781
-        if (! $protected || ! $model->hasPassword()) {
782
-            return $results_so_far;
783
-        }
784
-        $password_field  = $model->getPasswordField();
785
-        $all_protected   = array_merge(
786
-            [$password_field->get_name()],
787
-            $password_field->protectedFields()
788
-        );
789
-        $fields_included = array_keys($results_so_far);
790
-        $fields_included = array_intersect(
791
-            $all_protected,
792
-            $fields_included
793
-        );
794
-        foreach ($fields_included as $field_name) {
795
-            $results_so_far['_protected'][] = $field_name;
796
-        }
797
-        return $results_so_far;
798
-    }
799
-
800
-
801
-    /**
802
-     * Creates a REST entity array (JSON object we're going to return in the response, but
803
-     * for now still a PHP array, but soon enough we'll call json_encode on it, don't worry),
804
-     * from $wpdb->get_row( $sql, ARRAY_A)
805
-     *
806
-     * @param EEM_Base $model
807
-     * @param array    $db_row
808
-     * @return array entity mostly ready for converting to JSON and sending in the response
809
-     * @throws EE_Error
810
-     * @throws ReflectionException
811
-     * @throws RestException
812
-     * @throws Exception
813
-     */
814
-    protected function createBareEntityFromWpdbResults(EEM_Base $model, array $db_row): array
815
-    {
816
-        $result = $model->deduce_fields_n_values_from_cols_n_values($db_row);
817
-        $result = array_intersect_key(
818
-            $result,
819
-            $this->getModelVersionInfo()->fieldsOnModelInThisVersion($model)
820
-        );
821
-        // if this is a CPT, we need to set the global $post to it,
822
-        // otherwise shortcodes etc won't work properly while rendering it
823
-        if ($model instanceof EEM_CPT_Base) {
824
-            $do_chevy_shuffle = true;
825
-        } else {
826
-            $do_chevy_shuffle = false;
827
-        }
828
-        if ($do_chevy_shuffle) {
829
-            global $post;
830
-            $old_post = $post;
831
-            $post     = get_post($result[ $model->primary_key_name() ]);
832
-            if (! $post instanceof WP_Post) {
833
-                // well that's weird, because $result is what we JUST fetched from the database
834
-                throw new RestException(
835
-                    'error_fetching_post_from_database_results',
836
-                    esc_html__(
837
-                        'An item was retrieved from the database but it\'s not a WP_Post like it should be.',
838
-                        'event_espresso'
839
-                    )
840
-                );
841
-            }
842
-            $model_object_classname          = 'EE_' . $model->get_this_model_name();
843
-            $post->{$model_object_classname} = EE_Registry::instance()->load_class(
844
-                $model_object_classname,
845
-                $result,
846
-                false,
847
-                false
848
-            );
849
-        }
850
-        foreach ($result as $field_name => $field_value) {
851
-            $field_obj = $model->field_settings_for($field_name);
852
-            if ($this->isSubclassOfOne($field_obj, $this->getModelVersionInfo()->fieldsIgnored())) {
853
-                unset($result[ $field_name ]);
854
-            } elseif (
855
-                $this->isSubclassOfOne(
856
-                    $field_obj,
857
-                    $this->getModelVersionInfo()->fieldsThatHaveRenderedFormat()
858
-                )
859
-            ) {
860
-                $result[ $field_name ] = [
861
-                    'raw'      => $this->prepareFieldObjValueForJson($field_obj, $field_value),
862
-                    'rendered' => $this->prepareFieldObjValueForJson($field_obj, $field_value, 'pretty'),
863
-                ];
864
-            } elseif (
865
-                $this->isSubclassOfOne(
866
-                    $field_obj,
867
-                    $this->getModelVersionInfo()->fieldsThatHavePrettyFormat()
868
-                )
869
-            ) {
870
-                $result[ $field_name ] = [
871
-                    'raw'    => $this->prepareFieldObjValueForJson($field_obj, $field_value),
872
-                    'pretty' => $this->prepareFieldObjValueForJson($field_obj, $field_value, 'pretty'),
873
-                ];
874
-            } elseif ($field_obj instanceof EE_Datetime_Field) {
875
-                $field_value = $field_obj->prepare_for_set_from_db($field_value);
876
-                // if the value is null, but we're not supposed to permit null, then set to the field's default
877
-                if (is_null($field_value)) {
878
-                    $field_value = $field_obj->getDefaultDateTimeObj();
879
-                }
880
-                if (is_null($field_value)) {
881
-                    $gmt_date = $local_date = ModelDataTranslator::prepareFieldValuesForJson(
882
-                        $field_obj,
883
-                        $field_value,
884
-                        $this->getModelVersionInfo()->requestedVersion()
885
-                    );
886
-                } else {
887
-                    $timezone = $field_value->getTimezone();
888
-                    EEH_DTT_Helper::setTimezone($field_value, new DateTimeZone('UTC'));
889
-                    $gmt_date = ModelDataTranslator::prepareFieldValuesForJson(
890
-                        $field_obj,
891
-                        $field_value,
892
-                        $this->getModelVersionInfo()->requestedVersion()
893
-                    );
894
-                    EEH_DTT_Helper::setTimezone($field_value, $timezone);
895
-                    $local_date = ModelDataTranslator::prepareFieldValuesForJson(
896
-                        $field_obj,
897
-                        $field_value,
898
-                        $this->getModelVersionInfo()->requestedVersion()
899
-                    );
900
-                }
901
-                $result[ $field_name . '_gmt' ] = $gmt_date;
902
-                $result[ $field_name ]          = $local_date;
903
-            } else {
904
-                $result[ $field_name ] = $this->prepareFieldObjValueForJson($field_obj, $field_value);
905
-            }
906
-        }
907
-        if ($do_chevy_shuffle) {
908
-            $post = $old_post;
909
-        }
910
-        return $result;
911
-    }
912
-
913
-
914
-    /**
915
-     * Takes a value all the way from the DB representation, to the model object's representation, to the
916
-     * user-facing PHP representation, to the REST API representation. (Assumes you've already taken from the DB
917
-     * representation using $field_obj->prepare_for_set_from_db())
918
-     *
919
-     * @param EE_Model_Field_Base $field_obj
920
-     * @param mixed               $value  as it's stored on a model object
921
-     * @param string              $format valid values are 'normal' (default), 'pretty', 'datetime_obj'
922
-     * @return array|int
923
-     * @throws RestException if $value contains a PHP object
924
-     * @throws EE_Error
925
-     */
926
-    protected function prepareFieldObjValueForJson(
927
-        EE_Model_Field_Base $field_obj,
928
-        $value,
929
-        string $format = 'normal'
930
-    ) {
931
-        $value = $field_obj->prepare_for_set_from_db($value);
932
-        switch ($format) {
933
-            case 'pretty':
934
-                $value = $field_obj->prepare_for_pretty_echoing($value);
935
-                break;
936
-            case 'normal':
937
-            default:
938
-                $value = $field_obj->prepare_for_get($value);
939
-                break;
940
-        }
941
-        return ModelDataTranslator::prepareFieldValuesForJson(
942
-            $field_obj,
943
-            $value,
944
-            $this->getModelVersionInfo()->requestedVersion()
945
-        );
946
-    }
947
-
948
-
949
-    /**
950
-     * Adds a few extra fields to the entity response
951
-     *
952
-     * @param EEM_Base $model
953
-     * @param array    $db_row
954
-     * @param array    $entity_array
955
-     * @return array modified entity
956
-     * @throws EE_Error
957
-     */
958
-    protected function addExtraFields(EEM_Base $model, array $db_row, array $entity_array): array
959
-    {
960
-        if ($model instanceof EEM_CPT_Base) {
961
-            $entity_array['link'] = get_permalink($db_row[ $model->get_primary_key_field()->get_qualified_column() ]);
962
-        }
963
-        return $entity_array;
964
-    }
965
-
966
-
967
-    /**
968
-     * Gets links we want to add to the response
969
-     *
970
-     * @param EEM_Base        $model
971
-     * @param array           $db_row
972
-     * @param array           $entity_array
973
-     * @return array the _links item in the entity
974
-     * @throws EE_Error
975
-     * @global WP_REST_Server $wp_rest_server
976
-     */
977
-    protected function getEntityLinks(EEM_Base $model, array $db_row, array $entity_array): array
978
-    {
979
-        // add basic links
980
-        $links = [];
981
-        if ($model->has_primary_key_field()) {
982
-            $links['self'] = [
983
-                [
984
-                    'href' => $this->getVersionedLinkTo(
985
-                        EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
986
-                        . '/'
987
-                        . $entity_array[ $model->primary_key_name() ]
988
-                    ),
989
-                ],
990
-            ];
991
-        }
992
-        $links['collection'] = [
993
-            [
994
-                'href' => $this->getVersionedLinkTo(
995
-                    EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
996
-                ),
997
-            ],
998
-        ];
999
-        // add links to related models
1000
-        if ($model->has_primary_key_field()) {
1001
-            foreach ($this->getModelVersionInfo()->relationSettings($model) as $relation_name => $relation_obj) {
1002
-                $related_model_part                                                      =
1003
-                    Read::getRelatedEntityName($relation_name, $relation_obj);
1004
-                $links[ EED_Core_Rest_Api::ee_api_link_namespace . $related_model_part ] = [
1005
-                    [
1006
-                        'href'   => $this->getVersionedLinkTo(
1007
-                            EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
1008
-                            . '/'
1009
-                            . $entity_array[ $model->primary_key_name() ]
1010
-                            . '/'
1011
-                            . $related_model_part
1012
-                        ),
1013
-                        'single' => $relation_obj instanceof EE_Belongs_To_Relation,
1014
-                    ],
1015
-                ];
1016
-            }
1017
-        }
1018
-        return $links;
1019
-    }
1020
-
1021
-
1022
-    /**
1023
-     * Adds the included models indicated in the request to the entity provided
1024
-     *
1025
-     * @param EEM_Base        $model
1026
-     * @param WP_REST_Request $rest_request
1027
-     * @param array           $entity_array
1028
-     * @param array           $db_row
1029
-     * @param boolean         $included_items_protected if the original item is password protected, don't include any
1030
-     *                                                  related models.
1031
-     * @return array the modified entity
1032
-     * @throws DomainException
1033
-     * @throws EE_Error
1034
-     * @throws InvalidArgumentException
1035
-     * @throws InvalidDataTypeException
1036
-     * @throws InvalidInterfaceException
1037
-     * @throws ModelConfigurationException
1038
-     * @throws ReflectionException
1039
-     * @throws UnexpectedEntityException
1040
-     */
1041
-    protected function includeRequestedModels(
1042
-        EEM_Base $model,
1043
-        WP_REST_Request $rest_request,
1044
-        array $entity_array,
1045
-        array $db_row = [],
1046
-        bool $included_items_protected = false
1047
-    ): array {
1048
-        // if $db_row not included, hope the entity array has what we need
1049
-        if (! $db_row) {
1050
-            $db_row = $entity_array;
1051
-        }
1052
-        $relation_settings = $this->getModelVersionInfo()->relationSettings($model);
1053
-        foreach ($relation_settings as $relation_name => $relation_obj) {
1054
-            $related_fields_to_include   = $this->explodeAndGetItemsPrefixedWith(
1055
-                $rest_request->get_param('include'),
1056
-                $relation_name
1057
-            );
1058
-            $related_fields_to_calculate = $this->explodeAndGetItemsPrefixedWith(
1059
-                $rest_request->get_param('calculate'),
1060
-                $relation_name
1061
-            );
1062
-            // did they specify they wanted to include a related model, or
1063
-            // specific fields from a related model?
1064
-            // or did they specify to calculate a field from a related model?
1065
-            if ($related_fields_to_include || $related_fields_to_calculate) {
1066
-                // if so, we should include at least some part of the related model
1067
-                $pretend_related_request = new WP_REST_Request();
1068
-                $pretend_related_request->set_query_params(
1069
-                    [
1070
-                        'caps'      => $rest_request->get_param('caps'),
1071
-                        'include'   => $related_fields_to_include,
1072
-                        'calculate' => $related_fields_to_calculate,
1073
-                        'password'  => $rest_request->get_param('password'),
1074
-                    ]
1075
-                );
1076
-                $pretend_related_request->add_header('no_rest_headers', true);
1077
-                $primary_model_query_params = $model->alter_query_params_to_restrict_by_ID(
1078
-                    $model->get_index_primary_key_string(
1079
-                        $model->deduce_fields_n_values_from_cols_n_values($db_row)
1080
-                    )
1081
-                );
1082
-                if (! $included_items_protected) {
1083
-                    try {
1084
-                        $related_results = $this->getEntitiesFromRelationUsingModelQueryParams(
1085
-                            $primary_model_query_params,
1086
-                            $relation_obj,
1087
-                            $pretend_related_request
1088
-                        );
1089
-                    } catch (RestException $e) {
1090
-                        $related_results = null;
1091
-                    }
1092
-                } else {
1093
-                    // they're protected, hide them.
1094
-                    $related_results              = null;
1095
-                    $entity_array['_protected'][] = Read::getRelatedEntityName($relation_name, $relation_obj);
1096
-                }
1097
-                if ($related_results instanceof WP_Error || $related_results === null) {
1098
-                    $related_results = $relation_obj instanceof EE_Belongs_To_Relation
1099
-                            ? null
1100
-                            : [];
1101
-                }
1102
-                $entity_array[ Read::getRelatedEntityName($relation_name, $relation_obj) ] = $related_results;
1103
-            }
1104
-        }
1105
-        return $entity_array;
1106
-    }
1107
-
1108
-
1109
-    /**
1110
-     * If the user has requested only specific properties (including meta properties like _links or _protected)
1111
-     * remove everything else.
1112
-     *
1113
-     * @param EEM_Base        $model
1114
-     * @param WP_REST_Request $rest_request
1115
-     * @param                 $entity_array
1116
-     * @return array
1117
-     * @throws EE_Error
1118
-     * @since 4.9.74.p
1119
-     */
1120
-    protected function includeOnlyRequestedProperties(
1121
-        EEM_Base $model,
1122
-        WP_REST_Request $rest_request,
1123
-        $entity_array
1124
-    ): array {
1125
-
1126
-        $includes_for_this_model = $this->explodeAndGetItemsPrefixedWith($rest_request->get_param('include'), '');
1127
-        $includes_for_this_model = $this->removeModelNamesFromArray($includes_for_this_model);
1128
-        // if they passed in * or didn't specify any includes, return everything
1129
-        if (! empty($includes_for_this_model) && ! in_array('*', $includes_for_this_model, true)) {
1130
-            if ($model->has_primary_key_field()) {
1131
-                // always include the primary key. ya just gotta know that at least
1132
-                $includes_for_this_model[] = $model->primary_key_name();
1133
-            }
1134
-            if ($this->explodeAndGetItemsPrefixedWith($rest_request->get_param('calculate'), '')) {
1135
-                $includes_for_this_model[] = '_calculated_fields';
1136
-            }
1137
-            $entity_array = array_intersect_key($entity_array, array_flip($includes_for_this_model));
1138
-        }
1139
-        return $entity_array;
1140
-    }
1141
-
1142
-
1143
-    /**
1144
-     * Returns a new array with all the names of models removed. Eg
1145
-     * array( 'Event', 'Datetime.*', 'foobar' ) would become array( 'Datetime.*', 'foobar' )
1146
-     *
1147
-     * @param array $array
1148
-     * @return array
1149
-     */
1150
-    private function removeModelNamesFromArray(array $array): array
1151
-    {
1152
-        return array_diff($array, array_keys(EE_Registry::instance()->non_abstract_db_models));
1153
-    }
1154
-
1155
-
1156
-    /**
1157
-     * Gets the calculated fields for the response
1158
-     *
1159
-     * @param EEM_Base        $model
1160
-     * @param array           $wpdb_row
1161
-     * @param WP_REST_Request $rest_request
1162
-     * @param bool            $row_is_protected whether this row is password protected or not
1163
-     * @return stdClass the _calculations item in the entity
1164
-     * @throws RestException if a default value has a PHP object, which should never do (and if we
1165
-     *                                          did, let's know about it ASAP, so let the exception bubble up)
1166
-     * @throws EE_Error
1167
-     */
1168
-    protected function getEntityCalculations(
1169
-        EEM_Base $model,
1170
-        array $wpdb_row,
1171
-        WP_REST_Request $rest_request,
1172
-        bool $row_is_protected = false
1173
-    ): stdClass {
1174
-        $calculated_fields = $this->explodeAndGetItemsPrefixedWith(
1175
-            $rest_request->get_param('calculate'),
1176
-            ''
1177
-        );
1178
-        // note: setting calculate=* doesn't do anything
1179
-        $calculated_fields_to_return = new stdClass();
1180
-        $protected_fields            = [];
1181
-        foreach ($calculated_fields as $field_to_calculate) {
1182
-            try {
1183
-                // it's password protected, so they shouldn't be able to read this. Remove the value
1184
-                $schema = $this->fields_calculator->getJsonSchemaForModel($model);
1185
-                if (
1186
-                    $row_is_protected
1187
-                    && isset($schema['properties'][ $field_to_calculate ]['protected'])
1188
-                    && $schema['properties'][ $field_to_calculate ]['protected']
1189
-                ) {
1190
-                    $calculated_value   = null;
1191
-                    $protected_fields[] = $field_to_calculate;
1192
-                    if ($schema['properties'][ $field_to_calculate ]['type']) {
1193
-                        switch ($schema['properties'][ $field_to_calculate ]['type']) {
1194
-                            case 'boolean':
1195
-                                $calculated_value = false;
1196
-                                break;
1197
-                            case 'integer':
1198
-                                $calculated_value = 0;
1199
-                                break;
1200
-                            case 'string':
1201
-                                $calculated_value = '';
1202
-                                break;
1203
-                            case 'array':
1204
-                                $calculated_value = [];
1205
-                                break;
1206
-                            case 'object':
1207
-                                $calculated_value = new stdClass();
1208
-                                break;
1209
-                        }
1210
-                    }
1211
-                } else {
1212
-                    $calculated_value = ModelDataTranslator::prepareFieldValueForJson(
1213
-                        null,
1214
-                        $this->fields_calculator->retrieveCalculatedFieldValue(
1215
-                            $model,
1216
-                            $field_to_calculate,
1217
-                            $wpdb_row,
1218
-                            $rest_request,
1219
-                            $this
1220
-                        ),
1221
-                        $this->getModelVersionInfo()->requestedVersion()
1222
-                    );
1223
-                }
1224
-                $calculated_fields_to_return->{$field_to_calculate} = $calculated_value;
1225
-            } catch (RestException $e) {
1226
-                // if we don't have permission to read it, just leave it out. but let devs know about the problem
1227
-                $this->setResponseHeader(
1228
-                    'Notices-Field-Calculation-Errors['
1229
-                    . $e->getStringCode()
1230
-                    . ']['
1231
-                    . $model->get_this_model_name()
1232
-                    . ']['
1233
-                    . $field_to_calculate
1234
-                    . ']',
1235
-                    $e->getMessage()
1236
-                );
1237
-            }
1238
-        }
1239
-        $calculated_fields_to_return->_protected = $protected_fields;
1240
-        return $calculated_fields_to_return;
1241
-    }
1242
-
1243
-
1244
-    /**
1245
-     * Gets the full URL to the resource, taking the requested version into account
1246
-     *
1247
-     * @param string $link_part_after_version_and_slash eg "events/10/datetimes"
1248
-     * @return string url eg "http://mysite.com/wp-json/ee/v4.6/events/10/datetimes"
1249
-     * @throws EE_Error
1250
-     */
1251
-    public function getVersionedLinkTo(string $link_part_after_version_and_slash): string
1252
-    {
1253
-        return rest_url(
1254
-            EED_Core_Rest_Api::get_versioned_route_to(
1255
-                $link_part_after_version_and_slash,
1256
-                $this->getModelVersionInfo()->requestedVersion()
1257
-            )
1258
-        );
1259
-    }
1260
-
1261
-
1262
-    /**
1263
-     * Gets the correct lowercase name for the relation in the API according
1264
-     * to the relation's type
1265
-     *
1266
-     * @param string                 $relation_name
1267
-     * @param EE_Model_Relation_Base $relation_obj
1268
-     * @return string
1269
-     */
1270
-    public static function getRelatedEntityName(string $relation_name, EE_Model_Relation_Base $relation_obj): string
1271
-    {
1272
-        if ($relation_obj instanceof EE_Belongs_To_Relation) {
1273
-            return strtolower($relation_name);
1274
-        }
1275
-        return EEH_Inflector::pluralize_and_lower($relation_name);
1276
-    }
1277
-
1278
-
1279
-    /**
1280
-     * Gets the one model object with the specified id for the specified model
1281
-     *
1282
-     * @param EEM_Base        $model
1283
-     * @param WP_REST_Request $request
1284
-     * @return array
1285
-     * @throws EE_Error
1286
-     * @throws InvalidArgumentException
1287
-     * @throws InvalidDataTypeException
1288
-     * @throws InvalidInterfaceException
1289
-     * @throws ModelConfigurationException
1290
-     * @throws ReflectionException
1291
-     * @throws RestException
1292
-     * @throws RestPasswordIncorrectException
1293
-     * @throws RestPasswordRequiredException
1294
-     * @throws UnexpectedEntityException
1295
-     * @throws DomainException
1296
-     */
1297
-    public function getEntityFromModel(EEM_Base $model, WP_REST_Request $request): array
1298
-    {
1299
-        $context = $this->validateContext($request->get_param('caps'));
1300
-        return $this->getOneOrReportPermissionError($model, $request, $context);
1301
-    }
1302
-
1303
-
1304
-    /**
1305
-     * If a context is provided which isn't valid, maybe it was added in a future
1306
-     * version so just treat it as a default read
1307
-     *
1308
-     * @param string|null $context
1309
-     * @return string array key of EEM_Base::cap_contexts_to_cap_action_map()
1310
-     */
1311
-    public function validateContext(?string $context): string
1312
-    {
1313
-        if (! $context) {
1314
-            $context = EEM_Base::caps_read;
1315
-        }
1316
-        $valid_contexts = EEM_Base::valid_cap_contexts();
1317
-        if (in_array($context, $valid_contexts, true)) {
1318
-            return $context;
1319
-        }
1320
-        return EEM_Base::caps_read;
1321
-    }
1322
-
1323
-
1324
-    /**
1325
-     * Verifies the passed in value is an allowable default where conditions value.
1326
-     *
1327
-     * @param string $default_where_conditions
1328
-     * @return string
1329
-     */
1330
-    public function validateDefaultQueryParams(string $default_where_conditions): string
1331
-    {
1332
-        $valid_default_where_conditions_for_api_calls = [
1333
-            EEM_Base::default_where_conditions_all,
1334
-            EEM_Base::default_where_conditions_minimum_all,
1335
-            EEM_Base::default_where_conditions_minimum_others,
1336
-        ];
1337
-        if (! $default_where_conditions) {
1338
-            $default_where_conditions = EEM_Base::default_where_conditions_all;
1339
-        }
1340
-        if (
1341
-            in_array(
1342
-                $default_where_conditions,
1343
-                $valid_default_where_conditions_for_api_calls,
1344
-                true
1345
-            )
1346
-        ) {
1347
-            return $default_where_conditions;
1348
-        }
1349
-        return EEM_Base::default_where_conditions_all;
1350
-    }
1351
-
1352
-
1353
-    /**
1354
-     * Translates API filter get parameter into model query params @param EEM_Base $model
1355
-     *
1356
-     * @param array $query_params
1357
-     * @return array model query params (@see
1358
-     *               https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions)
1359
-     *               or FALSE to indicate that absolutely no results should be returned
1360
-     * @throws EE_Error
1361
-     * @throws InvalidArgumentException
1362
-     * @throws InvalidDataTypeException
1363
-     * @throws InvalidInterfaceException
1364
-     * @throws RestException
1365
-     * @throws DomainException
1366
-     * @throws ReflectionException
1367
-     * @see
1368
-     *               https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions.
1369
-     *               Note: right now the query parameter keys for fields (and related fields) can be left as-is, but
1370
-     *               it's quite possible this will change someday. Also, this method's contents might be candidate for
1371
-     *               moving to Model_Data_Translator
1372
-     *
1373
-     */
1374
-    public function createModelQueryParams(EEM_Base $model, array $query_params): array
1375
-    {
1376
-        $model_query_params = [];
1377
-        if (isset($query_params['where'])) {
1378
-            $model_query_params[0] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1379
-                $query_params['where'],
1380
-                $model,
1381
-                $this->getModelVersionInfo()->requestedVersion()
1382
-            );
1383
-        }
1384
-        if (isset($query_params['order_by'])) {
1385
-            $order_by = $query_params['order_by'];
1386
-        } elseif (isset($query_params['orderby'])) {
1387
-            $order_by = $query_params['orderby'];
1388
-        } else {
1389
-            $order_by = null;
1390
-        }
1391
-        if ($order_by !== null) {
1392
-            if (is_array($order_by)) {
1393
-                $order_by = ModelDataTranslator::prepareFieldNamesInArrayKeysFromJson($order_by);
1394
-            } else {
1395
-                // it's a single item
1396
-                $order_by = ModelDataTranslator::prepareFieldNameFromJson($order_by);
1397
-            }
1398
-            $model_query_params['order_by'] = $order_by;
1399
-        }
1400
-        if (isset($query_params['group_by'])) {
1401
-            $group_by = $query_params['group_by'];
1402
-        } elseif (isset($query_params['groupby'])) {
1403
-            $group_by = $query_params['groupby'];
1404
-        } else {
1405
-            $group_by = array_keys($model->get_combined_primary_key_fields());
1406
-        }
1407
-        // make sure they're all real names
1408
-        if (is_array($group_by)) {
1409
-            $group_by = ModelDataTranslator::prepareFieldNamesFromJson($group_by);
1410
-        }
1411
-        if ($group_by !== null) {
1412
-            $model_query_params['group_by'] = $group_by;
1413
-        }
1414
-        if (isset($query_params['having'])) {
1415
-            $model_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1416
-                $query_params['having'],
1417
-                $model,
1418
-                $this->getModelVersionInfo()->requestedVersion()
1419
-            );
1420
-        }
1421
-        if (isset($query_params['order'])) {
1422
-            $model_query_params['order'] = $query_params['order'];
1423
-        }
1424
-        if (isset($query_params['mine'])) {
1425
-            $model_query_params = $model->alter_query_params_to_only_include_mine($model_query_params);
1426
-        }
1427
-        if (isset($query_params['limit'])) {
1428
-            // limit should be either a string like '23' or '23,43', or an array with two items in it
1429
-            if (! is_array($query_params['limit'])) {
1430
-                $limit_array = explode(',', (string) $query_params['limit']);
1431
-            } else {
1432
-                $limit_array = $query_params['limit'];
1433
-            }
1434
-            $sanitized_limit = [];
1435
-            foreach ($limit_array as $limit_part) {
1436
-                if ($this->debug_mode && (! is_numeric($limit_part) || count($sanitized_limit) > 2)) {
1437
-                    throw new EE_Error(
1438
-                        sprintf(
1439
-                            esc_html__(
1440
-                            // @codingStandardsIgnoreStart
1441
-                                'An invalid limit filter was provided. It was: %s. If the EE4 JSON REST API weren\'t in debug mode, this message would not appear.',
1442
-                                // @codingStandardsIgnoreEnd
1443
-                                'event_espresso'
1444
-                            ),
1445
-                            wp_json_encode($query_params['limit'])
1446
-                        )
1447
-                    );
1448
-                }
1449
-                $sanitized_limit[] = (int) $limit_part;
1450
-            }
1451
-            $model_query_params['limit'] = implode(',', $sanitized_limit);
1452
-        } else {
1453
-            $model_query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
1454
-        }
1455
-        if (isset($query_params['caps'])) {
1456
-            $model_query_params['caps'] = $this->validateContext($query_params['caps']);
1457
-        } else {
1458
-            $model_query_params['caps'] = EEM_Base::caps_read;
1459
-        }
1460
-        if (isset($query_params['default_where_conditions'])) {
1461
-            $model_query_params['default_where_conditions'] = $this->validateDefaultQueryParams(
1462
-                $query_params['default_where_conditions']
1463
-            );
1464
-        }
1465
-        // if this is a model protected by a password on another model, exclude the password protected
1466
-        // entities by default. But if they passed in a password, try to show them all. If the password is wrong,
1467
-        // though, they'll get an error (see Read::createEntityFromWpdbResult() which calls Read::checkPassword)
1468
-        if (
1469
-            $model_query_params['caps'] === EEM_Base::caps_read
1470
-            && empty($query_params['password'])
1471
-            && ! $model->hasPassword()
1472
-            && $model->restrictedByRelatedModelPassword()
1473
-        ) {
1474
-            $model_query_params['exclude_protected'] = true;
1475
-        }
1476
-
1477
-        return apply_filters('FHEE__Read__create_model_query_params', $model_query_params, $query_params, $model);
1478
-    }
1479
-
1480
-
1481
-    /**
1482
-     * Changes the REST-style query params for use in the models
1483
-     *
1484
-     * @param EEM_Base $model
1485
-     * @param array    $query_params sub-array from @see EEM_Base::get_all()
1486
-     * @return array
1487
-     * @deprecated
1488
-     */
1489
-    public function prepareRestQueryParamsKeyForModels(EEM_Base $model, array $query_params): array
1490
-    {
1491
-        $model_ready_query_params = [];
1492
-        foreach ($query_params as $key => $value) {
1493
-            $model_ready_query_params[ $key ] = is_array($value)
1494
-                ? $this->prepareRestQueryParamsKeyForModels($model, $value)
1495
-                : $value;
1496
-        }
1497
-        return $model_ready_query_params;
1498
-    }
1499
-
1500
-
1501
-    /**
1502
-     * @param $model
1503
-     * @param $query_params
1504
-     * @return array
1505
-     * @deprecated instead use ModelDataTranslator::prepareFieldValuesFromJson()
1506
-     */
1507
-    public function prepareRestQueryParamsValuesForModels($model, $query_params): array
1508
-    {
1509
-        $model_ready_query_params = [];
1510
-        foreach ($query_params as $key => $value) {
1511
-            if (is_array($value)) {
1512
-                $model_ready_query_params[ $key ] = $this->prepareRestQueryParamsValuesForModels($model, $value);
1513
-            } else {
1514
-                $model_ready_query_params[ $key ] = $value;
1515
-            }
1516
-        }
1517
-        return $model_ready_query_params;
1518
-    }
1519
-
1520
-
1521
-    /**
1522
-     * Explodes the string on commas, and only returns items with $prefix followed by a period.
1523
-     * If no prefix is specified, returns items with no period.
1524
-     *
1525
-     * @param string|array $string_to_explode eg "jibba,jabba, blah, blah, blah" or array('jibba', 'jabba' )
1526
-     * @param string       $prefix            "Event" or "foobar"
1527
-     * @return array $string_to_exploded exploded on COMMAS, and if a prefix was specified
1528
-     *                                        we only return strings starting with that and a period; if no prefix was
1529
-     *                                        specified we return all items containing NO periods
1530
-     */
1531
-    public function explodeAndGetItemsPrefixedWith($string_to_explode, string $prefix): array
1532
-    {
1533
-        if (is_string($string_to_explode)) {
1534
-            $exploded_contents = explode(',', $string_to_explode);
1535
-        } elseif (is_array($string_to_explode)) {
1536
-            $exploded_contents = $string_to_explode;
1537
-        } else {
1538
-            $exploded_contents = [];
1539
-        }
1540
-        // if the string was empty, we want an empty array
1541
-        $exploded_contents    = array_filter($exploded_contents);
1542
-        $contents_with_prefix = [];
1543
-        foreach ($exploded_contents as $item) {
1544
-            $item = trim($item);
1545
-            // if no prefix was provided, so we look for items with no "." in them
1546
-            if (! $prefix) {
1547
-                // does this item have a period?
1548
-                if (strpos($item, '.') === false) {
1549
-                    // if not, then its what we're looking for
1550
-                    $contents_with_prefix[] = $item;
1551
-                }
1552
-            } elseif (strpos($item, $prefix . '.') === 0) {
1553
-                // this item has the prefix and a period, grab it
1554
-                $contents_with_prefix[] = substr(
1555
-                    $item,
1556
-                    strpos($item, $prefix . '.') + strlen($prefix . '.')
1557
-                );
1558
-            } elseif ($item === $prefix) {
1559
-                // this item is JUST the prefix
1560
-                // so let's grab everything after, which is a blank string
1561
-                $contents_with_prefix[] = '';
1562
-            }
1563
-        }
1564
-        return $contents_with_prefix;
1565
-    }
1566
-
1567
-
1568
-    /**
1569
-     * @param string      $include_string @see Read:handle_request_get_all
1570
-     * @param string|null $model_name
1571
-     * @return array of fields for this model. If $model_name is provided, then
1572
-     *                                    the fields for that model, with the model's name removed from each.
1573
-     *                                    If $include_string was blank or '*' returns an empty array
1574
-     * @throws EE_Error
1575
-     * @throws EE_Error
1576
-     * @deprecated since 4.8.36.rc.001 You should instead use Read::explode_and_get_items_prefixed_with.
1577
-     *                                    Deprecated because its return values were really quite confusing- sometimes
1578
-     *                                    it
1579
-     *                                    returned an empty array (when the include string was blank or '*') or
1580
-     *                                    sometimes it returned array('*') (when you provided a model and a model of
1581
-     *                                    that kind was found). Parses the $include_string so we fetch all the field
1582
-     *                                    names relating to THIS model
1583
-     *                                    (ie have NO period in them), or for the provided model (ie start with the
1584
-     *                                    model name and then a period).
1585
-     */
1586
-    public function extractIncludesForThisModel(string $include_string, string $model_name = null): array
1587
-    {
1588
-        if (is_array($include_string)) {
1589
-            $include_string = implode(',', $include_string);
1590
-        }
1591
-        if ($include_string === '*' || $include_string === '') {
1592
-            return [];
1593
-        }
1594
-        $includes                    = explode(',', $include_string);
1595
-        $extracted_fields_to_include = [];
1596
-        if ($model_name) {
1597
-            foreach ($includes as $field_to_include) {
1598
-                $field_to_include = trim($field_to_include);
1599
-                if (strpos($field_to_include, $model_name . '.') === 0) {
1600
-                    // found the model name at the exact start
1601
-                    $field_sans_model_name         = str_replace($model_name . '.', '', $field_to_include);
1602
-                    $extracted_fields_to_include[] = $field_sans_model_name;
1603
-                } elseif ($field_to_include === $model_name) {
1604
-                    $extracted_fields_to_include[] = '*';
1605
-                }
1606
-            }
1607
-        } else {
1608
-            // look for ones with no period
1609
-            foreach ($includes as $field_to_include) {
1610
-                $field_to_include = trim($field_to_include);
1611
-                if (
1612
-                    strpos($field_to_include, '.') === false
1613
-                    && ! $this->getModelVersionInfo()->isModelNameInThisVersion($field_to_include)
1614
-                ) {
1615
-                    $extracted_fields_to_include[] = $field_to_include;
1616
-                }
1617
-            }
1618
-        }
1619
-        return $extracted_fields_to_include;
1620
-    }
1621
-
1622
-
1623
-    /**
1624
-     * Gets the single item using the model according to the request in the context given, otherwise
1625
-     * returns that it's inaccessible to the current user
1626
-     *
1627
-     * @param EEM_Base        $model
1628
-     * @param WP_REST_Request $request
1629
-     * @param null            $context
1630
-     * @return array
1631
-     * @throws EE_Error
1632
-     * @throws ReflectionException
1633
-     */
1634
-    public function getOneOrReportPermissionError(EEM_Base $model, WP_REST_Request $request, $context = null): array
1635
-    {
1636
-        $query_params = [[$model->primary_key_name() => $request->get_param('id')], 'limit' => 1];
1637
-        if ($model instanceof EEM_Soft_Delete_Base) {
1638
-            $query_params = $model->alter_query_params_so_deleted_and_undeleted_items_included($query_params);
1639
-        }
1640
-        $restricted_query_params         = $query_params;
1641
-        $restricted_query_params['caps'] = $context;
1642
-        $this->setDebugInfo('model query params', $restricted_query_params);
1643
-        $model_rows = $model->get_all_wpdb_results($restricted_query_params);
1644
-        if (! empty($model_rows)) {
1645
-            return $this->createEntityFromWpdbResult(
1646
-                $model,
1647
-                reset($model_rows),
1648
-                $request
1649
-            );
1650
-        }
1651
-        // ok let's test to see if we WOULD have found it, had we not had restrictions from missing capabilities
1652
-        $lowercase_model_name = strtolower($model->get_this_model_name());
1653
-        if ($model->exists($query_params)) {
1654
-            // you got shafted- it existed but we didn't want to tell you!
1655
-            throw new RestException(
1656
-                'rest_user_cannot_' . $context,
1657
-                sprintf(
1658
-                    __('Sorry, you cannot %1$s this %2$s. Missing permissions are: %3$s', 'event_espresso'),
1659
-                    $context,
1660
-                    $lowercase_model_name,
1661
-                    Capabilities::getMissingPermissionsString(
1662
-                        $model,
1663
-                        $context
1664
-                    )
1665
-                ),
1666
-                ['status' => 403]
1667
-            );
1668
-        }
1669
-        // it's not you. It just doesn't exist
1670
-        throw new RestException(
1671
-            sprintf('rest_%s_invalid_id', $lowercase_model_name),
1672
-            sprintf(__('Invalid %s ID.', 'event_espresso'), $lowercase_model_name),
1673
-            ['status' => 404]
1674
-        );
1675
-    }
1676
-
1677
-
1678
-    /**
1679
-     * Checks that if this content requires a password to be read, that it's been provided and is correct.
1680
-     *
1681
-     * @param EEM_Base        $model
1682
-     * @param array           $model_row
1683
-     * @param array           $query_params Adds 'default_where_conditions' => 'minimum'
1684
-     *                                      to ensure we don't confuse trashed with password protected.
1685
-     * @param WP_REST_Request $request
1686
-     * @throws EE_Error
1687
-     * @throws InvalidArgumentException
1688
-     * @throws InvalidDataTypeException
1689
-     * @throws InvalidInterfaceException
1690
-     * @throws RestPasswordRequiredException
1691
-     * @throws RestPasswordIncorrectException
1692
-     * @throws ModelConfigurationException
1693
-     * @throws ReflectionException
1694
-     * @since 4.9.74.p
1695
-     */
1696
-    protected function checkPassword(EEM_Base $model, array $model_row, array $query_params, WP_REST_Request $request)
1697
-    {
1698
-        $query_params['default_where_conditions'] = 'minimum';
1699
-        // stuff is only "protected" for front-end requests. Elsewhere, you either get full permission to access the object
1700
-        // or you don't.
1701
-        $request_caps = $request->get_param('caps');
1702
-        if (isset($request_caps) && $request_caps !== EEM_Base::caps_read) {
1703
-            return;
1704
-        }
1705
-        // if this entity requires a password, they better give it and it better be right!
1706
-        if (
1707
-            $model->hasPassword()
1708
-            && $model_row[ $model->getPasswordField()->get_qualified_column() ] !== ''
1709
-        ) {
1710
-            if (empty($request['password'])) {
1711
-                throw new RestPasswordRequiredException();
1712
-            }
1713
-            if (
1714
-                ! hash_equals(
1715
-                    $model_row[ $model->getPasswordField()->get_qualified_column() ],
1716
-                    $request['password']
1717
-                )
1718
-            ) {
1719
-                throw new RestPasswordIncorrectException();
1720
-            }
1721
-        } elseif (
1722
-            // wait! maybe this content is password protected
1723
-            $model->restrictedByRelatedModelPassword()
1724
-            && $request->get_param('caps') === EEM_Base::caps_read
1725
-        ) {
1726
-            $password_supplied = $request->get_param('password');
1727
-            if (empty($password_supplied)) {
1728
-                $query_params['exclude_protected'] = true;
1729
-                if (! $model->exists($query_params)) {
1730
-                    throw new RestPasswordRequiredException();
1731
-                }
1732
-            } else {
1733
-                $query_params[0][ $model->modelChainAndPassword() ] = $password_supplied;
1734
-                if (! $model->exists($query_params)) {
1735
-                    throw new RestPasswordIncorrectException();
1736
-                }
1737
-            }
1738
-        }
1739
-    }
54
+	protected CalculatedModelFields $fields_calculator;
55
+
56
+
57
+	/**
58
+	 * Read constructor.
59
+	 *
60
+	 * @param CalculatedModelFields $fields_calculator
61
+	 */
62
+	public function __construct(CalculatedModelFields $fields_calculator)
63
+	{
64
+		parent::__construct();
65
+		$this->fields_calculator = $fields_calculator;
66
+	}
67
+
68
+
69
+	/**
70
+	 * @return Read
71
+	 */
72
+	private static function getReadController(): Read
73
+	{
74
+		return LoaderFactory::getLoader()->getNew(Read::class);
75
+	}
76
+
77
+
78
+	/**
79
+	 * Handles requests to get all (or a filtered subset) of entities for a particular model
80
+	 *
81
+	 * @param WP_REST_Request $request
82
+	 * @param string          $version
83
+	 * @param string          $model_name
84
+	 * @return WP_REST_Response
85
+	 * @throws InvalidArgumentException
86
+	 * @throws InvalidDataTypeException
87
+	 * @throws InvalidInterfaceException
88
+	 */
89
+	public static function handleRequestGetAll(
90
+		WP_REST_Request $request,
91
+		string $version,
92
+		string $model_name
93
+	): WP_REST_Response {
94
+		$controller = self::getReadController();
95
+		try {
96
+			$controller->setRequestedVersion($version);
97
+			if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
98
+				return $controller->sendResponse(
99
+					new WP_Error(
100
+						'endpoint_parsing_error',
101
+						sprintf(
102
+							esc_html__(
103
+								'There is no model for endpoint %s. Please contact event espresso support',
104
+								'event_espresso'
105
+							),
106
+							$model_name
107
+						)
108
+					)
109
+				);
110
+			}
111
+			return $controller->sendResponse(
112
+				$controller->getEntitiesFromModel(
113
+					$controller->getModelVersionInfo()->loadModel($model_name),
114
+					$request
115
+				)
116
+			);
117
+		} catch (Exception $e) {
118
+			return $controller->sendResponse($e);
119
+		}
120
+	}
121
+
122
+
123
+	/**
124
+	 * Prepares and returns schema for any OPTIONS request.
125
+	 *
126
+	 * @param string $version    The API endpoint version being used.
127
+	 * @param string $model_name Something like `Event` or `Registration`
128
+	 * @return array
129
+	 * @throws InvalidArgumentException
130
+	 * @throws InvalidDataTypeException
131
+	 * @throws InvalidInterfaceException
132
+	 */
133
+	public static function handleSchemaRequest(string $version, string $model_name): array
134
+	{
135
+		$controller = self::getReadController();
136
+		try {
137
+			$controller->setRequestedVersion($version);
138
+			if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
139
+				return [];
140
+			}
141
+			// get the model for this version
142
+			$model        = $controller->getModelVersionInfo()->loadModel($model_name);
143
+			$model_schema = new JsonModelSchema(
144
+				$model,
145
+				LoaderFactory::getLoader()->getShared('EventEspresso\core\libraries\rest_api\CalculatedModelFields')
146
+			);
147
+			return $model_schema->getModelSchemaForRelations(
148
+				$controller->getModelVersionInfo()->relationSettings($model),
149
+				$controller->customizeSchemaForRestResponse(
150
+					$model,
151
+					$model_schema->getModelSchemaForFields(
152
+						$controller->getModelVersionInfo()->fieldsOnModelInThisVersion($model),
153
+						$model_schema->getInitialSchemaStructure()
154
+					)
155
+				)
156
+			);
157
+		} catch (Exception $e) {
158
+			error_log($e->getMessage());
159
+			return [];
160
+		}
161
+	}
162
+
163
+
164
+	/**
165
+	 * This loops through each field in the given schema for the model and does the following:
166
+	 * - add any extra fields that are REST API specific and related to existing fields.
167
+	 * - transform default values into the correct format for a REST API response.
168
+	 *
169
+	 * @param EEM_Base $model
170
+	 * @param array    $schema
171
+	 * @return array  The final schema.
172
+	 * @throws EE_Error
173
+	 */
174
+	public function customizeSchemaForRestResponse(EEM_Base $model, array $schema): array
175
+	{
176
+		foreach ($this->getModelVersionInfo()->fieldsOnModelInThisVersion($model) as $field_name => $field) {
177
+			$schema = $this->translateDefaultsForRestResponse(
178
+				$field_name,
179
+				$field,
180
+				$this->maybeAddExtraFieldsToSchema($field_name, $field, $schema)
181
+			);
182
+		}
183
+		return $schema;
184
+	}
185
+
186
+
187
+	/**
188
+	 * This is used to ensure that the 'default' value set in the schema response is formatted correctly for the REST
189
+	 * response.
190
+	 *
191
+	 * @param                      $field_name
192
+	 * @param EE_Model_Field_Base  $field
193
+	 * @param array                $schema
194
+	 * @return array
195
+	 * @throws RestException  if a default value has a PHP object, which we should never do
196
+	 *                                  (but if we did, let's know about it ASAP, so let the exception bubble up)
197
+	 * @throws EE_Error
198
+	 *
199
+	 */
200
+	protected function translateDefaultsForRestResponse($field_name, EE_Model_Field_Base $field, array $schema): array
201
+	{
202
+		if (isset($schema['properties'][ $field_name ]['default'])) {
203
+			if (is_array($schema['properties'][ $field_name ]['default'])) {
204
+				foreach ($schema['properties'][ $field_name ]['default'] as $default_key => $default_value) {
205
+					if ($default_key === 'raw') {
206
+						$schema['properties'][ $field_name ]['default'][ $default_key ] =
207
+							ModelDataTranslator::prepareFieldValueForJson(
208
+								$field,
209
+								$default_value,
210
+								$this->getModelVersionInfo()->requestedVersion()
211
+							);
212
+					}
213
+				}
214
+			} else {
215
+				$schema['properties'][ $field_name ]['default'] = ModelDataTranslator::prepareFieldValueForJson(
216
+					$field,
217
+					$schema['properties'][ $field_name ]['default'],
218
+					$this->getModelVersionInfo()->requestedVersion()
219
+				);
220
+			}
221
+		}
222
+		return $schema;
223
+	}
224
+
225
+
226
+	/**
227
+	 * Adds additional fields to the schema
228
+	 * The REST API returns a GMT value field for each datetime field in the resource.  Thus the description about this
229
+	 * needs to be added to the schema.
230
+	 *
231
+	 * @param                      $field_name
232
+	 * @param EE_Model_Field_Base  $field
233
+	 * @param array                $schema
234
+	 * @return array
235
+	 */
236
+	protected function maybeAddExtraFieldsToSchema($field_name, EE_Model_Field_Base $field, array $schema): array
237
+	{
238
+		if ($field instanceof EE_Datetime_Field) {
239
+			$schema['properties'][ $field_name . '_gmt' ] = $field->getSchema();
240
+			// modify the description
241
+			$schema['properties'][ $field_name . '_gmt' ]['description'] = sprintf(
242
+				esc_html__('%s - the value for this field is in GMT.', 'event_espresso'),
243
+				wp_specialchars_decode($field->get_nicename(), ENT_QUOTES)
244
+			);
245
+		}
246
+		return $schema;
247
+	}
248
+
249
+
250
+	/**
251
+	 * Used to figure out the route from the request when a `WP_REST_Request` object is not available
252
+	 *
253
+	 * @return string
254
+	 */
255
+	protected function getRouteFromRequest(): string
256
+	{
257
+		if (
258
+			isset($GLOBALS['wp'])
259
+			&& $GLOBALS['wp'] instanceof WP
260
+			&& isset($GLOBALS['wp']->query_vars['rest_route'])
261
+		) {
262
+			return $GLOBALS['wp']->query_vars['rest_route'];
263
+		}
264
+		/** @var RequestInterface $request */
265
+		$request = LoaderFactory::getLoader()->getShared(RequestInterface::class);
266
+		return $request->serverParamIsSet('PATH_INFO')
267
+			? $request->getServerParam('PATH_INFO')
268
+			: '/';
269
+	}
270
+
271
+
272
+	/**
273
+	 * Gets a single entity related to the model indicated in the path and its id
274
+	 *
275
+	 * @param WP_REST_Request $request
276
+	 * @param string          $version
277
+	 * @param string          $model_name
278
+	 * @return WP_REST_Response
279
+	 * @throws InvalidDataTypeException
280
+	 * @throws InvalidInterfaceException
281
+	 * @throws InvalidArgumentException
282
+	 */
283
+	public static function handleRequestGetOne(
284
+		WP_REST_Request $request,
285
+		string $version,
286
+		string $model_name
287
+	): WP_REST_Response {
288
+		$controller = self::getReadController();
289
+		try {
290
+			$controller->setRequestedVersion($version);
291
+			if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
292
+				return $controller->sendResponse(
293
+					new WP_Error(
294
+						'endpoint_parsing_error',
295
+						sprintf(
296
+							esc_html__(
297
+								'There is no model for endpoint %s. Please contact event espresso support',
298
+								'event_espresso'
299
+							),
300
+							$model_name
301
+						)
302
+					)
303
+				);
304
+			}
305
+			return $controller->sendResponse(
306
+				$controller->getEntityFromModel(
307
+					$controller->getModelVersionInfo()->loadModel($model_name),
308
+					$request
309
+				)
310
+			);
311
+		} catch (Exception $e) {
312
+			return $controller->sendResponse($e);
313
+		}
314
+	}
315
+
316
+
317
+	/**
318
+	 * Gets all the related entities (or if its a belongs-to relation just the one)
319
+	 * to the item with the given id
320
+	 *
321
+	 * @param WP_REST_Request $request
322
+	 * @param string          $version
323
+	 * @param string          $model_name
324
+	 * @param string          $related_model_name
325
+	 * @return WP_REST_Response
326
+	 * @throws InvalidDataTypeException
327
+	 * @throws InvalidInterfaceException
328
+	 * @throws InvalidArgumentException
329
+	 */
330
+	public static function handleRequestGetRelated(
331
+		WP_REST_Request $request,
332
+		string $version,
333
+		string $model_name,
334
+		string $related_model_name
335
+	): WP_REST_Response {
336
+		$controller = self::getReadController();
337
+		try {
338
+			$controller->setRequestedVersion($version);
339
+			$main_model = $controller->validateModel($model_name);
340
+			$controller->validateModel($related_model_name);
341
+			return $controller->sendResponse(
342
+				$controller->getEntitiesFromRelation(
343
+					$request->get_param('id'),
344
+					$main_model->related_settings_for($related_model_name),
345
+					$request
346
+				)
347
+			);
348
+		} catch (Exception $e) {
349
+			return $controller->sendResponse($e);
350
+		}
351
+	}
352
+
353
+
354
+	/**
355
+	 * Gets a collection for the given model and filters
356
+	 *
357
+	 * @param EEM_Base        $model
358
+	 * @param WP_REST_Request $request
359
+	 * @return array
360
+	 * @throws DomainException
361
+	 * @throws EE_Error
362
+	 * @throws InvalidArgumentException
363
+	 * @throws InvalidDataTypeException
364
+	 * @throws InvalidInterfaceException
365
+	 * @throws ModelConfigurationException
366
+	 * @throws ReflectionException
367
+	 * @throws RestException
368
+	 * @throws RestPasswordIncorrectException
369
+	 * @throws RestPasswordRequiredException
370
+	 * @throws UnexpectedEntityException
371
+	 */
372
+	public function getEntitiesFromModel(EEM_Base $model, WP_REST_Request $request): array
373
+	{
374
+		$query_params = $this->createModelQueryParams($model, $request->get_params());
375
+		if (! Capabilities::currentUserHasPartialAccessTo($model, $query_params['caps'])) {
376
+			$model_name_plural = EEH_Inflector::pluralize_and_lower($model->get_this_model_name());
377
+			throw new RestException(
378
+				sprintf('rest_%s_cannot_list', $model_name_plural),
379
+				sprintf(
380
+					esc_html__('Sorry, you are not allowed to list %1$s. Missing permissions: %2$s', 'event_espresso'),
381
+					$model_name_plural,
382
+					Capabilities::getMissingPermissionsString($model, $query_params['caps'])
383
+				),
384
+				['status' => 403]
385
+			);
386
+		}
387
+		if (! $request->get_header('no_rest_headers')) {
388
+			$this->setHeadersFromQueryParams($model, $query_params);
389
+		}
390
+		/** @type array $results */
391
+		$results      = $model->get_all_wpdb_results($query_params);
392
+		$nice_results = [];
393
+		foreach ($results as $result) {
394
+			$nice_results[] = $this->createEntityFromWpdbResult(
395
+				$model,
396
+				$result,
397
+				$request
398
+			);
399
+		}
400
+		return $nice_results;
401
+	}
402
+
403
+
404
+	/**
405
+	 * Gets the collection for given relation object
406
+	 * The same as Read::get_entities_from_model(), except if the relation
407
+	 * is a HABTM relation, in which case it merges any non-foreign-key fields from
408
+	 * the join-model-object into the results
409
+	 *
410
+	 * @param array                  $primary_model_query_params  query params for finding the item from which
411
+	 *                                                            relations will be based
412
+	 * @param EE_Model_Relation_Base $relation
413
+	 * @param WP_REST_Request        $request
414
+	 * @return array|null
415
+	 * @throws DomainException
416
+	 * @throws EE_Error
417
+	 * @throws InvalidArgumentException
418
+	 * @throws InvalidDataTypeException
419
+	 * @throws InvalidInterfaceException
420
+	 * @throws ModelConfigurationException
421
+	 * @throws ReflectionException
422
+	 * @throws RestException
423
+	 * @throws ModelConfigurationException
424
+	 * @throws UnexpectedEntityException
425
+	 * @throws Exception
426
+	 */
427
+	protected function getEntitiesFromRelationUsingModelQueryParams(
428
+		array $primary_model_query_params,
429
+		EE_Model_Relation_Base $relation,
430
+		WP_REST_Request $request
431
+	): ?array {
432
+		$context       = $this->validateContext($request->get_param('caps'));
433
+		$model         = $relation->get_this_model();
434
+		$related_model = $relation->get_other_model();
435
+		if (! isset($primary_model_query_params[0])) {
436
+			$primary_model_query_params[0] = [];
437
+		}
438
+		// check if they can access the 1st model object
439
+		$primary_model_query_params = [
440
+			0       => $primary_model_query_params[0],
441
+			'limit' => 1,
442
+		];
443
+		if ($model instanceof EEM_Soft_Delete_Base) {
444
+			$primary_model_query_params = $model->alter_query_params_so_deleted_and_undeleted_items_included(
445
+				$primary_model_query_params
446
+			);
447
+		}
448
+		$restricted_query_params          = $primary_model_query_params;
449
+		$restricted_query_params['caps']  = $context;
450
+		$restricted_query_params['limit'] = 1;
451
+		$this->setDebugInfo('main model query params', $restricted_query_params);
452
+		$this->setDebugInfo('missing caps', Capabilities::getMissingPermissionsString($related_model, $context));
453
+		$primary_model_rows = $model->get_all_wpdb_results($restricted_query_params);
454
+		$primary_model_row  = null;
455
+		if (is_array($primary_model_rows)) {
456
+			$primary_model_row = reset($primary_model_rows);
457
+		}
458
+		if (
459
+			! (
460
+				$primary_model_row
461
+				&& Capabilities::currentUserHasPartialAccessTo($related_model, $context)
462
+			)
463
+		) {
464
+			if ($relation instanceof EE_Belongs_To_Relation) {
465
+				$related_model_name_maybe_plural = strtolower($related_model->get_this_model_name());
466
+			} else {
467
+				$related_model_name_maybe_plural = EEH_Inflector::pluralize_and_lower(
468
+					$related_model->get_this_model_name()
469
+				);
470
+			}
471
+			throw new RestException(
472
+				sprintf('rest_%s_cannot_list', $related_model_name_maybe_plural),
473
+				sprintf(
474
+					esc_html__(
475
+						'Sorry, you are not allowed to list %1$s related to %2$s. Missing permissions: %3$s',
476
+						'event_espresso'
477
+					),
478
+					$related_model_name_maybe_plural,
479
+					$relation->get_this_model()->get_this_model_name(),
480
+					implode(
481
+						',',
482
+						array_keys(
483
+							Capabilities::getMissingPermissions($related_model, $context)
484
+						)
485
+					)
486
+				),
487
+				['status' => 403]
488
+			);
489
+		}
490
+
491
+		$this->checkPassword(
492
+			$model,
493
+			$primary_model_row,
494
+			$restricted_query_params,
495
+			$request
496
+		);
497
+		$query_params = $this->createModelQueryParams($relation->get_other_model(), $request->get_params());
498
+		foreach ($primary_model_query_params[0] as $where_condition_key => $where_condition_value) {
499
+			$query_params[0][ $relation->get_this_model()->get_this_model_name()
500
+							  . '.'
501
+							  . $where_condition_key ] = $where_condition_value;
502
+		}
503
+		$query_params['default_where_conditions'] = 'none';
504
+		$query_params['caps']                     = $context;
505
+		if (! $request->get_header('no_rest_headers')) {
506
+			$this->setHeadersFromQueryParams($relation->get_other_model(), $query_params);
507
+		}
508
+		/** @type array $results */
509
+		$results      = $relation->get_other_model()->get_all_wpdb_results($query_params);
510
+		$nice_results = [];
511
+		foreach ($results as $result) {
512
+			$nice_result = $this->createEntityFromWpdbResult(
513
+				$relation->get_other_model(),
514
+				$result,
515
+				$request
516
+			);
517
+			if ($relation instanceof EE_HABTM_Relation) {
518
+				// put the unusual stuff (properties from the HABTM relation) first, and make sure
519
+				// if there are conflicts we prefer the properties from the main model
520
+				$join_model_result = $this->createEntityFromWpdbResult(
521
+					$relation->get_join_model(),
522
+					$result,
523
+					$request
524
+				);
525
+				$joined_result     = array_merge($join_model_result, $nice_result);
526
+				// but keep the meta stuff from the main model
527
+				if (isset($nice_result['meta'])) {
528
+					$joined_result['meta'] = $nice_result['meta'];
529
+				}
530
+				$nice_result = $joined_result;
531
+			}
532
+			$nice_results[] = $nice_result;
533
+		}
534
+		if ($relation instanceof EE_Belongs_To_Relation) {
535
+			return array_shift($nice_results);
536
+		} else {
537
+			return $nice_results;
538
+		}
539
+	}
540
+
541
+
542
+	/**
543
+	 * Gets the collection for given relation object
544
+	 * The same as Read::get_entities_from_model(), except if the relation
545
+	 * is a HABTM relation, in which case it merges any non-foreign-key fields from
546
+	 * the join-model-object into the results
547
+	 *
548
+	 * @param int|string             $id the ID of the thing we are fetching related stuff from
549
+	 * @param EE_Model_Relation_Base $relation
550
+	 * @param WP_REST_Request        $request
551
+	 * @return array
552
+	 * @throws DomainException
553
+	 * @throws EE_Error
554
+	 * @throws InvalidArgumentException
555
+	 * @throws InvalidDataTypeException
556
+	 * @throws InvalidInterfaceException
557
+	 * @throws ReflectionException
558
+	 * @throws RestException
559
+	 * @throws ModelConfigurationException
560
+	 * @throws UnexpectedEntityException
561
+	 * @throws Exception
562
+	 */
563
+	public function getEntitiesFromRelation($id, EE_Model_Relation_Base $relation, WP_REST_Request $request): array
564
+	{
565
+		if (! $relation->get_this_model()->has_primary_key_field()) {
566
+			throw new EE_Error(
567
+				sprintf(
568
+					esc_html__(
569
+					// @codingStandardsIgnoreStart
570
+						'Read::get_entities_from_relation should only be called from a model with a primary key, it was called from %1$s',
571
+						// @codingStandardsIgnoreEnd
572
+						'event_espresso'
573
+					),
574
+					$relation->get_this_model()->get_this_model_name()
575
+				)
576
+			);
577
+		}
578
+		// can we edit that main item?
579
+		// if not, show nothing but an error
580
+		// otherwise, please proceed
581
+		return $this->getEntitiesFromRelationUsingModelQueryParams(
582
+			[
583
+				[
584
+					$relation->get_this_model()->primary_key_name() => $id,
585
+				],
586
+			],
587
+			$relation,
588
+			$request
589
+		);
590
+	}
591
+
592
+
593
+	/**
594
+	 * Sets the headers that are based on the model and query params,
595
+	 * like the total records. This should only be called on the original request
596
+	 * from the client, not on subsequent internal
597
+	 *
598
+	 * @param EEM_Base $model
599
+	 * @param array    $query_params
600
+	 * @return void
601
+	 * @throws EE_Error
602
+	 * @throws ReflectionException
603
+	 */
604
+	protected function setHeadersFromQueryParams(EEM_Base $model, array $query_params)
605
+	{
606
+		$this->setDebugInfo('model query params', $query_params);
607
+		$this->setDebugInfo(
608
+			'missing caps',
609
+			Capabilities::getMissingPermissionsString($model, $query_params['caps'])
610
+		);
611
+		// normally the limit to a 2-part array, where the 2nd item is the limit
612
+		if (! isset($query_params['limit'])) {
613
+			$query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
614
+		}
615
+		if (is_array($query_params['limit'])) {
616
+			$limit_parts = $query_params['limit'];
617
+		} else {
618
+			$limit_parts = explode(',', $query_params['limit']);
619
+			if (count($limit_parts) === 1) {
620
+				$limit_parts = [0, $limit_parts[0]];
621
+			}
622
+		}
623
+		// remove the group by and having parts of the query, as those will
624
+		// make the sql query return an array of values, instead of just a single value
625
+		unset($query_params['group_by'], $query_params['having'], $query_params['limit']);
626
+		$count = $model->count($query_params, null, true);
627
+		$pages = $count / $limit_parts[1];
628
+		$this->setResponseHeader('Total', $count, false);
629
+		$this->setResponseHeader('PageSize', $limit_parts[1], false);
630
+		$this->setResponseHeader('TotalPages', ceil($pages), false);
631
+	}
632
+
633
+
634
+	/**
635
+	 * Changes database results into REST API entities
636
+	 *
637
+	 * @param EEM_Base             $model
638
+	 * @param array                $db_row     like results from $wpdb->get_results()
639
+	 * @param WP_REST_Request|null $rest_request
640
+	 * @param string|null          $deprecated no longer used
641
+	 * @return array ready for being converted into json for sending to client
642
+	 * @throws DomainException
643
+	 * @throws EE_Error
644
+	 * @throws InvalidArgumentException
645
+	 * @throws InvalidDataTypeException
646
+	 * @throws InvalidInterfaceException
647
+	 * @throws ModelConfigurationException
648
+	 * @throws ReflectionException
649
+	 * @throws RestException
650
+	 * @throws RestPasswordIncorrectException
651
+	 * @throws RestPasswordRequiredException
652
+	 * @throws UnexpectedEntityException
653
+	 */
654
+	public function createEntityFromWpdbResult(
655
+		EEM_Base $model,
656
+		array $db_row,
657
+		?WP_REST_Request $rest_request,
658
+		string $deprecated = null
659
+	): array {
660
+		if (! $rest_request instanceof WP_REST_Request) {
661
+			// ok so this was called in the old style, where the 3rd arg was
662
+			// $include, and the 4th arg was $context
663
+			// now setup the request just to avoid fatal errors, although we won't be able
664
+			// to truly make use of it because it's kinda devoid of info
665
+			$rest_request = new WP_REST_Request();
666
+			$rest_request->set_param('include', $rest_request);
667
+			$rest_request->set_param('caps', $deprecated);
668
+		}
669
+		if ($rest_request->get_param('caps') === null) {
670
+			$rest_request->set_param('caps', EEM_Base::caps_read);
671
+		}
672
+		$current_user_full_access_to_entity = $model->currentUserCan(
673
+			EEM_Base::caps_read_admin,
674
+			$model->deduce_fields_n_values_from_cols_n_values($db_row)
675
+		);
676
+		$entity_array                       = $this->createBareEntityFromWpdbResults($model, $db_row);
677
+		$entity_array                       = $this->addExtraFields($model, $db_row, $entity_array);
678
+		$entity_array['_links']             = $this->getEntityLinks($model, $db_row, $entity_array);
679
+		// when it's a regular read request for a model with a password and the password wasn't provided
680
+		// remove the password protected fields
681
+		$has_protected_fields = false;
682
+		try {
683
+			$this->checkPassword(
684
+				$model,
685
+				$db_row,
686
+				$model->alter_query_params_to_restrict_by_ID(
687
+					$model->get_index_primary_key_string(
688
+						$model->deduce_fields_n_values_from_cols_n_values($db_row)
689
+					)
690
+				),
691
+				$rest_request
692
+			);
693
+		} catch (RestPasswordRequiredException $e) {
694
+			if ($model->hasPassword()) {
695
+				// just remove protected fields
696
+				$has_protected_fields = true;
697
+				$entity_array         = Capabilities::filterOutPasswordProtectedFields(
698
+					$entity_array,
699
+					$model,
700
+					$this->getModelVersionInfo()
701
+				);
702
+			} else {
703
+				// that's a problem. None of this should be accessible if no password was provided
704
+				throw $e;
705
+			}
706
+		}
707
+
708
+		$entity_array['_calculated_fields'] = $this->getEntityCalculations(
709
+			$model,
710
+			$db_row,
711
+			$rest_request,
712
+			$has_protected_fields
713
+		);
714
+		$entity_array                       = apply_filters(
715
+			'FHEE__Read__create_entity_from_wpdb_results__entity_before_including_requested_models',
716
+			$entity_array,
717
+			$model,
718
+			$rest_request->get_param('caps'),
719
+			$rest_request,
720
+			$this
721
+		);
722
+		// add an empty protected property for now. If it's still around after we remove everything the request didn't
723
+		// want, we'll populate it then. k?
724
+		$entity_array['_protected'] = [];
725
+		// remove any properties the request didn't want. This way _protected won't bother mentioning them
726
+		$entity_array = $this->includeOnlyRequestedProperties($model, $rest_request, $entity_array);
727
+		$entity_array = $this->includeRequestedModels(
728
+			$model,
729
+			$rest_request,
730
+			$entity_array,
731
+			$db_row,
732
+			$has_protected_fields
733
+		);
734
+		// if they still wanted the _protected property, add it.
735
+		if (isset($entity_array['_protected'])) {
736
+			$entity_array = $this->addProtectedProperty($model, $entity_array, $has_protected_fields);
737
+		}
738
+		$entity_array = apply_filters(
739
+			'FHEE__Read__create_entity_from_wpdb_results__entity_before_inaccessible_field_removal',
740
+			$entity_array,
741
+			$model,
742
+			$rest_request->get_param('caps'),
743
+			$rest_request,
744
+			$this
745
+		);
746
+		if (! $current_user_full_access_to_entity) {
747
+			$result_without_inaccessible_fields = Capabilities::filterOutInaccessibleEntityFields(
748
+				$entity_array,
749
+				$model,
750
+				$rest_request->get_param('caps'),
751
+				$this->getModelVersionInfo()
752
+			);
753
+		} else {
754
+			$result_without_inaccessible_fields = $entity_array;
755
+		}
756
+		$this->setDebugInfo(
757
+			'inaccessible fields',
758
+			array_keys(array_diff_key((array) $entity_array, (array) $result_without_inaccessible_fields))
759
+		);
760
+		return apply_filters(
761
+			'FHEE__Read__create_entity_from_wpdb_results__entity_return',
762
+			$result_without_inaccessible_fields,
763
+			$model,
764
+			$rest_request->get_param('caps')
765
+		);
766
+	}
767
+
768
+
769
+	/**
770
+	 * Returns an array describing which fields can be protected, and which actually were removed this request
771
+	 *
772
+	 * @param EEM_Base $model
773
+	 * @param array    $results_so_far
774
+	 * @param bool     $protected
775
+	 * @return array results
776
+	 * @throws EE_Error
777
+	 * @since 4.9.74.p
778
+	 */
779
+	protected function addProtectedProperty(EEM_Base $model, array $results_so_far, bool $protected): array
780
+	{
781
+		if (! $protected || ! $model->hasPassword()) {
782
+			return $results_so_far;
783
+		}
784
+		$password_field  = $model->getPasswordField();
785
+		$all_protected   = array_merge(
786
+			[$password_field->get_name()],
787
+			$password_field->protectedFields()
788
+		);
789
+		$fields_included = array_keys($results_so_far);
790
+		$fields_included = array_intersect(
791
+			$all_protected,
792
+			$fields_included
793
+		);
794
+		foreach ($fields_included as $field_name) {
795
+			$results_so_far['_protected'][] = $field_name;
796
+		}
797
+		return $results_so_far;
798
+	}
799
+
800
+
801
+	/**
802
+	 * Creates a REST entity array (JSON object we're going to return in the response, but
803
+	 * for now still a PHP array, but soon enough we'll call json_encode on it, don't worry),
804
+	 * from $wpdb->get_row( $sql, ARRAY_A)
805
+	 *
806
+	 * @param EEM_Base $model
807
+	 * @param array    $db_row
808
+	 * @return array entity mostly ready for converting to JSON and sending in the response
809
+	 * @throws EE_Error
810
+	 * @throws ReflectionException
811
+	 * @throws RestException
812
+	 * @throws Exception
813
+	 */
814
+	protected function createBareEntityFromWpdbResults(EEM_Base $model, array $db_row): array
815
+	{
816
+		$result = $model->deduce_fields_n_values_from_cols_n_values($db_row);
817
+		$result = array_intersect_key(
818
+			$result,
819
+			$this->getModelVersionInfo()->fieldsOnModelInThisVersion($model)
820
+		);
821
+		// if this is a CPT, we need to set the global $post to it,
822
+		// otherwise shortcodes etc won't work properly while rendering it
823
+		if ($model instanceof EEM_CPT_Base) {
824
+			$do_chevy_shuffle = true;
825
+		} else {
826
+			$do_chevy_shuffle = false;
827
+		}
828
+		if ($do_chevy_shuffle) {
829
+			global $post;
830
+			$old_post = $post;
831
+			$post     = get_post($result[ $model->primary_key_name() ]);
832
+			if (! $post instanceof WP_Post) {
833
+				// well that's weird, because $result is what we JUST fetched from the database
834
+				throw new RestException(
835
+					'error_fetching_post_from_database_results',
836
+					esc_html__(
837
+						'An item was retrieved from the database but it\'s not a WP_Post like it should be.',
838
+						'event_espresso'
839
+					)
840
+				);
841
+			}
842
+			$model_object_classname          = 'EE_' . $model->get_this_model_name();
843
+			$post->{$model_object_classname} = EE_Registry::instance()->load_class(
844
+				$model_object_classname,
845
+				$result,
846
+				false,
847
+				false
848
+			);
849
+		}
850
+		foreach ($result as $field_name => $field_value) {
851
+			$field_obj = $model->field_settings_for($field_name);
852
+			if ($this->isSubclassOfOne($field_obj, $this->getModelVersionInfo()->fieldsIgnored())) {
853
+				unset($result[ $field_name ]);
854
+			} elseif (
855
+				$this->isSubclassOfOne(
856
+					$field_obj,
857
+					$this->getModelVersionInfo()->fieldsThatHaveRenderedFormat()
858
+				)
859
+			) {
860
+				$result[ $field_name ] = [
861
+					'raw'      => $this->prepareFieldObjValueForJson($field_obj, $field_value),
862
+					'rendered' => $this->prepareFieldObjValueForJson($field_obj, $field_value, 'pretty'),
863
+				];
864
+			} elseif (
865
+				$this->isSubclassOfOne(
866
+					$field_obj,
867
+					$this->getModelVersionInfo()->fieldsThatHavePrettyFormat()
868
+				)
869
+			) {
870
+				$result[ $field_name ] = [
871
+					'raw'    => $this->prepareFieldObjValueForJson($field_obj, $field_value),
872
+					'pretty' => $this->prepareFieldObjValueForJson($field_obj, $field_value, 'pretty'),
873
+				];
874
+			} elseif ($field_obj instanceof EE_Datetime_Field) {
875
+				$field_value = $field_obj->prepare_for_set_from_db($field_value);
876
+				// if the value is null, but we're not supposed to permit null, then set to the field's default
877
+				if (is_null($field_value)) {
878
+					$field_value = $field_obj->getDefaultDateTimeObj();
879
+				}
880
+				if (is_null($field_value)) {
881
+					$gmt_date = $local_date = ModelDataTranslator::prepareFieldValuesForJson(
882
+						$field_obj,
883
+						$field_value,
884
+						$this->getModelVersionInfo()->requestedVersion()
885
+					);
886
+				} else {
887
+					$timezone = $field_value->getTimezone();
888
+					EEH_DTT_Helper::setTimezone($field_value, new DateTimeZone('UTC'));
889
+					$gmt_date = ModelDataTranslator::prepareFieldValuesForJson(
890
+						$field_obj,
891
+						$field_value,
892
+						$this->getModelVersionInfo()->requestedVersion()
893
+					);
894
+					EEH_DTT_Helper::setTimezone($field_value, $timezone);
895
+					$local_date = ModelDataTranslator::prepareFieldValuesForJson(
896
+						$field_obj,
897
+						$field_value,
898
+						$this->getModelVersionInfo()->requestedVersion()
899
+					);
900
+				}
901
+				$result[ $field_name . '_gmt' ] = $gmt_date;
902
+				$result[ $field_name ]          = $local_date;
903
+			} else {
904
+				$result[ $field_name ] = $this->prepareFieldObjValueForJson($field_obj, $field_value);
905
+			}
906
+		}
907
+		if ($do_chevy_shuffle) {
908
+			$post = $old_post;
909
+		}
910
+		return $result;
911
+	}
912
+
913
+
914
+	/**
915
+	 * Takes a value all the way from the DB representation, to the model object's representation, to the
916
+	 * user-facing PHP representation, to the REST API representation. (Assumes you've already taken from the DB
917
+	 * representation using $field_obj->prepare_for_set_from_db())
918
+	 *
919
+	 * @param EE_Model_Field_Base $field_obj
920
+	 * @param mixed               $value  as it's stored on a model object
921
+	 * @param string              $format valid values are 'normal' (default), 'pretty', 'datetime_obj'
922
+	 * @return array|int
923
+	 * @throws RestException if $value contains a PHP object
924
+	 * @throws EE_Error
925
+	 */
926
+	protected function prepareFieldObjValueForJson(
927
+		EE_Model_Field_Base $field_obj,
928
+		$value,
929
+		string $format = 'normal'
930
+	) {
931
+		$value = $field_obj->prepare_for_set_from_db($value);
932
+		switch ($format) {
933
+			case 'pretty':
934
+				$value = $field_obj->prepare_for_pretty_echoing($value);
935
+				break;
936
+			case 'normal':
937
+			default:
938
+				$value = $field_obj->prepare_for_get($value);
939
+				break;
940
+		}
941
+		return ModelDataTranslator::prepareFieldValuesForJson(
942
+			$field_obj,
943
+			$value,
944
+			$this->getModelVersionInfo()->requestedVersion()
945
+		);
946
+	}
947
+
948
+
949
+	/**
950
+	 * Adds a few extra fields to the entity response
951
+	 *
952
+	 * @param EEM_Base $model
953
+	 * @param array    $db_row
954
+	 * @param array    $entity_array
955
+	 * @return array modified entity
956
+	 * @throws EE_Error
957
+	 */
958
+	protected function addExtraFields(EEM_Base $model, array $db_row, array $entity_array): array
959
+	{
960
+		if ($model instanceof EEM_CPT_Base) {
961
+			$entity_array['link'] = get_permalink($db_row[ $model->get_primary_key_field()->get_qualified_column() ]);
962
+		}
963
+		return $entity_array;
964
+	}
965
+
966
+
967
+	/**
968
+	 * Gets links we want to add to the response
969
+	 *
970
+	 * @param EEM_Base        $model
971
+	 * @param array           $db_row
972
+	 * @param array           $entity_array
973
+	 * @return array the _links item in the entity
974
+	 * @throws EE_Error
975
+	 * @global WP_REST_Server $wp_rest_server
976
+	 */
977
+	protected function getEntityLinks(EEM_Base $model, array $db_row, array $entity_array): array
978
+	{
979
+		// add basic links
980
+		$links = [];
981
+		if ($model->has_primary_key_field()) {
982
+			$links['self'] = [
983
+				[
984
+					'href' => $this->getVersionedLinkTo(
985
+						EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
986
+						. '/'
987
+						. $entity_array[ $model->primary_key_name() ]
988
+					),
989
+				],
990
+			];
991
+		}
992
+		$links['collection'] = [
993
+			[
994
+				'href' => $this->getVersionedLinkTo(
995
+					EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
996
+				),
997
+			],
998
+		];
999
+		// add links to related models
1000
+		if ($model->has_primary_key_field()) {
1001
+			foreach ($this->getModelVersionInfo()->relationSettings($model) as $relation_name => $relation_obj) {
1002
+				$related_model_part                                                      =
1003
+					Read::getRelatedEntityName($relation_name, $relation_obj);
1004
+				$links[ EED_Core_Rest_Api::ee_api_link_namespace . $related_model_part ] = [
1005
+					[
1006
+						'href'   => $this->getVersionedLinkTo(
1007
+							EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
1008
+							. '/'
1009
+							. $entity_array[ $model->primary_key_name() ]
1010
+							. '/'
1011
+							. $related_model_part
1012
+						),
1013
+						'single' => $relation_obj instanceof EE_Belongs_To_Relation,
1014
+					],
1015
+				];
1016
+			}
1017
+		}
1018
+		return $links;
1019
+	}
1020
+
1021
+
1022
+	/**
1023
+	 * Adds the included models indicated in the request to the entity provided
1024
+	 *
1025
+	 * @param EEM_Base        $model
1026
+	 * @param WP_REST_Request $rest_request
1027
+	 * @param array           $entity_array
1028
+	 * @param array           $db_row
1029
+	 * @param boolean         $included_items_protected if the original item is password protected, don't include any
1030
+	 *                                                  related models.
1031
+	 * @return array the modified entity
1032
+	 * @throws DomainException
1033
+	 * @throws EE_Error
1034
+	 * @throws InvalidArgumentException
1035
+	 * @throws InvalidDataTypeException
1036
+	 * @throws InvalidInterfaceException
1037
+	 * @throws ModelConfigurationException
1038
+	 * @throws ReflectionException
1039
+	 * @throws UnexpectedEntityException
1040
+	 */
1041
+	protected function includeRequestedModels(
1042
+		EEM_Base $model,
1043
+		WP_REST_Request $rest_request,
1044
+		array $entity_array,
1045
+		array $db_row = [],
1046
+		bool $included_items_protected = false
1047
+	): array {
1048
+		// if $db_row not included, hope the entity array has what we need
1049
+		if (! $db_row) {
1050
+			$db_row = $entity_array;
1051
+		}
1052
+		$relation_settings = $this->getModelVersionInfo()->relationSettings($model);
1053
+		foreach ($relation_settings as $relation_name => $relation_obj) {
1054
+			$related_fields_to_include   = $this->explodeAndGetItemsPrefixedWith(
1055
+				$rest_request->get_param('include'),
1056
+				$relation_name
1057
+			);
1058
+			$related_fields_to_calculate = $this->explodeAndGetItemsPrefixedWith(
1059
+				$rest_request->get_param('calculate'),
1060
+				$relation_name
1061
+			);
1062
+			// did they specify they wanted to include a related model, or
1063
+			// specific fields from a related model?
1064
+			// or did they specify to calculate a field from a related model?
1065
+			if ($related_fields_to_include || $related_fields_to_calculate) {
1066
+				// if so, we should include at least some part of the related model
1067
+				$pretend_related_request = new WP_REST_Request();
1068
+				$pretend_related_request->set_query_params(
1069
+					[
1070
+						'caps'      => $rest_request->get_param('caps'),
1071
+						'include'   => $related_fields_to_include,
1072
+						'calculate' => $related_fields_to_calculate,
1073
+						'password'  => $rest_request->get_param('password'),
1074
+					]
1075
+				);
1076
+				$pretend_related_request->add_header('no_rest_headers', true);
1077
+				$primary_model_query_params = $model->alter_query_params_to_restrict_by_ID(
1078
+					$model->get_index_primary_key_string(
1079
+						$model->deduce_fields_n_values_from_cols_n_values($db_row)
1080
+					)
1081
+				);
1082
+				if (! $included_items_protected) {
1083
+					try {
1084
+						$related_results = $this->getEntitiesFromRelationUsingModelQueryParams(
1085
+							$primary_model_query_params,
1086
+							$relation_obj,
1087
+							$pretend_related_request
1088
+						);
1089
+					} catch (RestException $e) {
1090
+						$related_results = null;
1091
+					}
1092
+				} else {
1093
+					// they're protected, hide them.
1094
+					$related_results              = null;
1095
+					$entity_array['_protected'][] = Read::getRelatedEntityName($relation_name, $relation_obj);
1096
+				}
1097
+				if ($related_results instanceof WP_Error || $related_results === null) {
1098
+					$related_results = $relation_obj instanceof EE_Belongs_To_Relation
1099
+							? null
1100
+							: [];
1101
+				}
1102
+				$entity_array[ Read::getRelatedEntityName($relation_name, $relation_obj) ] = $related_results;
1103
+			}
1104
+		}
1105
+		return $entity_array;
1106
+	}
1107
+
1108
+
1109
+	/**
1110
+	 * If the user has requested only specific properties (including meta properties like _links or _protected)
1111
+	 * remove everything else.
1112
+	 *
1113
+	 * @param EEM_Base        $model
1114
+	 * @param WP_REST_Request $rest_request
1115
+	 * @param                 $entity_array
1116
+	 * @return array
1117
+	 * @throws EE_Error
1118
+	 * @since 4.9.74.p
1119
+	 */
1120
+	protected function includeOnlyRequestedProperties(
1121
+		EEM_Base $model,
1122
+		WP_REST_Request $rest_request,
1123
+		$entity_array
1124
+	): array {
1125
+
1126
+		$includes_for_this_model = $this->explodeAndGetItemsPrefixedWith($rest_request->get_param('include'), '');
1127
+		$includes_for_this_model = $this->removeModelNamesFromArray($includes_for_this_model);
1128
+		// if they passed in * or didn't specify any includes, return everything
1129
+		if (! empty($includes_for_this_model) && ! in_array('*', $includes_for_this_model, true)) {
1130
+			if ($model->has_primary_key_field()) {
1131
+				// always include the primary key. ya just gotta know that at least
1132
+				$includes_for_this_model[] = $model->primary_key_name();
1133
+			}
1134
+			if ($this->explodeAndGetItemsPrefixedWith($rest_request->get_param('calculate'), '')) {
1135
+				$includes_for_this_model[] = '_calculated_fields';
1136
+			}
1137
+			$entity_array = array_intersect_key($entity_array, array_flip($includes_for_this_model));
1138
+		}
1139
+		return $entity_array;
1140
+	}
1141
+
1142
+
1143
+	/**
1144
+	 * Returns a new array with all the names of models removed. Eg
1145
+	 * array( 'Event', 'Datetime.*', 'foobar' ) would become array( 'Datetime.*', 'foobar' )
1146
+	 *
1147
+	 * @param array $array
1148
+	 * @return array
1149
+	 */
1150
+	private function removeModelNamesFromArray(array $array): array
1151
+	{
1152
+		return array_diff($array, array_keys(EE_Registry::instance()->non_abstract_db_models));
1153
+	}
1154
+
1155
+
1156
+	/**
1157
+	 * Gets the calculated fields for the response
1158
+	 *
1159
+	 * @param EEM_Base        $model
1160
+	 * @param array           $wpdb_row
1161
+	 * @param WP_REST_Request $rest_request
1162
+	 * @param bool            $row_is_protected whether this row is password protected or not
1163
+	 * @return stdClass the _calculations item in the entity
1164
+	 * @throws RestException if a default value has a PHP object, which should never do (and if we
1165
+	 *                                          did, let's know about it ASAP, so let the exception bubble up)
1166
+	 * @throws EE_Error
1167
+	 */
1168
+	protected function getEntityCalculations(
1169
+		EEM_Base $model,
1170
+		array $wpdb_row,
1171
+		WP_REST_Request $rest_request,
1172
+		bool $row_is_protected = false
1173
+	): stdClass {
1174
+		$calculated_fields = $this->explodeAndGetItemsPrefixedWith(
1175
+			$rest_request->get_param('calculate'),
1176
+			''
1177
+		);
1178
+		// note: setting calculate=* doesn't do anything
1179
+		$calculated_fields_to_return = new stdClass();
1180
+		$protected_fields            = [];
1181
+		foreach ($calculated_fields as $field_to_calculate) {
1182
+			try {
1183
+				// it's password protected, so they shouldn't be able to read this. Remove the value
1184
+				$schema = $this->fields_calculator->getJsonSchemaForModel($model);
1185
+				if (
1186
+					$row_is_protected
1187
+					&& isset($schema['properties'][ $field_to_calculate ]['protected'])
1188
+					&& $schema['properties'][ $field_to_calculate ]['protected']
1189
+				) {
1190
+					$calculated_value   = null;
1191
+					$protected_fields[] = $field_to_calculate;
1192
+					if ($schema['properties'][ $field_to_calculate ]['type']) {
1193
+						switch ($schema['properties'][ $field_to_calculate ]['type']) {
1194
+							case 'boolean':
1195
+								$calculated_value = false;
1196
+								break;
1197
+							case 'integer':
1198
+								$calculated_value = 0;
1199
+								break;
1200
+							case 'string':
1201
+								$calculated_value = '';
1202
+								break;
1203
+							case 'array':
1204
+								$calculated_value = [];
1205
+								break;
1206
+							case 'object':
1207
+								$calculated_value = new stdClass();
1208
+								break;
1209
+						}
1210
+					}
1211
+				} else {
1212
+					$calculated_value = ModelDataTranslator::prepareFieldValueForJson(
1213
+						null,
1214
+						$this->fields_calculator->retrieveCalculatedFieldValue(
1215
+							$model,
1216
+							$field_to_calculate,
1217
+							$wpdb_row,
1218
+							$rest_request,
1219
+							$this
1220
+						),
1221
+						$this->getModelVersionInfo()->requestedVersion()
1222
+					);
1223
+				}
1224
+				$calculated_fields_to_return->{$field_to_calculate} = $calculated_value;
1225
+			} catch (RestException $e) {
1226
+				// if we don't have permission to read it, just leave it out. but let devs know about the problem
1227
+				$this->setResponseHeader(
1228
+					'Notices-Field-Calculation-Errors['
1229
+					. $e->getStringCode()
1230
+					. ']['
1231
+					. $model->get_this_model_name()
1232
+					. ']['
1233
+					. $field_to_calculate
1234
+					. ']',
1235
+					$e->getMessage()
1236
+				);
1237
+			}
1238
+		}
1239
+		$calculated_fields_to_return->_protected = $protected_fields;
1240
+		return $calculated_fields_to_return;
1241
+	}
1242
+
1243
+
1244
+	/**
1245
+	 * Gets the full URL to the resource, taking the requested version into account
1246
+	 *
1247
+	 * @param string $link_part_after_version_and_slash eg "events/10/datetimes"
1248
+	 * @return string url eg "http://mysite.com/wp-json/ee/v4.6/events/10/datetimes"
1249
+	 * @throws EE_Error
1250
+	 */
1251
+	public function getVersionedLinkTo(string $link_part_after_version_and_slash): string
1252
+	{
1253
+		return rest_url(
1254
+			EED_Core_Rest_Api::get_versioned_route_to(
1255
+				$link_part_after_version_and_slash,
1256
+				$this->getModelVersionInfo()->requestedVersion()
1257
+			)
1258
+		);
1259
+	}
1260
+
1261
+
1262
+	/**
1263
+	 * Gets the correct lowercase name for the relation in the API according
1264
+	 * to the relation's type
1265
+	 *
1266
+	 * @param string                 $relation_name
1267
+	 * @param EE_Model_Relation_Base $relation_obj
1268
+	 * @return string
1269
+	 */
1270
+	public static function getRelatedEntityName(string $relation_name, EE_Model_Relation_Base $relation_obj): string
1271
+	{
1272
+		if ($relation_obj instanceof EE_Belongs_To_Relation) {
1273
+			return strtolower($relation_name);
1274
+		}
1275
+		return EEH_Inflector::pluralize_and_lower($relation_name);
1276
+	}
1277
+
1278
+
1279
+	/**
1280
+	 * Gets the one model object with the specified id for the specified model
1281
+	 *
1282
+	 * @param EEM_Base        $model
1283
+	 * @param WP_REST_Request $request
1284
+	 * @return array
1285
+	 * @throws EE_Error
1286
+	 * @throws InvalidArgumentException
1287
+	 * @throws InvalidDataTypeException
1288
+	 * @throws InvalidInterfaceException
1289
+	 * @throws ModelConfigurationException
1290
+	 * @throws ReflectionException
1291
+	 * @throws RestException
1292
+	 * @throws RestPasswordIncorrectException
1293
+	 * @throws RestPasswordRequiredException
1294
+	 * @throws UnexpectedEntityException
1295
+	 * @throws DomainException
1296
+	 */
1297
+	public function getEntityFromModel(EEM_Base $model, WP_REST_Request $request): array
1298
+	{
1299
+		$context = $this->validateContext($request->get_param('caps'));
1300
+		return $this->getOneOrReportPermissionError($model, $request, $context);
1301
+	}
1302
+
1303
+
1304
+	/**
1305
+	 * If a context is provided which isn't valid, maybe it was added in a future
1306
+	 * version so just treat it as a default read
1307
+	 *
1308
+	 * @param string|null $context
1309
+	 * @return string array key of EEM_Base::cap_contexts_to_cap_action_map()
1310
+	 */
1311
+	public function validateContext(?string $context): string
1312
+	{
1313
+		if (! $context) {
1314
+			$context = EEM_Base::caps_read;
1315
+		}
1316
+		$valid_contexts = EEM_Base::valid_cap_contexts();
1317
+		if (in_array($context, $valid_contexts, true)) {
1318
+			return $context;
1319
+		}
1320
+		return EEM_Base::caps_read;
1321
+	}
1322
+
1323
+
1324
+	/**
1325
+	 * Verifies the passed in value is an allowable default where conditions value.
1326
+	 *
1327
+	 * @param string $default_where_conditions
1328
+	 * @return string
1329
+	 */
1330
+	public function validateDefaultQueryParams(string $default_where_conditions): string
1331
+	{
1332
+		$valid_default_where_conditions_for_api_calls = [
1333
+			EEM_Base::default_where_conditions_all,
1334
+			EEM_Base::default_where_conditions_minimum_all,
1335
+			EEM_Base::default_where_conditions_minimum_others,
1336
+		];
1337
+		if (! $default_where_conditions) {
1338
+			$default_where_conditions = EEM_Base::default_where_conditions_all;
1339
+		}
1340
+		if (
1341
+			in_array(
1342
+				$default_where_conditions,
1343
+				$valid_default_where_conditions_for_api_calls,
1344
+				true
1345
+			)
1346
+		) {
1347
+			return $default_where_conditions;
1348
+		}
1349
+		return EEM_Base::default_where_conditions_all;
1350
+	}
1351
+
1352
+
1353
+	/**
1354
+	 * Translates API filter get parameter into model query params @param EEM_Base $model
1355
+	 *
1356
+	 * @param array $query_params
1357
+	 * @return array model query params (@see
1358
+	 *               https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions)
1359
+	 *               or FALSE to indicate that absolutely no results should be returned
1360
+	 * @throws EE_Error
1361
+	 * @throws InvalidArgumentException
1362
+	 * @throws InvalidDataTypeException
1363
+	 * @throws InvalidInterfaceException
1364
+	 * @throws RestException
1365
+	 * @throws DomainException
1366
+	 * @throws ReflectionException
1367
+	 * @see
1368
+	 *               https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions.
1369
+	 *               Note: right now the query parameter keys for fields (and related fields) can be left as-is, but
1370
+	 *               it's quite possible this will change someday. Also, this method's contents might be candidate for
1371
+	 *               moving to Model_Data_Translator
1372
+	 *
1373
+	 */
1374
+	public function createModelQueryParams(EEM_Base $model, array $query_params): array
1375
+	{
1376
+		$model_query_params = [];
1377
+		if (isset($query_params['where'])) {
1378
+			$model_query_params[0] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1379
+				$query_params['where'],
1380
+				$model,
1381
+				$this->getModelVersionInfo()->requestedVersion()
1382
+			);
1383
+		}
1384
+		if (isset($query_params['order_by'])) {
1385
+			$order_by = $query_params['order_by'];
1386
+		} elseif (isset($query_params['orderby'])) {
1387
+			$order_by = $query_params['orderby'];
1388
+		} else {
1389
+			$order_by = null;
1390
+		}
1391
+		if ($order_by !== null) {
1392
+			if (is_array($order_by)) {
1393
+				$order_by = ModelDataTranslator::prepareFieldNamesInArrayKeysFromJson($order_by);
1394
+			} else {
1395
+				// it's a single item
1396
+				$order_by = ModelDataTranslator::prepareFieldNameFromJson($order_by);
1397
+			}
1398
+			$model_query_params['order_by'] = $order_by;
1399
+		}
1400
+		if (isset($query_params['group_by'])) {
1401
+			$group_by = $query_params['group_by'];
1402
+		} elseif (isset($query_params['groupby'])) {
1403
+			$group_by = $query_params['groupby'];
1404
+		} else {
1405
+			$group_by = array_keys($model->get_combined_primary_key_fields());
1406
+		}
1407
+		// make sure they're all real names
1408
+		if (is_array($group_by)) {
1409
+			$group_by = ModelDataTranslator::prepareFieldNamesFromJson($group_by);
1410
+		}
1411
+		if ($group_by !== null) {
1412
+			$model_query_params['group_by'] = $group_by;
1413
+		}
1414
+		if (isset($query_params['having'])) {
1415
+			$model_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1416
+				$query_params['having'],
1417
+				$model,
1418
+				$this->getModelVersionInfo()->requestedVersion()
1419
+			);
1420
+		}
1421
+		if (isset($query_params['order'])) {
1422
+			$model_query_params['order'] = $query_params['order'];
1423
+		}
1424
+		if (isset($query_params['mine'])) {
1425
+			$model_query_params = $model->alter_query_params_to_only_include_mine($model_query_params);
1426
+		}
1427
+		if (isset($query_params['limit'])) {
1428
+			// limit should be either a string like '23' or '23,43', or an array with two items in it
1429
+			if (! is_array($query_params['limit'])) {
1430
+				$limit_array = explode(',', (string) $query_params['limit']);
1431
+			} else {
1432
+				$limit_array = $query_params['limit'];
1433
+			}
1434
+			$sanitized_limit = [];
1435
+			foreach ($limit_array as $limit_part) {
1436
+				if ($this->debug_mode && (! is_numeric($limit_part) || count($sanitized_limit) > 2)) {
1437
+					throw new EE_Error(
1438
+						sprintf(
1439
+							esc_html__(
1440
+							// @codingStandardsIgnoreStart
1441
+								'An invalid limit filter was provided. It was: %s. If the EE4 JSON REST API weren\'t in debug mode, this message would not appear.',
1442
+								// @codingStandardsIgnoreEnd
1443
+								'event_espresso'
1444
+							),
1445
+							wp_json_encode($query_params['limit'])
1446
+						)
1447
+					);
1448
+				}
1449
+				$sanitized_limit[] = (int) $limit_part;
1450
+			}
1451
+			$model_query_params['limit'] = implode(',', $sanitized_limit);
1452
+		} else {
1453
+			$model_query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
1454
+		}
1455
+		if (isset($query_params['caps'])) {
1456
+			$model_query_params['caps'] = $this->validateContext($query_params['caps']);
1457
+		} else {
1458
+			$model_query_params['caps'] = EEM_Base::caps_read;
1459
+		}
1460
+		if (isset($query_params['default_where_conditions'])) {
1461
+			$model_query_params['default_where_conditions'] = $this->validateDefaultQueryParams(
1462
+				$query_params['default_where_conditions']
1463
+			);
1464
+		}
1465
+		// if this is a model protected by a password on another model, exclude the password protected
1466
+		// entities by default. But if they passed in a password, try to show them all. If the password is wrong,
1467
+		// though, they'll get an error (see Read::createEntityFromWpdbResult() which calls Read::checkPassword)
1468
+		if (
1469
+			$model_query_params['caps'] === EEM_Base::caps_read
1470
+			&& empty($query_params['password'])
1471
+			&& ! $model->hasPassword()
1472
+			&& $model->restrictedByRelatedModelPassword()
1473
+		) {
1474
+			$model_query_params['exclude_protected'] = true;
1475
+		}
1476
+
1477
+		return apply_filters('FHEE__Read__create_model_query_params', $model_query_params, $query_params, $model);
1478
+	}
1479
+
1480
+
1481
+	/**
1482
+	 * Changes the REST-style query params for use in the models
1483
+	 *
1484
+	 * @param EEM_Base $model
1485
+	 * @param array    $query_params sub-array from @see EEM_Base::get_all()
1486
+	 * @return array
1487
+	 * @deprecated
1488
+	 */
1489
+	public function prepareRestQueryParamsKeyForModels(EEM_Base $model, array $query_params): array
1490
+	{
1491
+		$model_ready_query_params = [];
1492
+		foreach ($query_params as $key => $value) {
1493
+			$model_ready_query_params[ $key ] = is_array($value)
1494
+				? $this->prepareRestQueryParamsKeyForModels($model, $value)
1495
+				: $value;
1496
+		}
1497
+		return $model_ready_query_params;
1498
+	}
1499
+
1500
+
1501
+	/**
1502
+	 * @param $model
1503
+	 * @param $query_params
1504
+	 * @return array
1505
+	 * @deprecated instead use ModelDataTranslator::prepareFieldValuesFromJson()
1506
+	 */
1507
+	public function prepareRestQueryParamsValuesForModels($model, $query_params): array
1508
+	{
1509
+		$model_ready_query_params = [];
1510
+		foreach ($query_params as $key => $value) {
1511
+			if (is_array($value)) {
1512
+				$model_ready_query_params[ $key ] = $this->prepareRestQueryParamsValuesForModels($model, $value);
1513
+			} else {
1514
+				$model_ready_query_params[ $key ] = $value;
1515
+			}
1516
+		}
1517
+		return $model_ready_query_params;
1518
+	}
1519
+
1520
+
1521
+	/**
1522
+	 * Explodes the string on commas, and only returns items with $prefix followed by a period.
1523
+	 * If no prefix is specified, returns items with no period.
1524
+	 *
1525
+	 * @param string|array $string_to_explode eg "jibba,jabba, blah, blah, blah" or array('jibba', 'jabba' )
1526
+	 * @param string       $prefix            "Event" or "foobar"
1527
+	 * @return array $string_to_exploded exploded on COMMAS, and if a prefix was specified
1528
+	 *                                        we only return strings starting with that and a period; if no prefix was
1529
+	 *                                        specified we return all items containing NO periods
1530
+	 */
1531
+	public function explodeAndGetItemsPrefixedWith($string_to_explode, string $prefix): array
1532
+	{
1533
+		if (is_string($string_to_explode)) {
1534
+			$exploded_contents = explode(',', $string_to_explode);
1535
+		} elseif (is_array($string_to_explode)) {
1536
+			$exploded_contents = $string_to_explode;
1537
+		} else {
1538
+			$exploded_contents = [];
1539
+		}
1540
+		// if the string was empty, we want an empty array
1541
+		$exploded_contents    = array_filter($exploded_contents);
1542
+		$contents_with_prefix = [];
1543
+		foreach ($exploded_contents as $item) {
1544
+			$item = trim($item);
1545
+			// if no prefix was provided, so we look for items with no "." in them
1546
+			if (! $prefix) {
1547
+				// does this item have a period?
1548
+				if (strpos($item, '.') === false) {
1549
+					// if not, then its what we're looking for
1550
+					$contents_with_prefix[] = $item;
1551
+				}
1552
+			} elseif (strpos($item, $prefix . '.') === 0) {
1553
+				// this item has the prefix and a period, grab it
1554
+				$contents_with_prefix[] = substr(
1555
+					$item,
1556
+					strpos($item, $prefix . '.') + strlen($prefix . '.')
1557
+				);
1558
+			} elseif ($item === $prefix) {
1559
+				// this item is JUST the prefix
1560
+				// so let's grab everything after, which is a blank string
1561
+				$contents_with_prefix[] = '';
1562
+			}
1563
+		}
1564
+		return $contents_with_prefix;
1565
+	}
1566
+
1567
+
1568
+	/**
1569
+	 * @param string      $include_string @see Read:handle_request_get_all
1570
+	 * @param string|null $model_name
1571
+	 * @return array of fields for this model. If $model_name is provided, then
1572
+	 *                                    the fields for that model, with the model's name removed from each.
1573
+	 *                                    If $include_string was blank or '*' returns an empty array
1574
+	 * @throws EE_Error
1575
+	 * @throws EE_Error
1576
+	 * @deprecated since 4.8.36.rc.001 You should instead use Read::explode_and_get_items_prefixed_with.
1577
+	 *                                    Deprecated because its return values were really quite confusing- sometimes
1578
+	 *                                    it
1579
+	 *                                    returned an empty array (when the include string was blank or '*') or
1580
+	 *                                    sometimes it returned array('*') (when you provided a model and a model of
1581
+	 *                                    that kind was found). Parses the $include_string so we fetch all the field
1582
+	 *                                    names relating to THIS model
1583
+	 *                                    (ie have NO period in them), or for the provided model (ie start with the
1584
+	 *                                    model name and then a period).
1585
+	 */
1586
+	public function extractIncludesForThisModel(string $include_string, string $model_name = null): array
1587
+	{
1588
+		if (is_array($include_string)) {
1589
+			$include_string = implode(',', $include_string);
1590
+		}
1591
+		if ($include_string === '*' || $include_string === '') {
1592
+			return [];
1593
+		}
1594
+		$includes                    = explode(',', $include_string);
1595
+		$extracted_fields_to_include = [];
1596
+		if ($model_name) {
1597
+			foreach ($includes as $field_to_include) {
1598
+				$field_to_include = trim($field_to_include);
1599
+				if (strpos($field_to_include, $model_name . '.') === 0) {
1600
+					// found the model name at the exact start
1601
+					$field_sans_model_name         = str_replace($model_name . '.', '', $field_to_include);
1602
+					$extracted_fields_to_include[] = $field_sans_model_name;
1603
+				} elseif ($field_to_include === $model_name) {
1604
+					$extracted_fields_to_include[] = '*';
1605
+				}
1606
+			}
1607
+		} else {
1608
+			// look for ones with no period
1609
+			foreach ($includes as $field_to_include) {
1610
+				$field_to_include = trim($field_to_include);
1611
+				if (
1612
+					strpos($field_to_include, '.') === false
1613
+					&& ! $this->getModelVersionInfo()->isModelNameInThisVersion($field_to_include)
1614
+				) {
1615
+					$extracted_fields_to_include[] = $field_to_include;
1616
+				}
1617
+			}
1618
+		}
1619
+		return $extracted_fields_to_include;
1620
+	}
1621
+
1622
+
1623
+	/**
1624
+	 * Gets the single item using the model according to the request in the context given, otherwise
1625
+	 * returns that it's inaccessible to the current user
1626
+	 *
1627
+	 * @param EEM_Base        $model
1628
+	 * @param WP_REST_Request $request
1629
+	 * @param null            $context
1630
+	 * @return array
1631
+	 * @throws EE_Error
1632
+	 * @throws ReflectionException
1633
+	 */
1634
+	public function getOneOrReportPermissionError(EEM_Base $model, WP_REST_Request $request, $context = null): array
1635
+	{
1636
+		$query_params = [[$model->primary_key_name() => $request->get_param('id')], 'limit' => 1];
1637
+		if ($model instanceof EEM_Soft_Delete_Base) {
1638
+			$query_params = $model->alter_query_params_so_deleted_and_undeleted_items_included($query_params);
1639
+		}
1640
+		$restricted_query_params         = $query_params;
1641
+		$restricted_query_params['caps'] = $context;
1642
+		$this->setDebugInfo('model query params', $restricted_query_params);
1643
+		$model_rows = $model->get_all_wpdb_results($restricted_query_params);
1644
+		if (! empty($model_rows)) {
1645
+			return $this->createEntityFromWpdbResult(
1646
+				$model,
1647
+				reset($model_rows),
1648
+				$request
1649
+			);
1650
+		}
1651
+		// ok let's test to see if we WOULD have found it, had we not had restrictions from missing capabilities
1652
+		$lowercase_model_name = strtolower($model->get_this_model_name());
1653
+		if ($model->exists($query_params)) {
1654
+			// you got shafted- it existed but we didn't want to tell you!
1655
+			throw new RestException(
1656
+				'rest_user_cannot_' . $context,
1657
+				sprintf(
1658
+					__('Sorry, you cannot %1$s this %2$s. Missing permissions are: %3$s', 'event_espresso'),
1659
+					$context,
1660
+					$lowercase_model_name,
1661
+					Capabilities::getMissingPermissionsString(
1662
+						$model,
1663
+						$context
1664
+					)
1665
+				),
1666
+				['status' => 403]
1667
+			);
1668
+		}
1669
+		// it's not you. It just doesn't exist
1670
+		throw new RestException(
1671
+			sprintf('rest_%s_invalid_id', $lowercase_model_name),
1672
+			sprintf(__('Invalid %s ID.', 'event_espresso'), $lowercase_model_name),
1673
+			['status' => 404]
1674
+		);
1675
+	}
1676
+
1677
+
1678
+	/**
1679
+	 * Checks that if this content requires a password to be read, that it's been provided and is correct.
1680
+	 *
1681
+	 * @param EEM_Base        $model
1682
+	 * @param array           $model_row
1683
+	 * @param array           $query_params Adds 'default_where_conditions' => 'minimum'
1684
+	 *                                      to ensure we don't confuse trashed with password protected.
1685
+	 * @param WP_REST_Request $request
1686
+	 * @throws EE_Error
1687
+	 * @throws InvalidArgumentException
1688
+	 * @throws InvalidDataTypeException
1689
+	 * @throws InvalidInterfaceException
1690
+	 * @throws RestPasswordRequiredException
1691
+	 * @throws RestPasswordIncorrectException
1692
+	 * @throws ModelConfigurationException
1693
+	 * @throws ReflectionException
1694
+	 * @since 4.9.74.p
1695
+	 */
1696
+	protected function checkPassword(EEM_Base $model, array $model_row, array $query_params, WP_REST_Request $request)
1697
+	{
1698
+		$query_params['default_where_conditions'] = 'minimum';
1699
+		// stuff is only "protected" for front-end requests. Elsewhere, you either get full permission to access the object
1700
+		// or you don't.
1701
+		$request_caps = $request->get_param('caps');
1702
+		if (isset($request_caps) && $request_caps !== EEM_Base::caps_read) {
1703
+			return;
1704
+		}
1705
+		// if this entity requires a password, they better give it and it better be right!
1706
+		if (
1707
+			$model->hasPassword()
1708
+			&& $model_row[ $model->getPasswordField()->get_qualified_column() ] !== ''
1709
+		) {
1710
+			if (empty($request['password'])) {
1711
+				throw new RestPasswordRequiredException();
1712
+			}
1713
+			if (
1714
+				! hash_equals(
1715
+					$model_row[ $model->getPasswordField()->get_qualified_column() ],
1716
+					$request['password']
1717
+				)
1718
+			) {
1719
+				throw new RestPasswordIncorrectException();
1720
+			}
1721
+		} elseif (
1722
+			// wait! maybe this content is password protected
1723
+			$model->restrictedByRelatedModelPassword()
1724
+			&& $request->get_param('caps') === EEM_Base::caps_read
1725
+		) {
1726
+			$password_supplied = $request->get_param('password');
1727
+			if (empty($password_supplied)) {
1728
+				$query_params['exclude_protected'] = true;
1729
+				if (! $model->exists($query_params)) {
1730
+					throw new RestPasswordRequiredException();
1731
+				}
1732
+			} else {
1733
+				$query_params[0][ $model->modelChainAndPassword() ] = $password_supplied;
1734
+				if (! $model->exists($query_params)) {
1735
+					throw new RestPasswordIncorrectException();
1736
+				}
1737
+			}
1738
+		}
1739
+	}
1740 1740
 }
Please login to merge, or discard this patch.
core/libraries/rest_api/controllers/model/Base.php 1 patch
Indentation   +80 added lines, -80 removed lines patch added patch discarded remove patch
@@ -18,95 +18,95 @@
 block discarded – undo
18 18
  */
19 19
 class Base extends Controller_Base
20 20
 {
21
-    /**
22
-     * Holds reference to the model version info, which knows the requested version
23
-     *
24
-     * @var ModelVersionInfo
25
-     */
26
-    protected $model_version_info;
21
+	/**
22
+	 * Holds reference to the model version info, which knows the requested version
23
+	 *
24
+	 * @var ModelVersionInfo
25
+	 */
26
+	protected $model_version_info;
27 27
 
28 28
 
29 29
 
30
-    /**
31
-     * Sets the version the user requested
32
-     *
33
-     * @param string $version eg '4.8'
34
-     */
35
-    public function setRequestedVersion(string $version)
36
-    {
37
-        parent::setRequestedVersion($version);
38
-        $this->model_version_info = new ModelVersionInfo($version);
39
-    }
30
+	/**
31
+	 * Sets the version the user requested
32
+	 *
33
+	 * @param string $version eg '4.8'
34
+	 */
35
+	public function setRequestedVersion(string $version)
36
+	{
37
+		parent::setRequestedVersion($version);
38
+		$this->model_version_info = new ModelVersionInfo($version);
39
+	}
40 40
 
41 41
 
42 42
 
43
-    /**
44
-     * Gets the object that should be used for getting any info from the models,
45
-     * because it's takes the requested and current core version into account
46
-     *
47
-     * @return ModelVersionInfo
48
-     * @throws EE_Error
49
-     */
50
-    public function getModelVersionInfo()
51
-    {
52
-        if (! $this->model_version_info) {
53
-            throw new EE_Error(
54
-                sprintf(
55
-                    esc_html__(
56
-                        'Cannot use model version info before setting the requested version in the controller',
57
-                        'event_espresso'
58
-                    )
59
-                )
60
-            );
61
-        }
62
-        return $this->model_version_info;
63
-    }
43
+	/**
44
+	 * Gets the object that should be used for getting any info from the models,
45
+	 * because it's takes the requested and current core version into account
46
+	 *
47
+	 * @return ModelVersionInfo
48
+	 * @throws EE_Error
49
+	 */
50
+	public function getModelVersionInfo()
51
+	{
52
+		if (! $this->model_version_info) {
53
+			throw new EE_Error(
54
+				sprintf(
55
+					esc_html__(
56
+						'Cannot use model version info before setting the requested version in the controller',
57
+						'event_espresso'
58
+					)
59
+				)
60
+			);
61
+		}
62
+		return $this->model_version_info;
63
+	}
64 64
 
65 65
 
66 66
 
67
-    /**
68
-     * Determines if $object is of one of the classes of $classes. Similar to
69
-     * in_array(), except this checks if $object is a subclass of the classnames provided
70
-     * in $classnames
71
-     *
72
-     * @param object $object
73
-     * @param array  $classnames
74
-     * @return boolean
75
-     */
76
-    public function isSubclassOfOne($object, $classnames)
77
-    {
78
-        foreach ($classnames as $classname) {
79
-            if (is_a($object, $classname)) {
80
-                return true;
81
-            }
82
-        }
83
-        return false;
84
-    }
67
+	/**
68
+	 * Determines if $object is of one of the classes of $classes. Similar to
69
+	 * in_array(), except this checks if $object is a subclass of the classnames provided
70
+	 * in $classnames
71
+	 *
72
+	 * @param object $object
73
+	 * @param array  $classnames
74
+	 * @return boolean
75
+	 */
76
+	public function isSubclassOfOne($object, $classnames)
77
+	{
78
+		foreach ($classnames as $classname) {
79
+			if (is_a($object, $classname)) {
80
+				return true;
81
+			}
82
+		}
83
+		return false;
84
+	}
85 85
 
86
-    /**
87
-     * Verifies the model name provided was valid. If so, returns the model (as an object). Otherwise, throws an
88
-     * exception. Must be called after `setRequestedVersion()`.
89
-     * @since 4.9.76.p
90
-     * @param $model_name
91
-     * @return EEM_Base
92
-     * @throws EE_Error
93
-     * @throws RestException
94
-     */
95
-    protected function validateModel($model_name)
96
-    {
97
-        if (! $this->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
98
-            throw new RestException(
99
-                'endpoint_parsing_error',
100
-                sprintf(
101
-                    esc_html__(
102
-                        'There is no model for endpoint %s. Please contact event espresso support',
103
-                        'event_espresso'
104
-                    ),
105
-                    $model_name
106
-                )
107
-            );
108
-        }
109
-        return $this->getModelVersionInfo()->loadModel($model_name);
110
-    }
86
+	/**
87
+	 * Verifies the model name provided was valid. If so, returns the model (as an object). Otherwise, throws an
88
+	 * exception. Must be called after `setRequestedVersion()`.
89
+	 * @since 4.9.76.p
90
+	 * @param $model_name
91
+	 * @return EEM_Base
92
+	 * @throws EE_Error
93
+	 * @throws RestException
94
+	 */
95
+	protected function validateModel($model_name)
96
+	{
97
+		if (! $this->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
98
+			throw new RestException(
99
+				'endpoint_parsing_error',
100
+				sprintf(
101
+					esc_html__(
102
+						'There is no model for endpoint %s. Please contact event espresso support',
103
+						'event_espresso'
104
+					),
105
+					$model_name
106
+				)
107
+			);
108
+		}
109
+		return $this->getModelVersionInfo()->loadModel($model_name);
110
+	}
111 111
 }
112 112
 // End of file Base.php
Please login to merge, or discard this patch.
core/libraries/rest_api/controllers/model/Write.php 2 patches
Indentation   +606 added lines, -606 removed lines patch added patch discarded remove patch
@@ -36,636 +36,636 @@
 block discarded – undo
36 36
  */
37 37
 class Write extends Base
38 38
 {
39
-    /**
40
-     * Handles requests to get all (or a filtered subset) of entities for a particular model
41
-     *
42
-     * @param WP_REST_Request $request
43
-     * @param string          $version
44
-     * @param string          $model_name
45
-     * @return WP_REST_Response
46
-     */
47
-    public static function handleRequestInsert(
48
-        WP_REST_Request $request,
49
-        string $version,
50
-        string $model_name
51
-    ): WP_REST_Response {
52
-        $controller = new Write();
53
-        try {
54
-            $controller->setRequestedVersion($version);
55
-            return $controller->sendResponse(
56
-                $controller->insert(
57
-                    $controller->getModelVersionInfo()->loadModel($model_name),
58
-                    $request
59
-                )
60
-            );
61
-        } catch (Exception $e) {
62
-            return $controller->sendResponse($e);
63
-        }
64
-    }
39
+	/**
40
+	 * Handles requests to get all (or a filtered subset) of entities for a particular model
41
+	 *
42
+	 * @param WP_REST_Request $request
43
+	 * @param string          $version
44
+	 * @param string          $model_name
45
+	 * @return WP_REST_Response
46
+	 */
47
+	public static function handleRequestInsert(
48
+		WP_REST_Request $request,
49
+		string $version,
50
+		string $model_name
51
+	): WP_REST_Response {
52
+		$controller = new Write();
53
+		try {
54
+			$controller->setRequestedVersion($version);
55
+			return $controller->sendResponse(
56
+				$controller->insert(
57
+					$controller->getModelVersionInfo()->loadModel($model_name),
58
+					$request
59
+				)
60
+			);
61
+		} catch (Exception $e) {
62
+			return $controller->sendResponse($e);
63
+		}
64
+	}
65 65
 
66 66
 
67
-    /**
68
-     * Handles a request from \WP_REST_Server to update an EE model
69
-     *
70
-     * @param WP_REST_Request $request
71
-     * @param string          $version
72
-     * @param string          $model_name
73
-     * @return WP_REST_Response
74
-     */
75
-    public static function handleRequestUpdate(
76
-        WP_REST_Request $request,
77
-        string $version,
78
-        string $model_name
79
-    ): WP_REST_Response {
80
-        $controller = new Write();
81
-        try {
82
-            $controller->setRequestedVersion($version);
83
-            return $controller->sendResponse(
84
-                $controller->update(
85
-                    $controller->getModelVersionInfo()->loadModel($model_name),
86
-                    $request
87
-                )
88
-            );
89
-        } catch (Exception $e) {
90
-            return $controller->sendResponse($e);
91
-        }
92
-    }
67
+	/**
68
+	 * Handles a request from \WP_REST_Server to update an EE model
69
+	 *
70
+	 * @param WP_REST_Request $request
71
+	 * @param string          $version
72
+	 * @param string          $model_name
73
+	 * @return WP_REST_Response
74
+	 */
75
+	public static function handleRequestUpdate(
76
+		WP_REST_Request $request,
77
+		string $version,
78
+		string $model_name
79
+	): WP_REST_Response {
80
+		$controller = new Write();
81
+		try {
82
+			$controller->setRequestedVersion($version);
83
+			return $controller->sendResponse(
84
+				$controller->update(
85
+					$controller->getModelVersionInfo()->loadModel($model_name),
86
+					$request
87
+				)
88
+			);
89
+		} catch (Exception $e) {
90
+			return $controller->sendResponse($e);
91
+		}
92
+	}
93 93
 
94 94
 
95
-    /**
96
-     * Deletes a single model object and returns it. Unless
97
-     *
98
-     * @param WP_REST_Request $request
99
-     * @param string          $version
100
-     * @param string          $model_name
101
-     * @return WP_REST_Response
102
-     */
103
-    public static function handleRequestDelete(
104
-        WP_REST_Request $request,
105
-        string $version,
106
-        string $model_name
107
-    ): WP_REST_Response {
108
-        $controller = new Write();
109
-        try {
110
-            $controller->setRequestedVersion($version);
111
-            return $controller->sendResponse(
112
-                $controller->delete(
113
-                    $controller->getModelVersionInfo()->loadModel($model_name),
114
-                    $request
115
-                )
116
-            );
117
-        } catch (Exception $e) {
118
-            return $controller->sendResponse($e);
119
-        }
120
-    }
95
+	/**
96
+	 * Deletes a single model object and returns it. Unless
97
+	 *
98
+	 * @param WP_REST_Request $request
99
+	 * @param string          $version
100
+	 * @param string          $model_name
101
+	 * @return WP_REST_Response
102
+	 */
103
+	public static function handleRequestDelete(
104
+		WP_REST_Request $request,
105
+		string $version,
106
+		string $model_name
107
+	): WP_REST_Response {
108
+		$controller = new Write();
109
+		try {
110
+			$controller->setRequestedVersion($version);
111
+			return $controller->sendResponse(
112
+				$controller->delete(
113
+					$controller->getModelVersionInfo()->loadModel($model_name),
114
+					$request
115
+				)
116
+			);
117
+		} catch (Exception $e) {
118
+			return $controller->sendResponse($e);
119
+		}
120
+	}
121 121
 
122 122
 
123
-    /**
124
-     * Inserts a new model object according to the $request
125
-     *
126
-     * @param EEM_Base        $model
127
-     * @param WP_REST_Request $request
128
-     * @return array
129
-     * @throws EE_Error
130
-     * @throws RestException
131
-     * @throws ReflectionException
132
-     * @throws Exception
133
-     */
134
-    public function insert(EEM_Base $model, WP_REST_Request $request): array
135
-    {
136
-        // echo "\n\n";
137
-        // \EEH_Debug_Tools::printr(__FUNCTION__, __CLASS__, __FILE__, __LINE__, 3);
138
-        Capabilities::verifyAtLeastPartialAccessTo($model, EEM_Base::caps_edit, 'create');
139
-        $default_cap_to_check_for = EE_Restriction_Generator_Base::get_default_restrictions_cap();
140
-        // \EEH_Debug_Tools::printr($default_cap_to_check_for, '$default_cap_to_check_for', __FILE__, __LINE__);
141
-        if (! current_user_can($default_cap_to_check_for)) {
142
-            throw new RestException(
143
-                'rest_cannot_create_' . EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
144
-                sprintf(
145
-                    esc_html__(
146
-                    // @codingStandardsIgnoreStart
147
-                        'For now, only those with the admin capability to "%1$s" are allowed to use the REST API to insert data into Event Espresso.',
148
-                        // @codingStandardsIgnoreEnd
149
-                        'event_espresso'
150
-                    ),
151
-                    $default_cap_to_check_for
152
-                ),
153
-                ['status' => 403]
154
-            );
155
-        }
156
-        $submitted_json_data = $this->getBodyParams($request);
157
-        // \EEH_Debug_Tools::printr($submitted_json_data, '$submitted_json_data', __FILE__, __LINE__);
158
-        $model_data          = ModelDataTranslator::prepareConditionsQueryParamsForModels(
159
-            $submitted_json_data,
160
-            $model,
161
-            $this->getModelVersionInfo()->requestedVersion(),
162
-            true
163
-        );
164
-        // \EEH_Debug_Tools::printr($model_data, '$model_data', __FILE__, __LINE__);
165
-        $model_obj           = EE_Registry::instance()->load_class(
166
-            $model->get_this_model_name(),
167
-            [$model_data, $model->get_timezone()],
168
-            false,
169
-            false
170
-        );
171
-        $model_obj->save();
172
-        $new_id = $model_obj->ID();
173
-        if (! $new_id) {
174
-            throw new RestException(
175
-                'rest_insertion_failed',
176
-                sprintf(esc_html__('Could not insert new %1$s', 'event_espresso'), $model->get_this_model_name())
177
-            );
178
-        }
179
-        return $this->returnModelObjAsJsonResponse($model_obj, $request);
180
-    }
123
+	/**
124
+	 * Inserts a new model object according to the $request
125
+	 *
126
+	 * @param EEM_Base        $model
127
+	 * @param WP_REST_Request $request
128
+	 * @return array
129
+	 * @throws EE_Error
130
+	 * @throws RestException
131
+	 * @throws ReflectionException
132
+	 * @throws Exception
133
+	 */
134
+	public function insert(EEM_Base $model, WP_REST_Request $request): array
135
+	{
136
+		// echo "\n\n";
137
+		// \EEH_Debug_Tools::printr(__FUNCTION__, __CLASS__, __FILE__, __LINE__, 3);
138
+		Capabilities::verifyAtLeastPartialAccessTo($model, EEM_Base::caps_edit, 'create');
139
+		$default_cap_to_check_for = EE_Restriction_Generator_Base::get_default_restrictions_cap();
140
+		// \EEH_Debug_Tools::printr($default_cap_to_check_for, '$default_cap_to_check_for', __FILE__, __LINE__);
141
+		if (! current_user_can($default_cap_to_check_for)) {
142
+			throw new RestException(
143
+				'rest_cannot_create_' . EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
144
+				sprintf(
145
+					esc_html__(
146
+					// @codingStandardsIgnoreStart
147
+						'For now, only those with the admin capability to "%1$s" are allowed to use the REST API to insert data into Event Espresso.',
148
+						// @codingStandardsIgnoreEnd
149
+						'event_espresso'
150
+					),
151
+					$default_cap_to_check_for
152
+				),
153
+				['status' => 403]
154
+			);
155
+		}
156
+		$submitted_json_data = $this->getBodyParams($request);
157
+		// \EEH_Debug_Tools::printr($submitted_json_data, '$submitted_json_data', __FILE__, __LINE__);
158
+		$model_data          = ModelDataTranslator::prepareConditionsQueryParamsForModels(
159
+			$submitted_json_data,
160
+			$model,
161
+			$this->getModelVersionInfo()->requestedVersion(),
162
+			true
163
+		);
164
+		// \EEH_Debug_Tools::printr($model_data, '$model_data', __FILE__, __LINE__);
165
+		$model_obj           = EE_Registry::instance()->load_class(
166
+			$model->get_this_model_name(),
167
+			[$model_data, $model->get_timezone()],
168
+			false,
169
+			false
170
+		);
171
+		$model_obj->save();
172
+		$new_id = $model_obj->ID();
173
+		if (! $new_id) {
174
+			throw new RestException(
175
+				'rest_insertion_failed',
176
+				sprintf(esc_html__('Could not insert new %1$s', 'event_espresso'), $model->get_this_model_name())
177
+			);
178
+		}
179
+		return $this->returnModelObjAsJsonResponse($model_obj, $request);
180
+	}
181 181
 
182 182
 
183
-    /**
184
-     * Updates an existing model object according to the $request
185
-     *
186
-     * @param EEM_Base        $model
187
-     * @param WP_REST_Request $request
188
-     * @return array
189
-     * @throws EE_Error
190
-     * @throws ReflectionException
191
-     */
192
-    public function update(EEM_Base $model, WP_REST_Request $request): array
193
-    {
194
-        Capabilities::verifyAtLeastPartialAccessTo($model, EEM_Base::caps_edit, 'edit');
195
-        $default_cap_to_check_for = EE_Restriction_Generator_Base::get_default_restrictions_cap();
196
-        if (! current_user_can($default_cap_to_check_for)) {
197
-            throw new RestException(
198
-                'rest_cannot_edit_' . EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
199
-                sprintf(
200
-                    esc_html__(
201
-                    // @codingStandardsIgnoreStart
202
-                        'For now, only those with the admin capability to "%1$s" are allowed to use the REST API to update data into Event Espresso.',
203
-                        // @codingStandardsIgnoreEnd
204
-                        'event_espresso'
205
-                    ),
206
-                    $default_cap_to_check_for
207
-                ),
208
-                ['status' => 403]
209
-            );
210
-        }
211
-        $obj_id = $request->get_param('id');
212
-        if (! $obj_id) {
213
-            throw new RestException(
214
-                'rest_edit_failed',
215
-                sprintf(esc_html__('Could not edit %1$s', 'event_espresso'), $model->get_this_model_name())
216
-            );
217
-        }
218
-        $model_data = ModelDataTranslator::prepareConditionsQueryParamsForModels(
219
-            $this->getBodyParams($request),
220
-            $model,
221
-            $this->getModelVersionInfo()->requestedVersion(),
222
-            true
223
-        );
224
-        $model_obj  = $model->get_one_by_ID($obj_id);
225
-        if (! $model_obj instanceof EE_Base_Class) {
226
-            $lowercase_model_name = strtolower($model->get_this_model_name());
227
-            throw new RestException(
228
-                sprintf('rest_%s_invalid_id', $lowercase_model_name),
229
-                sprintf(esc_html__('Invalid %s ID.', 'event_espresso'), $lowercase_model_name),
230
-                ['status' => 404]
231
-            );
232
-        }
233
-        $model_obj->save($model_data);
234
-        return $this->returnModelObjAsJsonResponse($model_obj, $request);
235
-    }
183
+	/**
184
+	 * Updates an existing model object according to the $request
185
+	 *
186
+	 * @param EEM_Base        $model
187
+	 * @param WP_REST_Request $request
188
+	 * @return array
189
+	 * @throws EE_Error
190
+	 * @throws ReflectionException
191
+	 */
192
+	public function update(EEM_Base $model, WP_REST_Request $request): array
193
+	{
194
+		Capabilities::verifyAtLeastPartialAccessTo($model, EEM_Base::caps_edit, 'edit');
195
+		$default_cap_to_check_for = EE_Restriction_Generator_Base::get_default_restrictions_cap();
196
+		if (! current_user_can($default_cap_to_check_for)) {
197
+			throw new RestException(
198
+				'rest_cannot_edit_' . EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
199
+				sprintf(
200
+					esc_html__(
201
+					// @codingStandardsIgnoreStart
202
+						'For now, only those with the admin capability to "%1$s" are allowed to use the REST API to update data into Event Espresso.',
203
+						// @codingStandardsIgnoreEnd
204
+						'event_espresso'
205
+					),
206
+					$default_cap_to_check_for
207
+				),
208
+				['status' => 403]
209
+			);
210
+		}
211
+		$obj_id = $request->get_param('id');
212
+		if (! $obj_id) {
213
+			throw new RestException(
214
+				'rest_edit_failed',
215
+				sprintf(esc_html__('Could not edit %1$s', 'event_espresso'), $model->get_this_model_name())
216
+			);
217
+		}
218
+		$model_data = ModelDataTranslator::prepareConditionsQueryParamsForModels(
219
+			$this->getBodyParams($request),
220
+			$model,
221
+			$this->getModelVersionInfo()->requestedVersion(),
222
+			true
223
+		);
224
+		$model_obj  = $model->get_one_by_ID($obj_id);
225
+		if (! $model_obj instanceof EE_Base_Class) {
226
+			$lowercase_model_name = strtolower($model->get_this_model_name());
227
+			throw new RestException(
228
+				sprintf('rest_%s_invalid_id', $lowercase_model_name),
229
+				sprintf(esc_html__('Invalid %s ID.', 'event_espresso'), $lowercase_model_name),
230
+				['status' => 404]
231
+			);
232
+		}
233
+		$model_obj->save($model_data);
234
+		return $this->returnModelObjAsJsonResponse($model_obj, $request);
235
+	}
236 236
 
237 237
 
238
-    /**
239
-     * Updates an existing model object according to the $request
240
-     *
241
-     * @param EEM_Base        $model
242
-     * @param WP_REST_Request $request
243
-     * @return array of either the soft-deleted item, or
244
-     * @throws EE_Error
245
-     * @throws ReflectionException
246
-     */
247
-    public function delete(EEM_Base $model, WP_REST_Request $request): array
248
-    {
249
-        Capabilities::verifyAtLeastPartialAccessTo($model, EEM_Base::caps_delete, 'delete');
250
-        $default_cap_to_check_for = EE_Restriction_Generator_Base::get_default_restrictions_cap();
251
-        if (! current_user_can($default_cap_to_check_for)) {
252
-            throw new RestException(
253
-                'rest_cannot_delete_' . EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
254
-                sprintf(
255
-                    esc_html__(
256
-                    // @codingStandardsIgnoreStart
257
-                        'For now, only those with the admin capability to "%1$s" are allowed to use the REST API to delete data into Event Espresso.',
258
-                        // @codingStandardsIgnoreEnd
259
-                        'event_espresso'
260
-                    ),
261
-                    $default_cap_to_check_for
262
-                ),
263
-                ['status' => 403]
264
-            );
265
-        }
266
-        $obj_id = $request->get_param('id');
267
-        // this is where we would apply more fine-grained caps
268
-        $model_obj = $model->get_one_by_ID($obj_id);
269
-        if (! $model_obj instanceof EE_Base_Class) {
270
-            $lowercase_model_name = strtolower($model->get_this_model_name());
271
-            throw new RestException(
272
-                sprintf('rest_%s_invalid_id', $lowercase_model_name),
273
-                sprintf(esc_html__('Invalid %s ID.', 'event_espresso'), $lowercase_model_name),
274
-                ['status' => 404]
275
-            );
276
-        }
277
-        $requested_permanent_delete = filter_var($request->get_param('force'), FILTER_VALIDATE_BOOLEAN);
278
-        $requested_allow_blocking   = filter_var($request->get_param('allow_blocking'), FILTER_VALIDATE_BOOLEAN);
279
-        if ($requested_permanent_delete) {
280
-            $previous = $this->returnModelObjAsJsonResponse($model_obj, $request);
281
-            $deleted  = (bool) $model->delete_permanently_by_ID($obj_id, $requested_allow_blocking);
282
-            return [
283
-                'deleted'  => $deleted,
284
-                'previous' => $previous,
285
-            ];
286
-        } else {
287
-            if ($model instanceof EEM_Soft_Delete_Base) {
288
-                $model->delete_by_ID($obj_id, $requested_allow_blocking);
289
-                return $this->returnModelObjAsJsonResponse($model_obj, $request);
290
-            } else {
291
-                throw new RestException(
292
-                    'rest_trash_not_supported',
293
-                    sprintf(
294
-                        esc_html__('%1$s do not support trashing. Set force=1 to delete.', 'event_espresso'),
295
-                        EEH_Inflector::pluralize($model->get_this_model_name())
296
-                    )
297
-                );
298
-            }
299
-        }
300
-    }
238
+	/**
239
+	 * Updates an existing model object according to the $request
240
+	 *
241
+	 * @param EEM_Base        $model
242
+	 * @param WP_REST_Request $request
243
+	 * @return array of either the soft-deleted item, or
244
+	 * @throws EE_Error
245
+	 * @throws ReflectionException
246
+	 */
247
+	public function delete(EEM_Base $model, WP_REST_Request $request): array
248
+	{
249
+		Capabilities::verifyAtLeastPartialAccessTo($model, EEM_Base::caps_delete, 'delete');
250
+		$default_cap_to_check_for = EE_Restriction_Generator_Base::get_default_restrictions_cap();
251
+		if (! current_user_can($default_cap_to_check_for)) {
252
+			throw new RestException(
253
+				'rest_cannot_delete_' . EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
254
+				sprintf(
255
+					esc_html__(
256
+					// @codingStandardsIgnoreStart
257
+						'For now, only those with the admin capability to "%1$s" are allowed to use the REST API to delete data into Event Espresso.',
258
+						// @codingStandardsIgnoreEnd
259
+						'event_espresso'
260
+					),
261
+					$default_cap_to_check_for
262
+				),
263
+				['status' => 403]
264
+			);
265
+		}
266
+		$obj_id = $request->get_param('id');
267
+		// this is where we would apply more fine-grained caps
268
+		$model_obj = $model->get_one_by_ID($obj_id);
269
+		if (! $model_obj instanceof EE_Base_Class) {
270
+			$lowercase_model_name = strtolower($model->get_this_model_name());
271
+			throw new RestException(
272
+				sprintf('rest_%s_invalid_id', $lowercase_model_name),
273
+				sprintf(esc_html__('Invalid %s ID.', 'event_espresso'), $lowercase_model_name),
274
+				['status' => 404]
275
+			);
276
+		}
277
+		$requested_permanent_delete = filter_var($request->get_param('force'), FILTER_VALIDATE_BOOLEAN);
278
+		$requested_allow_blocking   = filter_var($request->get_param('allow_blocking'), FILTER_VALIDATE_BOOLEAN);
279
+		if ($requested_permanent_delete) {
280
+			$previous = $this->returnModelObjAsJsonResponse($model_obj, $request);
281
+			$deleted  = (bool) $model->delete_permanently_by_ID($obj_id, $requested_allow_blocking);
282
+			return [
283
+				'deleted'  => $deleted,
284
+				'previous' => $previous,
285
+			];
286
+		} else {
287
+			if ($model instanceof EEM_Soft_Delete_Base) {
288
+				$model->delete_by_ID($obj_id, $requested_allow_blocking);
289
+				return $this->returnModelObjAsJsonResponse($model_obj, $request);
290
+			} else {
291
+				throw new RestException(
292
+					'rest_trash_not_supported',
293
+					sprintf(
294
+						esc_html__('%1$s do not support trashing. Set force=1 to delete.', 'event_espresso'),
295
+						EEH_Inflector::pluralize($model->get_this_model_name())
296
+					)
297
+				);
298
+			}
299
+		}
300
+	}
301 301
 
302 302
 
303
-    /**
304
-     * Returns an array ready to be converted into a JSON response, based solely on the model object
305
-     *
306
-     * @param EE_Base_Class   $model_obj
307
-     * @param WP_REST_Request $request
308
-     * @return array ready for a response
309
-     * @throws EE_Error
310
-     * @throws ReflectionException
311
-     * @throws RestException
312
-     * @throws RestPasswordIncorrectException
313
-     * @throws RestPasswordRequiredException
314
-     */
315
-    protected function returnModelObjAsJsonResponse(EE_Base_Class $model_obj, WP_REST_Request $request): array
316
-    {
317
-        $model = $model_obj->get_model();
318
-        // create an array exactly like the wpdb results row,
319
-        // so we can pass it to controllers/model/Read::create_entity_from_wpdb_result()
320
-        $simulated_db_row = [];
321
-        foreach ($model->field_settings(true) as $field_name => $field_obj) {
322
-            // we need to reconstruct the normal wpdb results, including the db-only fields
323
-            // like a secondary table's primary key. The models expect those (but don't care what value they have)
324
-            if ($field_obj instanceof EE_DB_Only_Field_Base) {
325
-                $raw_value = true;
326
-            } elseif ($field_obj instanceof EE_Datetime_Field) {
327
-                $raw_value = $model_obj->get_DateTime_object($field_name);
328
-            } else {
329
-                $raw_value = $model_obj->get_raw($field_name);
330
-            }
331
-            $simulated_db_row[ $field_obj->get_qualified_column() ] = $field_obj->prepare_for_use_in_db($raw_value);
332
-        }
333
-        /** @var Read $read_controller */
334
-        $read_controller = LoaderFactory::getLoader()->getNew(Read::class);
335
-        $read_controller->setRequestedVersion($this->getRequestedVersion());
336
-        // the simulates request really doesn't need any info downstream
337
-        $simulated_request = new WP_REST_Request('GET');
338
-        // set the caps context on the simulated according to the original request.
339
-        switch ($request->get_method()) {
340
-            case 'POST':
341
-            case 'PUT':
342
-                $caps_context = EEM_Base::caps_edit;
343
-                break;
344
-            case 'DELETE':
345
-                $caps_context = EEM_Base::caps_delete;
346
-                break;
347
-            default:
348
-                $caps_context = EEM_Base::caps_read_admin;
349
-        }
350
-        $simulated_request->set_param('caps', $caps_context);
351
-        return $read_controller->createEntityFromWpdbResult(
352
-            $model_obj->get_model(),
353
-            $simulated_db_row,
354
-            $simulated_request
355
-        );
356
-    }
303
+	/**
304
+	 * Returns an array ready to be converted into a JSON response, based solely on the model object
305
+	 *
306
+	 * @param EE_Base_Class   $model_obj
307
+	 * @param WP_REST_Request $request
308
+	 * @return array ready for a response
309
+	 * @throws EE_Error
310
+	 * @throws ReflectionException
311
+	 * @throws RestException
312
+	 * @throws RestPasswordIncorrectException
313
+	 * @throws RestPasswordRequiredException
314
+	 */
315
+	protected function returnModelObjAsJsonResponse(EE_Base_Class $model_obj, WP_REST_Request $request): array
316
+	{
317
+		$model = $model_obj->get_model();
318
+		// create an array exactly like the wpdb results row,
319
+		// so we can pass it to controllers/model/Read::create_entity_from_wpdb_result()
320
+		$simulated_db_row = [];
321
+		foreach ($model->field_settings(true) as $field_name => $field_obj) {
322
+			// we need to reconstruct the normal wpdb results, including the db-only fields
323
+			// like a secondary table's primary key. The models expect those (but don't care what value they have)
324
+			if ($field_obj instanceof EE_DB_Only_Field_Base) {
325
+				$raw_value = true;
326
+			} elseif ($field_obj instanceof EE_Datetime_Field) {
327
+				$raw_value = $model_obj->get_DateTime_object($field_name);
328
+			} else {
329
+				$raw_value = $model_obj->get_raw($field_name);
330
+			}
331
+			$simulated_db_row[ $field_obj->get_qualified_column() ] = $field_obj->prepare_for_use_in_db($raw_value);
332
+		}
333
+		/** @var Read $read_controller */
334
+		$read_controller = LoaderFactory::getLoader()->getNew(Read::class);
335
+		$read_controller->setRequestedVersion($this->getRequestedVersion());
336
+		// the simulates request really doesn't need any info downstream
337
+		$simulated_request = new WP_REST_Request('GET');
338
+		// set the caps context on the simulated according to the original request.
339
+		switch ($request->get_method()) {
340
+			case 'POST':
341
+			case 'PUT':
342
+				$caps_context = EEM_Base::caps_edit;
343
+				break;
344
+			case 'DELETE':
345
+				$caps_context = EEM_Base::caps_delete;
346
+				break;
347
+			default:
348
+				$caps_context = EEM_Base::caps_read_admin;
349
+		}
350
+		$simulated_request->set_param('caps', $caps_context);
351
+		return $read_controller->createEntityFromWpdbResult(
352
+			$model_obj->get_model(),
353
+			$simulated_db_row,
354
+			$simulated_request
355
+		);
356
+	}
357 357
 
358 358
 
359
-    /**
360
-     * Gets the item affected by this request
361
-     *
362
-     * @param EEM_Base        $model
363
-     * @param WP_REST_Request $request
364
-     * @param int|string      $obj_id
365
-     * @return WP_Error|array
366
-     * @throws EE_Error
367
-     */
368
-    protected function getOneBasedOnRequest(EEM_Base $model, WP_REST_Request $request, $obj_id)
369
-    {
370
-        $requested_version = $this->getRequestedVersion($request->get_route());
371
-        $get_request       = new WP_REST_Request(
372
-            'GET',
373
-            EED_Core_Rest_Api::ee_api_namespace
374
-            . $requested_version
375
-            . '/'
376
-            . EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
377
-            . '/'
378
-            . $obj_id
379
-        );
380
-        $get_request->set_url_params(
381
-            [
382
-                'id'      => $obj_id,
383
-                'include' => $request->get_param('include'),
384
-            ]
385
-        );
386
-        $read_controller = LoaderFactory::getLoader()->getNew(
387
-            'EventEspresso\core\libraries\rest_api\controllers\model\Read'
388
-        );
389
-        $read_controller->setRequestedVersion($this->getRequestedVersion());
390
-        return $read_controller->getEntityFromModel($model, $get_request);
391
-    }
359
+	/**
360
+	 * Gets the item affected by this request
361
+	 *
362
+	 * @param EEM_Base        $model
363
+	 * @param WP_REST_Request $request
364
+	 * @param int|string      $obj_id
365
+	 * @return WP_Error|array
366
+	 * @throws EE_Error
367
+	 */
368
+	protected function getOneBasedOnRequest(EEM_Base $model, WP_REST_Request $request, $obj_id)
369
+	{
370
+		$requested_version = $this->getRequestedVersion($request->get_route());
371
+		$get_request       = new WP_REST_Request(
372
+			'GET',
373
+			EED_Core_Rest_Api::ee_api_namespace
374
+			. $requested_version
375
+			. '/'
376
+			. EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
377
+			. '/'
378
+			. $obj_id
379
+		);
380
+		$get_request->set_url_params(
381
+			[
382
+				'id'      => $obj_id,
383
+				'include' => $request->get_param('include'),
384
+			]
385
+		);
386
+		$read_controller = LoaderFactory::getLoader()->getNew(
387
+			'EventEspresso\core\libraries\rest_api\controllers\model\Read'
388
+		);
389
+		$read_controller->setRequestedVersion($this->getRequestedVersion());
390
+		return $read_controller->getEntityFromModel($model, $get_request);
391
+	}
392 392
 
393 393
 
394
-    /**
395
-     * Adds a relation between the specified models (if it doesn't already exist.)
396
-     *
397
-     * @param WP_REST_Request $request
398
-     * @param string          $version
399
-     * @param string          $model_name
400
-     * @param string          $related_model_name
401
-     * @return WP_REST_Response
402
-     * @since 4.9.76.p
403
-     */
404
-    public static function handleRequestAddRelation(
405
-        WP_REST_Request $request,
406
-        string $version,
407
-        string $model_name,
408
-        string $related_model_name
409
-    ): WP_REST_Response {
410
-        $controller = new Write();
411
-        try {
412
-            $controller->setRequestedVersion($version);
413
-            $main_model = $controller->validateModel($model_name);
414
-            $controller->validateModel($related_model_name);
415
-            return $controller->sendResponse(
416
-                $controller->addRelation(
417
-                    $main_model,
418
-                    $main_model->related_settings_for($related_model_name),
419
-                    $request
420
-                )
421
-            );
422
-        } catch (Exception $e) {
423
-            return $controller->sendResponse($e);
424
-        }
425
-    }
394
+	/**
395
+	 * Adds a relation between the specified models (if it doesn't already exist.)
396
+	 *
397
+	 * @param WP_REST_Request $request
398
+	 * @param string          $version
399
+	 * @param string          $model_name
400
+	 * @param string          $related_model_name
401
+	 * @return WP_REST_Response
402
+	 * @since 4.9.76.p
403
+	 */
404
+	public static function handleRequestAddRelation(
405
+		WP_REST_Request $request,
406
+		string $version,
407
+		string $model_name,
408
+		string $related_model_name
409
+	): WP_REST_Response {
410
+		$controller = new Write();
411
+		try {
412
+			$controller->setRequestedVersion($version);
413
+			$main_model = $controller->validateModel($model_name);
414
+			$controller->validateModel($related_model_name);
415
+			return $controller->sendResponse(
416
+				$controller->addRelation(
417
+					$main_model,
418
+					$main_model->related_settings_for($related_model_name),
419
+					$request
420
+				)
421
+			);
422
+		} catch (Exception $e) {
423
+			return $controller->sendResponse($e);
424
+		}
425
+	}
426 426
 
427 427
 
428
-    /**
429
-     * Adds a relation between the two model specified model objects.
430
-     *
431
-     * @param EEM_Base               $model
432
-     * @param EE_Model_Relation_Base $relation
433
-     * @param WP_REST_Request        $request
434
-     * @return array
435
-     * @throws EE_Error
436
-     * @throws ReflectionException
437
-     * @throws RestException
438
-     * @throws Exception
439
-     * @since 4.9.76.p
440
-     */
441
-    public function addRelation(EEM_Base $model, EE_Model_Relation_Base $relation, WP_REST_Request $request): array
442
-    {
443
-        [$model_obj, $other_obj] = $this->getBothModelObjects($model, $relation, $request);
444
-        $extra_params = [];
445
-        if ($relation instanceof EE_HABTM_Relation) {
446
-            $extra_params = array_intersect_key(
447
-                ModelDataTranslator::prepareConditionsQueryParamsForModels(
448
-                    $request->get_body_params(),
449
-                    $relation->get_join_model(),
450
-                    $this->getModelVersionInfo()->requestedVersion(),
451
-                    true
452
-                ),
453
-                $relation->getNonKeyFields()
454
-            );
455
-        }
456
-        // Add a relation.
457
-        $related_obj = $model_obj->_add_relation_to(
458
-            $other_obj,
459
-            $relation->get_other_model()->get_this_model_name(),
460
-            $extra_params
461
-        );
462
-        $response    = [
463
-            strtolower($model->get_this_model_name())                       => $this->returnModelObjAsJsonResponse(
464
-                $model_obj,
465
-                $request
466
-            ),
467
-            strtolower($relation->get_other_model()->get_this_model_name()) => $this->returnModelObjAsJsonResponse(
468
-                $related_obj,
469
-                $request
470
-            ),
471
-        ];
472
-        if ($relation instanceof EE_HABTM_Relation) {
473
-            $join_model_obj                                                                     =
474
-                $relation->get_join_model()->get_one(
475
-                    [
476
-                        [
477
-                            $relation->get_join_model()->get_foreign_key_to($model->get_this_model_name())->get_name(
478
-                            )             => $model_obj->ID(),
479
-                            $relation->get_join_model()->get_foreign_key_to(
480
-                                $relation->get_other_model()->get_this_model_name()
481
-                            )->get_name() => $related_obj->ID(),
482
-                        ],
483
-                    ]
484
-                );
485
-            $response['join'][ strtolower($relation->get_join_model()->get_this_model_name()) ] =
486
-                $this->returnModelObjAsJsonResponse($join_model_obj, $request);
487
-        }
488
-        return $response;
489
-    }
428
+	/**
429
+	 * Adds a relation between the two model specified model objects.
430
+	 *
431
+	 * @param EEM_Base               $model
432
+	 * @param EE_Model_Relation_Base $relation
433
+	 * @param WP_REST_Request        $request
434
+	 * @return array
435
+	 * @throws EE_Error
436
+	 * @throws ReflectionException
437
+	 * @throws RestException
438
+	 * @throws Exception
439
+	 * @since 4.9.76.p
440
+	 */
441
+	public function addRelation(EEM_Base $model, EE_Model_Relation_Base $relation, WP_REST_Request $request): array
442
+	{
443
+		[$model_obj, $other_obj] = $this->getBothModelObjects($model, $relation, $request);
444
+		$extra_params = [];
445
+		if ($relation instanceof EE_HABTM_Relation) {
446
+			$extra_params = array_intersect_key(
447
+				ModelDataTranslator::prepareConditionsQueryParamsForModels(
448
+					$request->get_body_params(),
449
+					$relation->get_join_model(),
450
+					$this->getModelVersionInfo()->requestedVersion(),
451
+					true
452
+				),
453
+				$relation->getNonKeyFields()
454
+			);
455
+		}
456
+		// Add a relation.
457
+		$related_obj = $model_obj->_add_relation_to(
458
+			$other_obj,
459
+			$relation->get_other_model()->get_this_model_name(),
460
+			$extra_params
461
+		);
462
+		$response    = [
463
+			strtolower($model->get_this_model_name())                       => $this->returnModelObjAsJsonResponse(
464
+				$model_obj,
465
+				$request
466
+			),
467
+			strtolower($relation->get_other_model()->get_this_model_name()) => $this->returnModelObjAsJsonResponse(
468
+				$related_obj,
469
+				$request
470
+			),
471
+		];
472
+		if ($relation instanceof EE_HABTM_Relation) {
473
+			$join_model_obj                                                                     =
474
+				$relation->get_join_model()->get_one(
475
+					[
476
+						[
477
+							$relation->get_join_model()->get_foreign_key_to($model->get_this_model_name())->get_name(
478
+							)             => $model_obj->ID(),
479
+							$relation->get_join_model()->get_foreign_key_to(
480
+								$relation->get_other_model()->get_this_model_name()
481
+							)->get_name() => $related_obj->ID(),
482
+						],
483
+					]
484
+				);
485
+			$response['join'][ strtolower($relation->get_join_model()->get_this_model_name()) ] =
486
+				$this->returnModelObjAsJsonResponse($join_model_obj, $request);
487
+		}
488
+		return $response;
489
+	}
490 490
 
491 491
 
492
-    /**
493
-     * Removes the relation between the specified models (if it exists).
494
-     *
495
-     * @param WP_REST_Request $request
496
-     * @param                 $version
497
-     * @param                 $model_name
498
-     * @param                 $related_model_name
499
-     * @return WP_REST_Response
500
-     * @since 4.9.76.p
501
-     */
502
-    public static function handleRequestRemoveRelation(
503
-        WP_REST_Request $request,
504
-        $version,
505
-        $model_name,
506
-        $related_model_name
507
-    ): WP_REST_Response {
508
-        $controller = new Write();
509
-        try {
510
-            $controller->setRequestedVersion($version);
511
-            $main_model = $controller->getModelVersionInfo()->loadModel($model_name);
512
-            return $controller->sendResponse(
513
-                $controller->removeRelation(
514
-                    $main_model,
515
-                    $main_model->related_settings_for($related_model_name),
516
-                    $request
517
-                )
518
-            );
519
-        } catch (Exception $e) {
520
-            return $controller->sendResponse($e);
521
-        }
522
-    }
492
+	/**
493
+	 * Removes the relation between the specified models (if it exists).
494
+	 *
495
+	 * @param WP_REST_Request $request
496
+	 * @param                 $version
497
+	 * @param                 $model_name
498
+	 * @param                 $related_model_name
499
+	 * @return WP_REST_Response
500
+	 * @since 4.9.76.p
501
+	 */
502
+	public static function handleRequestRemoveRelation(
503
+		WP_REST_Request $request,
504
+		$version,
505
+		$model_name,
506
+		$related_model_name
507
+	): WP_REST_Response {
508
+		$controller = new Write();
509
+		try {
510
+			$controller->setRequestedVersion($version);
511
+			$main_model = $controller->getModelVersionInfo()->loadModel($model_name);
512
+			return $controller->sendResponse(
513
+				$controller->removeRelation(
514
+					$main_model,
515
+					$main_model->related_settings_for($related_model_name),
516
+					$request
517
+				)
518
+			);
519
+		} catch (Exception $e) {
520
+			return $controller->sendResponse($e);
521
+		}
522
+	}
523 523
 
524 524
 
525
-    /**
526
-     * Adds a relation between the two model specified model objects.
527
-     *
528
-     * @param EEM_Base               $model
529
-     * @param EE_Model_Relation_Base $relation
530
-     * @param WP_REST_Request        $request
531
-     * @return array
532
-     * @throws EE_Error
533
-     * @throws ReflectionException
534
-     * @throws RestException *@throws ReflectionException
535
-     * @throws Exception
536
-     * @since 4.9.76.p
537
-     */
538
-    public function removeRelation(EEM_Base $model, EE_Model_Relation_Base $relation, WP_REST_Request $request): array
539
-    {
540
-        // This endpoint doesn't accept body parameters (it's understandable to think it might, so let developers know
541
-        // up-front that it doesn't.)
542
-        $body_params = $request->get_body_params();
543
-        if (! empty($body_params)) {
544
-            throw new RestException(
545
-                'invalid_field',
546
-                sprintf(
547
-                    esc_html__('This endpoint doesn\'t accept post body arguments, you sent in %1$s', 'event_espresso'),
548
-                    implode(array_keys($body_params))
549
-                )
550
-            );
551
-        }
552
-        [$model_obj, $other_obj] = $this->getBothModelObjects($model, $relation, $request);
553
-        // Remember the old relation, if it used a join entry.
554
-        $join_model_obj = null;
555
-        if ($relation instanceof EE_HABTM_Relation) {
556
-            $join_model_obj = $relation->get_join_model()->get_one(
557
-                [
558
-                    [
559
-                        $model->primary_key_name()                       => $model_obj->ID(),
560
-                        $relation->get_other_model()->primary_key_name() => $other_obj->ID(),
561
-                    ],
562
-                ]
563
-            );
564
-        }
565
-        // Remove the relation.
566
-        $related_obj = $model_obj->_remove_relation_to(
567
-            $other_obj,
568
-            $relation->get_other_model()->get_this_model_name()
569
-        );
570
-        $response    = [
571
-            strtolower($model->get_this_model_name())                       => $this->returnModelObjAsJsonResponse(
572
-                $model_obj,
573
-                $request
574
-            ),
575
-            strtolower($relation->get_other_model()->get_this_model_name()) => $this->returnModelObjAsJsonResponse(
576
-                $related_obj,
577
-                $request
578
-            ),
579
-        ];
580
-        if ($relation instanceof EE_HABTM_Relation) {
581
-            $relation->get_join_model()->get_one(
582
-                [
583
-                    [
584
-                        $model->primary_key_name()                       => $model_obj->ID(),
585
-                        $relation->get_other_model()->primary_key_name() => $other_obj->ID(),
586
-                    ],
587
-                ]
588
-            );
589
-            if ($join_model_obj instanceof EE_Base_Class) {
590
-                $response['join'][ strtolower($relation->get_join_model()->get_this_model_name()) ] =
591
-                    $this->returnModelObjAsJsonResponse($join_model_obj, $request);
592
-            } else {
593
-                $response['join'][ strtolower($relation->get_join_model()->get_this_model_name()) ] = null;
594
-            }
595
-        }
596
-        return $response;
597
-    }
525
+	/**
526
+	 * Adds a relation between the two model specified model objects.
527
+	 *
528
+	 * @param EEM_Base               $model
529
+	 * @param EE_Model_Relation_Base $relation
530
+	 * @param WP_REST_Request        $request
531
+	 * @return array
532
+	 * @throws EE_Error
533
+	 * @throws ReflectionException
534
+	 * @throws RestException *@throws ReflectionException
535
+	 * @throws Exception
536
+	 * @since 4.9.76.p
537
+	 */
538
+	public function removeRelation(EEM_Base $model, EE_Model_Relation_Base $relation, WP_REST_Request $request): array
539
+	{
540
+		// This endpoint doesn't accept body parameters (it's understandable to think it might, so let developers know
541
+		// up-front that it doesn't.)
542
+		$body_params = $request->get_body_params();
543
+		if (! empty($body_params)) {
544
+			throw new RestException(
545
+				'invalid_field',
546
+				sprintf(
547
+					esc_html__('This endpoint doesn\'t accept post body arguments, you sent in %1$s', 'event_espresso'),
548
+					implode(array_keys($body_params))
549
+				)
550
+			);
551
+		}
552
+		[$model_obj, $other_obj] = $this->getBothModelObjects($model, $relation, $request);
553
+		// Remember the old relation, if it used a join entry.
554
+		$join_model_obj = null;
555
+		if ($relation instanceof EE_HABTM_Relation) {
556
+			$join_model_obj = $relation->get_join_model()->get_one(
557
+				[
558
+					[
559
+						$model->primary_key_name()                       => $model_obj->ID(),
560
+						$relation->get_other_model()->primary_key_name() => $other_obj->ID(),
561
+					],
562
+				]
563
+			);
564
+		}
565
+		// Remove the relation.
566
+		$related_obj = $model_obj->_remove_relation_to(
567
+			$other_obj,
568
+			$relation->get_other_model()->get_this_model_name()
569
+		);
570
+		$response    = [
571
+			strtolower($model->get_this_model_name())                       => $this->returnModelObjAsJsonResponse(
572
+				$model_obj,
573
+				$request
574
+			),
575
+			strtolower($relation->get_other_model()->get_this_model_name()) => $this->returnModelObjAsJsonResponse(
576
+				$related_obj,
577
+				$request
578
+			),
579
+		];
580
+		if ($relation instanceof EE_HABTM_Relation) {
581
+			$relation->get_join_model()->get_one(
582
+				[
583
+					[
584
+						$model->primary_key_name()                       => $model_obj->ID(),
585
+						$relation->get_other_model()->primary_key_name() => $other_obj->ID(),
586
+					],
587
+				]
588
+			);
589
+			if ($join_model_obj instanceof EE_Base_Class) {
590
+				$response['join'][ strtolower($relation->get_join_model()->get_this_model_name()) ] =
591
+					$this->returnModelObjAsJsonResponse($join_model_obj, $request);
592
+			} else {
593
+				$response['join'][ strtolower($relation->get_join_model()->get_this_model_name()) ] = null;
594
+			}
595
+		}
596
+		return $response;
597
+	}
598 598
 
599 599
 
600
-    /**
601
-     * Gets the model objects indicated by the model, relation object, and request.
602
-     * Throws an exception if the first object doesn't exist, and currently if the related object also doesn't exist.
603
-     * However, this behaviour may change, as we may add support for simultaneously creating and relating data.
604
-     *
605
-     * @param EEM_Base               $model
606
-     * @param EE_Model_Relation_Base $relation
607
-     * @param WP_REST_Request        $request
608
-     * @return array {
609
-     * @throws EE_Error
610
-     * @throws ReflectionException
611
-     * @throws RestException *@throws EE_Error
612
-     * @throws Exception
613
-     * @since 4.9.76.p
614
-     */
615
-    protected function getBothModelObjects(
616
-        EEM_Base $model,
617
-        EE_Model_Relation_Base $relation,
618
-        WP_REST_Request $request
619
-    ): array {
620
-        // Check generic caps. For now, we're only allowing access to this endpoint to full admins.
621
-        Capabilities::verifyAtLeastPartialAccessTo($model, EEM_Base::caps_edit, 'edit');
622
-        $default_cap_to_check_for = EE_Restriction_Generator_Base::get_default_restrictions_cap();
623
-        if (! current_user_can($default_cap_to_check_for)) {
624
-            throw new RestException(
625
-                'rest_cannot_edit_' . EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
626
-                sprintf(
627
-                    esc_html__(
628
-                    // @codingStandardsIgnoreStart
629
-                        'For now, only those with the admin capability to "%1$s" are allowed to use the REST API to add relations in Event Espresso.',
630
-                        // @codingStandardsIgnoreEnd
631
-                        'event_espresso'
632
-                    ),
633
-                    $default_cap_to_check_for
634
-                ),
635
-                ['status' => 403]
636
-            );
637
-        }
638
-        // Get the main model object.
639
-        $model_obj = $this->getOneOrThrowException($model, $request->get_param('id'));
640
-        // For now, we require the other model object to exist too. This might be relaxed later.
641
-        $other_obj = $this->getOneOrThrowException($relation->get_other_model(), $request->get_param('related_id'));
642
-        return [$model_obj, $other_obj];
643
-    }
600
+	/**
601
+	 * Gets the model objects indicated by the model, relation object, and request.
602
+	 * Throws an exception if the first object doesn't exist, and currently if the related object also doesn't exist.
603
+	 * However, this behaviour may change, as we may add support for simultaneously creating and relating data.
604
+	 *
605
+	 * @param EEM_Base               $model
606
+	 * @param EE_Model_Relation_Base $relation
607
+	 * @param WP_REST_Request        $request
608
+	 * @return array {
609
+	 * @throws EE_Error
610
+	 * @throws ReflectionException
611
+	 * @throws RestException *@throws EE_Error
612
+	 * @throws Exception
613
+	 * @since 4.9.76.p
614
+	 */
615
+	protected function getBothModelObjects(
616
+		EEM_Base $model,
617
+		EE_Model_Relation_Base $relation,
618
+		WP_REST_Request $request
619
+	): array {
620
+		// Check generic caps. For now, we're only allowing access to this endpoint to full admins.
621
+		Capabilities::verifyAtLeastPartialAccessTo($model, EEM_Base::caps_edit, 'edit');
622
+		$default_cap_to_check_for = EE_Restriction_Generator_Base::get_default_restrictions_cap();
623
+		if (! current_user_can($default_cap_to_check_for)) {
624
+			throw new RestException(
625
+				'rest_cannot_edit_' . EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
626
+				sprintf(
627
+					esc_html__(
628
+					// @codingStandardsIgnoreStart
629
+						'For now, only those with the admin capability to "%1$s" are allowed to use the REST API to add relations in Event Espresso.',
630
+						// @codingStandardsIgnoreEnd
631
+						'event_espresso'
632
+					),
633
+					$default_cap_to_check_for
634
+				),
635
+				['status' => 403]
636
+			);
637
+		}
638
+		// Get the main model object.
639
+		$model_obj = $this->getOneOrThrowException($model, $request->get_param('id'));
640
+		// For now, we require the other model object to exist too. This might be relaxed later.
641
+		$other_obj = $this->getOneOrThrowException($relation->get_other_model(), $request->get_param('related_id'));
642
+		return [$model_obj, $other_obj];
643
+	}
644 644
 
645 645
 
646
-    /**
647
-     * Gets the model with that ID or throws a REST exception.
648
-     *
649
-     * @param EEM_Base   $model
650
-     * @param int|string $id
651
-     * @return EE_Base_Class
652
-     * @throws EE_Error
653
-     * @throws ReflectionException
654
-     * @throws RestException
655
-     * @since 4.9.76.p
656
-     */
657
-    protected function getOneOrThrowException(EEM_Base $model, $id): EE_Base_Class
658
-    {
659
-        $model_obj = $model->get_one_by_ID($id);
660
-        // @todo: check they can permission for it. For now unnecessary because only full admins can use this endpoint.
661
-        if ($model_obj instanceof EE_Base_Class) {
662
-            return $model_obj;
663
-        }
664
-        $lowercase_model_name = strtolower($model->get_this_model_name());
665
-        throw new RestException(
666
-            sprintf('rest_%s_invalid_id', $lowercase_model_name),
667
-            sprintf(esc_html__('Invalid %s ID.', 'event_espresso'), $lowercase_model_name),
668
-            ['status' => 404]
669
-        );
670
-    }
646
+	/**
647
+	 * Gets the model with that ID or throws a REST exception.
648
+	 *
649
+	 * @param EEM_Base   $model
650
+	 * @param int|string $id
651
+	 * @return EE_Base_Class
652
+	 * @throws EE_Error
653
+	 * @throws ReflectionException
654
+	 * @throws RestException
655
+	 * @since 4.9.76.p
656
+	 */
657
+	protected function getOneOrThrowException(EEM_Base $model, $id): EE_Base_Class
658
+	{
659
+		$model_obj = $model->get_one_by_ID($id);
660
+		// @todo: check they can permission for it. For now unnecessary because only full admins can use this endpoint.
661
+		if ($model_obj instanceof EE_Base_Class) {
662
+			return $model_obj;
663
+		}
664
+		$lowercase_model_name = strtolower($model->get_this_model_name());
665
+		throw new RestException(
666
+			sprintf('rest_%s_invalid_id', $lowercase_model_name),
667
+			sprintf(esc_html__('Invalid %s ID.', 'event_espresso'), $lowercase_model_name),
668
+			['status' => 404]
669
+		);
670
+	}
671 671
 }
Please login to merge, or discard this patch.
Spacing   +22 added lines, -22 removed lines patch added patch discarded remove patch
@@ -138,9 +138,9 @@  discard block
 block discarded – undo
138 138
         Capabilities::verifyAtLeastPartialAccessTo($model, EEM_Base::caps_edit, 'create');
139 139
         $default_cap_to_check_for = EE_Restriction_Generator_Base::get_default_restrictions_cap();
140 140
         // \EEH_Debug_Tools::printr($default_cap_to_check_for, '$default_cap_to_check_for', __FILE__, __LINE__);
141
-        if (! current_user_can($default_cap_to_check_for)) {
141
+        if ( ! current_user_can($default_cap_to_check_for)) {
142 142
             throw new RestException(
143
-                'rest_cannot_create_' . EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
143
+                'rest_cannot_create_'.EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
144 144
                 sprintf(
145 145
                     esc_html__(
146 146
                     // @codingStandardsIgnoreStart
@@ -162,7 +162,7 @@  discard block
 block discarded – undo
162 162
             true
163 163
         );
164 164
         // \EEH_Debug_Tools::printr($model_data, '$model_data', __FILE__, __LINE__);
165
-        $model_obj           = EE_Registry::instance()->load_class(
165
+        $model_obj = EE_Registry::instance()->load_class(
166 166
             $model->get_this_model_name(),
167 167
             [$model_data, $model->get_timezone()],
168 168
             false,
@@ -170,7 +170,7 @@  discard block
 block discarded – undo
170 170
         );
171 171
         $model_obj->save();
172 172
         $new_id = $model_obj->ID();
173
-        if (! $new_id) {
173
+        if ( ! $new_id) {
174 174
             throw new RestException(
175 175
                 'rest_insertion_failed',
176 176
                 sprintf(esc_html__('Could not insert new %1$s', 'event_espresso'), $model->get_this_model_name())
@@ -193,9 +193,9 @@  discard block
 block discarded – undo
193 193
     {
194 194
         Capabilities::verifyAtLeastPartialAccessTo($model, EEM_Base::caps_edit, 'edit');
195 195
         $default_cap_to_check_for = EE_Restriction_Generator_Base::get_default_restrictions_cap();
196
-        if (! current_user_can($default_cap_to_check_for)) {
196
+        if ( ! current_user_can($default_cap_to_check_for)) {
197 197
             throw new RestException(
198
-                'rest_cannot_edit_' . EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
198
+                'rest_cannot_edit_'.EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
199 199
                 sprintf(
200 200
                     esc_html__(
201 201
                     // @codingStandardsIgnoreStart
@@ -209,7 +209,7 @@  discard block
 block discarded – undo
209 209
             );
210 210
         }
211 211
         $obj_id = $request->get_param('id');
212
-        if (! $obj_id) {
212
+        if ( ! $obj_id) {
213 213
             throw new RestException(
214 214
                 'rest_edit_failed',
215 215
                 sprintf(esc_html__('Could not edit %1$s', 'event_espresso'), $model->get_this_model_name())
@@ -221,8 +221,8 @@  discard block
 block discarded – undo
221 221
             $this->getModelVersionInfo()->requestedVersion(),
222 222
             true
223 223
         );
224
-        $model_obj  = $model->get_one_by_ID($obj_id);
225
-        if (! $model_obj instanceof EE_Base_Class) {
224
+        $model_obj = $model->get_one_by_ID($obj_id);
225
+        if ( ! $model_obj instanceof EE_Base_Class) {
226 226
             $lowercase_model_name = strtolower($model->get_this_model_name());
227 227
             throw new RestException(
228 228
                 sprintf('rest_%s_invalid_id', $lowercase_model_name),
@@ -248,9 +248,9 @@  discard block
 block discarded – undo
248 248
     {
249 249
         Capabilities::verifyAtLeastPartialAccessTo($model, EEM_Base::caps_delete, 'delete');
250 250
         $default_cap_to_check_for = EE_Restriction_Generator_Base::get_default_restrictions_cap();
251
-        if (! current_user_can($default_cap_to_check_for)) {
251
+        if ( ! current_user_can($default_cap_to_check_for)) {
252 252
             throw new RestException(
253
-                'rest_cannot_delete_' . EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
253
+                'rest_cannot_delete_'.EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
254 254
                 sprintf(
255 255
                     esc_html__(
256 256
                     // @codingStandardsIgnoreStart
@@ -266,7 +266,7 @@  discard block
 block discarded – undo
266 266
         $obj_id = $request->get_param('id');
267 267
         // this is where we would apply more fine-grained caps
268 268
         $model_obj = $model->get_one_by_ID($obj_id);
269
-        if (! $model_obj instanceof EE_Base_Class) {
269
+        if ( ! $model_obj instanceof EE_Base_Class) {
270 270
             $lowercase_model_name = strtolower($model->get_this_model_name());
271 271
             throw new RestException(
272 272
                 sprintf('rest_%s_invalid_id', $lowercase_model_name),
@@ -328,7 +328,7 @@  discard block
 block discarded – undo
328 328
             } else {
329 329
                 $raw_value = $model_obj->get_raw($field_name);
330 330
             }
331
-            $simulated_db_row[ $field_obj->get_qualified_column() ] = $field_obj->prepare_for_use_in_db($raw_value);
331
+            $simulated_db_row[$field_obj->get_qualified_column()] = $field_obj->prepare_for_use_in_db($raw_value);
332 332
         }
333 333
         /** @var Read $read_controller */
334 334
         $read_controller = LoaderFactory::getLoader()->getNew(Read::class);
@@ -459,7 +459,7 @@  discard block
 block discarded – undo
459 459
             $relation->get_other_model()->get_this_model_name(),
460 460
             $extra_params
461 461
         );
462
-        $response    = [
462
+        $response = [
463 463
             strtolower($model->get_this_model_name())                       => $this->returnModelObjAsJsonResponse(
464 464
                 $model_obj,
465 465
                 $request
@@ -470,7 +470,7 @@  discard block
 block discarded – undo
470 470
             ),
471 471
         ];
472 472
         if ($relation instanceof EE_HABTM_Relation) {
473
-            $join_model_obj                                                                     =
473
+            $join_model_obj =
474 474
                 $relation->get_join_model()->get_one(
475 475
                     [
476 476
                         [
@@ -482,7 +482,7 @@  discard block
 block discarded – undo
482 482
                         ],
483 483
                     ]
484 484
                 );
485
-            $response['join'][ strtolower($relation->get_join_model()->get_this_model_name()) ] =
485
+            $response['join'][strtolower($relation->get_join_model()->get_this_model_name())] =
486 486
                 $this->returnModelObjAsJsonResponse($join_model_obj, $request);
487 487
         }
488 488
         return $response;
@@ -540,7 +540,7 @@  discard block
 block discarded – undo
540 540
         // This endpoint doesn't accept body parameters (it's understandable to think it might, so let developers know
541 541
         // up-front that it doesn't.)
542 542
         $body_params = $request->get_body_params();
543
-        if (! empty($body_params)) {
543
+        if ( ! empty($body_params)) {
544 544
             throw new RestException(
545 545
                 'invalid_field',
546 546
                 sprintf(
@@ -567,7 +567,7 @@  discard block
 block discarded – undo
567 567
             $other_obj,
568 568
             $relation->get_other_model()->get_this_model_name()
569 569
         );
570
-        $response    = [
570
+        $response = [
571 571
             strtolower($model->get_this_model_name())                       => $this->returnModelObjAsJsonResponse(
572 572
                 $model_obj,
573 573
                 $request
@@ -587,10 +587,10 @@  discard block
 block discarded – undo
587 587
                 ]
588 588
             );
589 589
             if ($join_model_obj instanceof EE_Base_Class) {
590
-                $response['join'][ strtolower($relation->get_join_model()->get_this_model_name()) ] =
590
+                $response['join'][strtolower($relation->get_join_model()->get_this_model_name())] =
591 591
                     $this->returnModelObjAsJsonResponse($join_model_obj, $request);
592 592
             } else {
593
-                $response['join'][ strtolower($relation->get_join_model()->get_this_model_name()) ] = null;
593
+                $response['join'][strtolower($relation->get_join_model()->get_this_model_name())] = null;
594 594
             }
595 595
         }
596 596
         return $response;
@@ -620,9 +620,9 @@  discard block
 block discarded – undo
620 620
         // Check generic caps. For now, we're only allowing access to this endpoint to full admins.
621 621
         Capabilities::verifyAtLeastPartialAccessTo($model, EEM_Base::caps_edit, 'edit');
622 622
         $default_cap_to_check_for = EE_Restriction_Generator_Base::get_default_restrictions_cap();
623
-        if (! current_user_can($default_cap_to_check_for)) {
623
+        if ( ! current_user_can($default_cap_to_check_for)) {
624 624
             throw new RestException(
625
-                'rest_cannot_edit_' . EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
625
+                'rest_cannot_edit_'.EEH_Inflector::pluralize_and_lower(($model->get_this_model_name())),
626 626
                 sprintf(
627 627
                     esc_html__(
628 628
                     // @codingStandardsIgnoreStart
Please login to merge, or discard this patch.