Completed
Branch FET/extract-doc-block-from-eem... (b14d68)
by
unknown
08:28 queued 21s
created
core/db_classes/EE_Datetime.class.php 1 patch
Indentation   +1272 added lines, -1272 removed lines patch added patch discarded remove patch
@@ -13,1276 +13,1276 @@
 block discarded – undo
13 13
 class EE_Datetime extends EE_Soft_Delete_Base_Class
14 14
 {
15 15
 
16
-    /**
17
-     * constant used by get_active_status, indicates datetime has no more available spaces
18
-     */
19
-    const sold_out = 'DTS';
20
-
21
-    /**
22
-     * constant used by get_active_status, indicating datetime is still active (even is not over, can be registered-for)
23
-     */
24
-    const active = 'DTA';
25
-
26
-    /**
27
-     * constant used by get_active_status, indicating the datetime cannot be used for registrations yet, but has not
28
-     * expired
29
-     */
30
-    const upcoming = 'DTU';
31
-
32
-    /**
33
-     * Datetime is postponed
34
-     */
35
-    const postponed = 'DTP';
36
-
37
-    /**
38
-     * Datetime is cancelled
39
-     */
40
-    const cancelled = 'DTC';
41
-
42
-    /**
43
-     * constant used by get_active_status, indicates datetime has expired (event is over)
44
-     */
45
-    const expired = 'DTE';
46
-
47
-    /**
48
-     * constant used in various places indicating that an event is INACTIVE (not yet ready to be published)
49
-     */
50
-    const inactive = 'DTI';
51
-
52
-
53
-    /**
54
-     * @param array  $props_n_values    incoming values
55
-     * @param string $timezone          incoming timezone (if not set the timezone set for the website will be used.)
56
-     * @param array  $date_formats      incoming date_formats in an array where the first value is the date_format
57
-     *                                  and the second value is the time format
58
-     * @return EE_Datetime
59
-     * @throws ReflectionException
60
-     * @throws InvalidArgumentException
61
-     * @throws InvalidInterfaceException
62
-     * @throws InvalidDataTypeException
63
-     * @throws EE_Error
64
-     */
65
-    public static function new_instance($props_n_values = array(), $timezone = null, $date_formats = array())
66
-    {
67
-        $has_object = parent::_check_for_object(
68
-            $props_n_values,
69
-            __CLASS__,
70
-            $timezone,
71
-            $date_formats
72
-        );
73
-        return $has_object
74
-            ? $has_object
75
-            : new self($props_n_values, false, $timezone, $date_formats);
76
-    }
77
-
78
-
79
-    /**
80
-     * @param array  $props_n_values  incoming values from the database
81
-     * @param string $timezone        incoming timezone as set by the model.  If not set the timezone for
82
-     *                                the website will be used.
83
-     * @return EE_Datetime
84
-     * @throws ReflectionException
85
-     * @throws InvalidArgumentException
86
-     * @throws InvalidInterfaceException
87
-     * @throws InvalidDataTypeException
88
-     * @throws EE_Error
89
-     */
90
-    public static function new_instance_from_db($props_n_values = array(), $timezone = null)
91
-    {
92
-        return new self($props_n_values, true, $timezone);
93
-    }
94
-
95
-
96
-    /**
97
-     * @param $name
98
-     * @throws ReflectionException
99
-     * @throws InvalidArgumentException
100
-     * @throws InvalidInterfaceException
101
-     * @throws InvalidDataTypeException
102
-     * @throws EE_Error
103
-     */
104
-    public function set_name($name)
105
-    {
106
-        $this->set('DTT_name', $name);
107
-    }
108
-
109
-
110
-    /**
111
-     * @param $description
112
-     * @throws ReflectionException
113
-     * @throws InvalidArgumentException
114
-     * @throws InvalidInterfaceException
115
-     * @throws InvalidDataTypeException
116
-     * @throws EE_Error
117
-     */
118
-    public function set_description($description)
119
-    {
120
-        $this->set('DTT_description', $description);
121
-    }
122
-
123
-
124
-    /**
125
-     * Set event start date
126
-     * set the start date for an event
127
-     *
128
-     * @param string $date a string representation of the event's date ex:  Dec. 25, 2025 or 12-25-2025
129
-     * @throws ReflectionException
130
-     * @throws InvalidArgumentException
131
-     * @throws InvalidInterfaceException
132
-     * @throws InvalidDataTypeException
133
-     * @throws EE_Error
134
-     */
135
-    public function set_start_date($date)
136
-    {
137
-        $this->_set_date_for($date, 'DTT_EVT_start');
138
-    }
139
-
140
-
141
-    /**
142
-     * Set event start time
143
-     * set the start time for an event
144
-     *
145
-     * @param string $time a string representation of the event time ex:  9am  or  7:30 PM
146
-     * @throws ReflectionException
147
-     * @throws InvalidArgumentException
148
-     * @throws InvalidInterfaceException
149
-     * @throws InvalidDataTypeException
150
-     * @throws EE_Error
151
-     */
152
-    public function set_start_time($time)
153
-    {
154
-        $this->_set_time_for($time, 'DTT_EVT_start');
155
-    }
156
-
157
-
158
-    /**
159
-     * Set event end date
160
-     * set the end date for an event
161
-     *
162
-     * @param string $date a string representation of the event's date ex:  Dec. 25, 2025 or 12-25-2025
163
-     * @throws ReflectionException
164
-     * @throws InvalidArgumentException
165
-     * @throws InvalidInterfaceException
166
-     * @throws InvalidDataTypeException
167
-     * @throws EE_Error
168
-     */
169
-    public function set_end_date($date)
170
-    {
171
-        $this->_set_date_for($date, 'DTT_EVT_end');
172
-    }
173
-
174
-
175
-    /**
176
-     * Set event end time
177
-     * set the end time for an event
178
-     *
179
-     * @param string $time a string representation of the event time ex:  9am  or  7:30 PM
180
-     * @throws ReflectionException
181
-     * @throws InvalidArgumentException
182
-     * @throws InvalidInterfaceException
183
-     * @throws InvalidDataTypeException
184
-     * @throws EE_Error
185
-     */
186
-    public function set_end_time($time)
187
-    {
188
-        $this->_set_time_for($time, 'DTT_EVT_end');
189
-    }
190
-
191
-
192
-    /**
193
-     * Set registration limit
194
-     * set the maximum number of attendees that can be registered for this datetime slot
195
-     *
196
-     * @param int $reg_limit
197
-     * @throws ReflectionException
198
-     * @throws InvalidArgumentException
199
-     * @throws InvalidInterfaceException
200
-     * @throws InvalidDataTypeException
201
-     * @throws EE_Error
202
-     */
203
-    public function set_reg_limit($reg_limit)
204
-    {
205
-        $this->set('DTT_reg_limit', $reg_limit);
206
-    }
207
-
208
-
209
-    /**
210
-     * get the number of tickets sold for this datetime slot
211
-     *
212
-     * @return mixed int on success, FALSE on fail
213
-     * @throws ReflectionException
214
-     * @throws InvalidArgumentException
215
-     * @throws InvalidInterfaceException
216
-     * @throws InvalidDataTypeException
217
-     * @throws EE_Error
218
-     */
219
-    public function sold()
220
-    {
221
-        return $this->get_raw('DTT_sold');
222
-    }
223
-
224
-
225
-    /**
226
-     * @param int $sold
227
-     * @throws ReflectionException
228
-     * @throws InvalidArgumentException
229
-     * @throws InvalidInterfaceException
230
-     * @throws InvalidDataTypeException
231
-     * @throws EE_Error
232
-     */
233
-    public function set_sold($sold)
234
-    {
235
-        // sold can not go below zero
236
-        $sold = max(0, $sold);
237
-        $this->set('DTT_sold', $sold);
238
-    }
239
-
240
-
241
-    /**
242
-     * increments sold by amount passed by $qty
243
-     *
244
-     * @param int $qty
245
-     * @throws ReflectionException
246
-     * @throws InvalidArgumentException
247
-     * @throws InvalidInterfaceException
248
-     * @throws InvalidDataTypeException
249
-     * @throws EE_Error
250
-     */
251
-    public function increase_sold($qty = 1)
252
-    {
253
-        $sold = $this->sold() + $qty;
254
-        // remove ticket reservation
255
-        $this->decrease_reserved($qty);
256
-        $this->set_sold($sold);
257
-        do_action(
258
-            'AHEE__EE_Datetime__increase_sold',
259
-            $this,
260
-            $qty,
261
-            $sold
262
-        );
263
-    }
264
-
265
-
266
-    /**
267
-     * decrements (subtracts) sold amount passed by $qty
268
-     *
269
-     * @param int $qty
270
-     * @throws ReflectionException
271
-     * @throws InvalidArgumentException
272
-     * @throws InvalidInterfaceException
273
-     * @throws InvalidDataTypeException
274
-     * @throws EE_Error
275
-     */
276
-    public function decrease_sold($qty = 1)
277
-    {
278
-        $sold = $this->sold() - $qty;
279
-        $this->set_sold($sold);
280
-        do_action(
281
-            'AHEE__EE_Datetime__decrease_sold',
282
-            $this,
283
-            $qty,
284
-            $sold
285
-        );
286
-    }
287
-
288
-
289
-    /**
290
-     * Gets qty of reserved tickets for this datetime
291
-     *
292
-     * @return int
293
-     * @throws ReflectionException
294
-     * @throws InvalidArgumentException
295
-     * @throws InvalidInterfaceException
296
-     * @throws InvalidDataTypeException
297
-     * @throws EE_Error
298
-     */
299
-    public function reserved()
300
-    {
301
-        return $this->get_raw('DTT_reserved');
302
-    }
303
-
304
-
305
-    /**
306
-     * Sets qty of reserved tickets for this datetime
307
-     *
308
-     * @param int $reserved
309
-     * @throws ReflectionException
310
-     * @throws InvalidArgumentException
311
-     * @throws InvalidInterfaceException
312
-     * @throws InvalidDataTypeException
313
-     * @throws EE_Error
314
-     */
315
-    public function set_reserved($reserved)
316
-    {
317
-        // reserved can not go below zero
318
-        $reserved = max(0, (int) $reserved);
319
-        $this->set('DTT_reserved', $reserved);
320
-    }
321
-
322
-
323
-    /**
324
-     * increments reserved by amount passed by $qty
325
-     *
326
-     * @param int $qty
327
-     * @return void
328
-     * @throws ReflectionException
329
-     * @throws InvalidArgumentException
330
-     * @throws InvalidInterfaceException
331
-     * @throws InvalidDataTypeException
332
-     * @throws EE_Error
333
-     */
334
-    public function increase_reserved($qty = 1)
335
-    {
336
-        $reserved = $this->reserved() + absint($qty);
337
-        do_action(
338
-            'AHEE__EE_Datetime__increase_reserved',
339
-            $this,
340
-            $qty,
341
-            $reserved
342
-        );
343
-        $this->set_reserved($reserved);
344
-    }
345
-
346
-
347
-    /**
348
-     * decrements (subtracts) reserved by amount passed by $qty
349
-     *
350
-     * @param int $qty
351
-     * @return void
352
-     * @throws ReflectionException
353
-     * @throws InvalidArgumentException
354
-     * @throws InvalidInterfaceException
355
-     * @throws InvalidDataTypeException
356
-     * @throws EE_Error
357
-     */
358
-    public function decrease_reserved($qty = 1)
359
-    {
360
-        $reserved = $this->reserved() - absint($qty);
361
-        do_action(
362
-            'AHEE__EE_Datetime__decrease_reserved',
363
-            $this,
364
-            $qty,
365
-            $reserved
366
-        );
367
-        $this->set_reserved($reserved);
368
-    }
369
-
370
-
371
-    /**
372
-     * total sold and reserved tickets
373
-     *
374
-     * @return int
375
-     * @throws ReflectionException
376
-     * @throws InvalidArgumentException
377
-     * @throws InvalidInterfaceException
378
-     * @throws InvalidDataTypeException
379
-     * @throws EE_Error
380
-     */
381
-    public function sold_and_reserved()
382
-    {
383
-        return $this->sold() + $this->reserved();
384
-    }
385
-
386
-
387
-    /**
388
-     * returns the datetime name
389
-     *
390
-     * @return string
391
-     * @throws ReflectionException
392
-     * @throws InvalidArgumentException
393
-     * @throws InvalidInterfaceException
394
-     * @throws InvalidDataTypeException
395
-     * @throws EE_Error
396
-     */
397
-    public function name()
398
-    {
399
-        return $this->get('DTT_name');
400
-    }
401
-
402
-
403
-    /**
404
-     * returns the datetime description
405
-     *
406
-     * @return string
407
-     * @throws ReflectionException
408
-     * @throws InvalidArgumentException
409
-     * @throws InvalidInterfaceException
410
-     * @throws InvalidDataTypeException
411
-     * @throws EE_Error
412
-     */
413
-    public function description()
414
-    {
415
-        return $this->get('DTT_description');
416
-    }
417
-
418
-
419
-    /**
420
-     * This helper simply returns whether the event_datetime for the current datetime is a primary datetime
421
-     *
422
-     * @return boolean  TRUE if is primary, FALSE if not.
423
-     * @throws ReflectionException
424
-     * @throws InvalidArgumentException
425
-     * @throws InvalidInterfaceException
426
-     * @throws InvalidDataTypeException
427
-     * @throws EE_Error
428
-     */
429
-    public function is_primary()
430
-    {
431
-        return $this->get('DTT_is_primary');
432
-    }
433
-
434
-
435
-    /**
436
-     * This helper simply returns the order for the datetime
437
-     *
438
-     * @return int  The order of the datetime for this event.
439
-     * @throws ReflectionException
440
-     * @throws InvalidArgumentException
441
-     * @throws InvalidInterfaceException
442
-     * @throws InvalidDataTypeException
443
-     * @throws EE_Error
444
-     */
445
-    public function order()
446
-    {
447
-        return $this->get('DTT_order');
448
-    }
449
-
450
-
451
-    /**
452
-     * This helper simply returns the parent id for the datetime
453
-     *
454
-     * @return int
455
-     * @throws ReflectionException
456
-     * @throws InvalidArgumentException
457
-     * @throws InvalidInterfaceException
458
-     * @throws InvalidDataTypeException
459
-     * @throws EE_Error
460
-     */
461
-    public function parent()
462
-    {
463
-        return $this->get('DTT_parent');
464
-    }
465
-
466
-
467
-    /**
468
-     * show date and/or time
469
-     *
470
-     * @param string $date_or_time    whether to display a date or time or both
471
-     * @param string $start_or_end    whether to display start or end datetimes
472
-     * @param string $dt_frmt
473
-     * @param string $tm_frmt
474
-     * @param bool   $echo            whether we echo or return (note echoing uses "pretty" formats,
475
-     *                                otherwise we use the standard formats)
476
-     * @return string|bool  string on success, FALSE on fail
477
-     * @throws ReflectionException
478
-     * @throws InvalidArgumentException
479
-     * @throws InvalidInterfaceException
480
-     * @throws InvalidDataTypeException
481
-     * @throws EE_Error
482
-     */
483
-    private function _show_datetime(
484
-        $date_or_time = null,
485
-        $start_or_end = 'start',
486
-        $dt_frmt = '',
487
-        $tm_frmt = '',
488
-        $echo = false
489
-    ) {
490
-        $field_name = "DTT_EVT_{$start_or_end}";
491
-        $dtt = $this->_get_datetime(
492
-            $field_name,
493
-            $dt_frmt,
494
-            $tm_frmt,
495
-            $date_or_time,
496
-            $echo
497
-        );
498
-        if (! $echo) {
499
-            return $dtt;
500
-        }
501
-        return '';
502
-    }
503
-
504
-
505
-    /**
506
-     * get event start date.  Provide either the date format, or NULL to re-use the
507
-     * last-used format, or '' to use the default date format
508
-     *
509
-     * @param string $dt_frmt string representation of date format defaults to 'F j, Y'
510
-     * @return mixed            string on success, FALSE on fail
511
-     * @throws ReflectionException
512
-     * @throws InvalidArgumentException
513
-     * @throws InvalidInterfaceException
514
-     * @throws InvalidDataTypeException
515
-     * @throws EE_Error
516
-     */
517
-    public function start_date($dt_frmt = '')
518
-    {
519
-        return $this->_show_datetime('D', 'start', $dt_frmt);
520
-    }
521
-
522
-
523
-    /**
524
-     * Echoes start_date()
525
-     *
526
-     * @param string $dt_frmt
527
-     * @throws ReflectionException
528
-     * @throws InvalidArgumentException
529
-     * @throws InvalidInterfaceException
530
-     * @throws InvalidDataTypeException
531
-     * @throws EE_Error
532
-     */
533
-    public function e_start_date($dt_frmt = '')
534
-    {
535
-        $this->_show_datetime('D', 'start', $dt_frmt, null, true);
536
-    }
537
-
538
-
539
-    /**
540
-     * get end date. Provide either the date format, or NULL to re-use the
541
-     * last-used format, or '' to use the default date format
542
-     *
543
-     * @param string $dt_frmt string representation of date format defaults to 'F j, Y'
544
-     * @return mixed            string on success, FALSE on fail
545
-     * @throws ReflectionException
546
-     * @throws InvalidArgumentException
547
-     * @throws InvalidInterfaceException
548
-     * @throws InvalidDataTypeException
549
-     * @throws EE_Error
550
-     */
551
-    public function end_date($dt_frmt = '')
552
-    {
553
-        return $this->_show_datetime('D', 'end', $dt_frmt);
554
-    }
555
-
556
-
557
-    /**
558
-     * Echoes the end date. See end_date()
559
-     *
560
-     * @param string $dt_frmt
561
-     * @throws ReflectionException
562
-     * @throws InvalidArgumentException
563
-     * @throws InvalidInterfaceException
564
-     * @throws InvalidDataTypeException
565
-     * @throws EE_Error
566
-     */
567
-    public function e_end_date($dt_frmt = '')
568
-    {
569
-        $this->_show_datetime('D', 'end', $dt_frmt, null, true);
570
-    }
571
-
572
-
573
-    /**
574
-     * get date_range - meaning the start AND end date
575
-     *
576
-     * @access public
577
-     * @param string $dt_frmt     string representation of date format defaults to WP settings
578
-     * @param string $conjunction conjunction junction what's your function ?
579
-     *                            this string joins the start date with the end date ie: Jan 01 "to" Dec 31
580
-     * @return mixed              string on success, FALSE on fail
581
-     * @throws ReflectionException
582
-     * @throws InvalidArgumentException
583
-     * @throws InvalidInterfaceException
584
-     * @throws InvalidDataTypeException
585
-     * @throws EE_Error
586
-     */
587
-    public function date_range($dt_frmt = '', $conjunction = ' - ')
588
-    {
589
-        $dt_frmt = ! empty($dt_frmt) ? $dt_frmt : $this->_dt_frmt;
590
-        $start = str_replace(
591
-            ' ',
592
-            ' ',
593
-            $this->get_i18n_datetime('DTT_EVT_start', $dt_frmt)
594
-        );
595
-        $end = str_replace(
596
-            ' ',
597
-            ' ',
598
-            $this->get_i18n_datetime('DTT_EVT_end', $dt_frmt)
599
-        );
600
-        return $start !== $end ? $start . $conjunction . $end : $start;
601
-    }
602
-
603
-
604
-    /**
605
-     * @param string $dt_frmt
606
-     * @param string $conjunction
607
-     * @throws ReflectionException
608
-     * @throws InvalidArgumentException
609
-     * @throws InvalidInterfaceException
610
-     * @throws InvalidDataTypeException
611
-     * @throws EE_Error
612
-     */
613
-    public function e_date_range($dt_frmt = '', $conjunction = ' - ')
614
-    {
615
-        echo $this->date_range($dt_frmt, $conjunction);
616
-    }
617
-
618
-
619
-    /**
620
-     * get start time
621
-     *
622
-     * @param string $tm_format - string representation of time format defaults to 'g:i a'
623
-     * @return mixed        string on success, FALSE on fail
624
-     * @throws ReflectionException
625
-     * @throws InvalidArgumentException
626
-     * @throws InvalidInterfaceException
627
-     * @throws InvalidDataTypeException
628
-     * @throws EE_Error
629
-     */
630
-    public function start_time($tm_format = '')
631
-    {
632
-        return $this->_show_datetime('T', 'start', null, $tm_format);
633
-    }
634
-
635
-
636
-    /**
637
-     * @param string $tm_format
638
-     * @throws ReflectionException
639
-     * @throws InvalidArgumentException
640
-     * @throws InvalidInterfaceException
641
-     * @throws InvalidDataTypeException
642
-     * @throws EE_Error
643
-     */
644
-    public function e_start_time($tm_format = '')
645
-    {
646
-        $this->_show_datetime('T', 'start', null, $tm_format, true);
647
-    }
648
-
649
-
650
-    /**
651
-     * get end time
652
-     *
653
-     * @param string $tm_format string representation of time format defaults to 'g:i a'
654
-     * @return mixed                string on success, FALSE on fail
655
-     * @throws ReflectionException
656
-     * @throws InvalidArgumentException
657
-     * @throws InvalidInterfaceException
658
-     * @throws InvalidDataTypeException
659
-     * @throws EE_Error
660
-     */
661
-    public function end_time($tm_format = '')
662
-    {
663
-        return $this->_show_datetime('T', 'end', null, $tm_format);
664
-    }
665
-
666
-
667
-    /**
668
-     * @param string $tm_format
669
-     * @throws ReflectionException
670
-     * @throws InvalidArgumentException
671
-     * @throws InvalidInterfaceException
672
-     * @throws InvalidDataTypeException
673
-     * @throws EE_Error
674
-     */
675
-    public function e_end_time($tm_format = '')
676
-    {
677
-        $this->_show_datetime('T', 'end', null, $tm_format, true);
678
-    }
679
-
680
-
681
-    /**
682
-     * get time_range
683
-     *
684
-     * @access public
685
-     * @param string $tm_format   string representation of time format defaults to 'g:i a'
686
-     * @param string $conjunction conjunction junction what's your function ?
687
-     *                            this string joins the start date with the end date ie: Jan 01 "to" Dec 31
688
-     * @return mixed              string on success, FALSE on fail
689
-     * @throws ReflectionException
690
-     * @throws InvalidArgumentException
691
-     * @throws InvalidInterfaceException
692
-     * @throws InvalidDataTypeException
693
-     * @throws EE_Error
694
-     */
695
-    public function time_range($tm_format = '', $conjunction = ' - ')
696
-    {
697
-        $tm_format = ! empty($tm_format) ? $tm_format : $this->_tm_frmt;
698
-        $start = str_replace(
699
-            ' ',
700
-            ' ',
701
-            $this->get_i18n_datetime('DTT_EVT_start', $tm_format)
702
-        );
703
-        $end = str_replace(
704
-            ' ',
705
-            ' ',
706
-            $this->get_i18n_datetime('DTT_EVT_end', $tm_format)
707
-        );
708
-        return $start !== $end ? $start . $conjunction . $end : $start;
709
-    }
710
-
711
-
712
-    /**
713
-     * @param string $tm_format
714
-     * @param string $conjunction
715
-     * @throws ReflectionException
716
-     * @throws InvalidArgumentException
717
-     * @throws InvalidInterfaceException
718
-     * @throws InvalidDataTypeException
719
-     * @throws EE_Error
720
-     */
721
-    public function e_time_range($tm_format = '', $conjunction = ' - ')
722
-    {
723
-        echo $this->time_range($tm_format, $conjunction);
724
-    }
725
-
726
-
727
-    /**
728
-     * This returns a range representation of the date and times.
729
-     * Output is dependent on the difference (or similarity) between DTT_EVT_start and DTT_EVT_end.
730
-     * Also, the return value is localized.
731
-     *
732
-     * @param string $dt_format
733
-     * @param string $tm_format
734
-     * @param string $conjunction used between two different dates or times.
735
-     *                            ex: Dec 1{$conjunction}}Dec 6, or 2pm{$conjunction}3pm
736
-     * @param string $separator   used between the date and time formats.
737
-     *                            ex: Dec 1, 2016{$separator}2pm
738
-     * @return string
739
-     * @throws ReflectionException
740
-     * @throws InvalidArgumentException
741
-     * @throws InvalidInterfaceException
742
-     * @throws InvalidDataTypeException
743
-     * @throws EE_Error
744
-     */
745
-    public function date_and_time_range(
746
-        $dt_format = '',
747
-        $tm_format = '',
748
-        $conjunction = ' - ',
749
-        $separator = ' '
750
-    ) {
751
-        $dt_format = ! empty($dt_format) ? $dt_format : $this->_dt_frmt;
752
-        $tm_format = ! empty($tm_format) ? $tm_format : $this->_tm_frmt;
753
-        $full_format = $dt_format . $separator . $tm_format;
754
-        // the range output depends on various conditions
755
-        switch (true) {
756
-            // start date timestamp and end date timestamp are the same.
757
-            case ($this->get_raw('DTT_EVT_start') === $this->get_raw('DTT_EVT_end')):
758
-                $output = $this->get_i18n_datetime('DTT_EVT_start', $full_format);
759
-                break;
760
-            // start and end date are the same but times are different
761
-            case ($this->start_date() === $this->end_date()):
762
-                $output = $this->get_i18n_datetime('DTT_EVT_start', $full_format)
763
-                          . $conjunction
764
-                          . $this->get_i18n_datetime('DTT_EVT_end', $tm_format);
765
-                break;
766
-            // all other conditions
767
-            default:
768
-                $output = $this->get_i18n_datetime('DTT_EVT_start', $full_format)
769
-                          . $conjunction
770
-                          . $this->get_i18n_datetime('DTT_EVT_end', $full_format);
771
-                break;
772
-        }
773
-        return $output;
774
-    }
775
-
776
-
777
-    /**
778
-     * This echos the results of date and time range.
779
-     *
780
-     * @see date_and_time_range() for more details on purpose.
781
-     * @param string $dt_format
782
-     * @param string $tm_format
783
-     * @param string $conjunction
784
-     * @return void
785
-     * @throws ReflectionException
786
-     * @throws InvalidArgumentException
787
-     * @throws InvalidInterfaceException
788
-     * @throws InvalidDataTypeException
789
-     * @throws EE_Error
790
-     */
791
-    public function e_date_and_time_range($dt_format = '', $tm_format = '', $conjunction = ' - ')
792
-    {
793
-        echo $this->date_and_time_range($dt_format, $tm_format, $conjunction);
794
-    }
795
-
796
-
797
-    /**
798
-     * get start date and start time
799
-     *
800
-     * @param    string $dt_format - string representation of date format defaults to 'F j, Y'
801
-     * @param    string $tm_format - string representation of time format defaults to 'g:i a'
802
-     * @return    mixed    string on success, FALSE on fail
803
-     * @throws ReflectionException
804
-     * @throws InvalidArgumentException
805
-     * @throws InvalidInterfaceException
806
-     * @throws InvalidDataTypeException
807
-     * @throws EE_Error
808
-     */
809
-    public function start_date_and_time($dt_format = '', $tm_format = '')
810
-    {
811
-        return $this->_show_datetime('', 'start', $dt_format, $tm_format);
812
-    }
813
-
814
-
815
-    /**
816
-     * @param string $dt_frmt
817
-     * @param string $tm_format
818
-     * @throws ReflectionException
819
-     * @throws InvalidArgumentException
820
-     * @throws InvalidInterfaceException
821
-     * @throws InvalidDataTypeException
822
-     * @throws EE_Error
823
-     */
824
-    public function e_start_date_and_time($dt_frmt = '', $tm_format = '')
825
-    {
826
-        $this->_show_datetime('', 'start', $dt_frmt, $tm_format, true);
827
-    }
828
-
829
-
830
-    /**
831
-     * Shows the length of the event (start to end time).
832
-     * Can be shown in 'seconds','minutes','hours', or 'days'.
833
-     * By default, rounds up. (So if you use 'days', and then event
834
-     * only occurs for 1 hour, it will return 1 day).
835
-     *
836
-     * @param string $units 'seconds','minutes','hours','days'
837
-     * @param bool   $round_up
838
-     * @return float|int|mixed
839
-     * @throws ReflectionException
840
-     * @throws InvalidArgumentException
841
-     * @throws InvalidInterfaceException
842
-     * @throws InvalidDataTypeException
843
-     * @throws EE_Error
844
-     */
845
-    public function length($units = 'seconds', $round_up = false)
846
-    {
847
-        $start = $this->get_raw('DTT_EVT_start');
848
-        $end = $this->get_raw('DTT_EVT_end');
849
-        $length_in_units = $end - $start;
850
-        switch ($units) {
851
-            // NOTE: We purposefully don't use "break;" in order to chain the divisions
852
-            /** @noinspection PhpMissingBreakStatementInspection */
853
-            // phpcs:disable PSR2.ControlStructures.SwitchDeclaration.TerminatingComment
854
-            case 'days':
855
-                $length_in_units /= 24;
856
-            /** @noinspection PhpMissingBreakStatementInspection */
857
-            case 'hours':
858
-                // fall through is intentional
859
-                $length_in_units /= 60;
860
-            /** @noinspection PhpMissingBreakStatementInspection */
861
-            case 'minutes':
862
-                // fall through is intentional
863
-                $length_in_units /= 60;
864
-            case 'seconds':
865
-            default:
866
-                $length_in_units = ceil($length_in_units);
867
-        }
868
-        // phpcs:enable
869
-        if ($round_up) {
870
-            $length_in_units = max($length_in_units, 1);
871
-        }
872
-        return $length_in_units;
873
-    }
874
-
875
-
876
-    /**
877
-     *        get end date and time
878
-     *
879
-     * @param string $dt_frmt   - string representation of date format defaults to 'F j, Y'
880
-     * @param string $tm_format - string representation of time format defaults to 'g:i a'
881
-     * @return    mixed                string on success, FALSE on fail
882
-     * @throws ReflectionException
883
-     * @throws InvalidArgumentException
884
-     * @throws InvalidInterfaceException
885
-     * @throws InvalidDataTypeException
886
-     * @throws EE_Error
887
-     */
888
-    public function end_date_and_time($dt_frmt = '', $tm_format = '')
889
-    {
890
-        return $this->_show_datetime('', 'end', $dt_frmt, $tm_format);
891
-    }
892
-
893
-
894
-    /**
895
-     * @param string $dt_frmt
896
-     * @param string $tm_format
897
-     * @throws ReflectionException
898
-     * @throws InvalidArgumentException
899
-     * @throws InvalidInterfaceException
900
-     * @throws InvalidDataTypeException
901
-     * @throws EE_Error
902
-     */
903
-    public function e_end_date_and_time($dt_frmt = '', $tm_format = '')
904
-    {
905
-        $this->_show_datetime('', 'end', $dt_frmt, $tm_format, true);
906
-    }
907
-
908
-
909
-    /**
910
-     *        get start timestamp
911
-     *
912
-     * @return        int
913
-     * @throws ReflectionException
914
-     * @throws InvalidArgumentException
915
-     * @throws InvalidInterfaceException
916
-     * @throws InvalidDataTypeException
917
-     * @throws EE_Error
918
-     */
919
-    public function start()
920
-    {
921
-        return $this->get_raw('DTT_EVT_start');
922
-    }
923
-
924
-
925
-    /**
926
-     *        get end timestamp
927
-     *
928
-     * @return        int
929
-     * @throws ReflectionException
930
-     * @throws InvalidArgumentException
931
-     * @throws InvalidInterfaceException
932
-     * @throws InvalidDataTypeException
933
-     * @throws EE_Error
934
-     */
935
-    public function end()
936
-    {
937
-        return $this->get_raw('DTT_EVT_end');
938
-    }
939
-
940
-
941
-    /**
942
-     *    get the registration limit for this datetime slot
943
-     *
944
-     * @return        mixed        int on success, FALSE on fail
945
-     * @throws ReflectionException
946
-     * @throws InvalidArgumentException
947
-     * @throws InvalidInterfaceException
948
-     * @throws InvalidDataTypeException
949
-     * @throws EE_Error
950
-     */
951
-    public function reg_limit()
952
-    {
953
-        return $this->get_raw('DTT_reg_limit');
954
-    }
955
-
956
-
957
-    /**
958
-     *    have the tickets sold for this datetime, met or exceed the registration limit ?
959
-     *
960
-     * @return        boolean
961
-     * @throws ReflectionException
962
-     * @throws InvalidArgumentException
963
-     * @throws InvalidInterfaceException
964
-     * @throws InvalidDataTypeException
965
-     * @throws EE_Error
966
-     */
967
-    public function sold_out()
968
-    {
969
-        return $this->reg_limit() > 0 && $this->sold() >= $this->reg_limit();
970
-    }
971
-
972
-
973
-    /**
974
-     * return the total number of spaces remaining at this venue.
975
-     * This only takes the venue's capacity into account, NOT the tickets available for sale
976
-     *
977
-     * @param bool $consider_tickets Whether to consider tickets remaining when determining if there are any spaces left
978
-     *                               Because if all tickets attached to this datetime have no spaces left,
979
-     *                               then this datetime IS effectively sold out.
980
-     *                               However, there are cases where we just want to know the spaces
981
-     *                               remaining for this particular datetime, hence the flag.
982
-     * @return int
983
-     * @throws ReflectionException
984
-     * @throws InvalidArgumentException
985
-     * @throws InvalidInterfaceException
986
-     * @throws InvalidDataTypeException
987
-     * @throws EE_Error
988
-     */
989
-    public function spaces_remaining($consider_tickets = false)
990
-    {
991
-        // tickets remaining available for purchase
992
-        // no need for special checks for infinite, because if DTT_reg_limit == EE_INF, then EE_INF - x = EE_INF
993
-        $dtt_remaining = $this->reg_limit() - $this->sold_and_reserved();
994
-        if (! $consider_tickets) {
995
-            return $dtt_remaining;
996
-        }
997
-        $tickets_remaining = $this->tickets_remaining();
998
-        return min($dtt_remaining, $tickets_remaining);
999
-    }
1000
-
1001
-
1002
-    /**
1003
-     * Counts the total tickets available
1004
-     * (from all the different types of tickets which are available for this datetime).
1005
-     *
1006
-     * @param array $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
1007
-     * @return int
1008
-     * @throws ReflectionException
1009
-     * @throws InvalidArgumentException
1010
-     * @throws InvalidInterfaceException
1011
-     * @throws InvalidDataTypeException
1012
-     * @throws EE_Error
1013
-     */
1014
-    public function tickets_remaining($query_params = array())
1015
-    {
1016
-        $sum = 0;
1017
-        $tickets = $this->tickets($query_params);
1018
-        if (! empty($tickets)) {
1019
-            foreach ($tickets as $ticket) {
1020
-                if ($ticket instanceof EE_Ticket) {
1021
-                    // get the actual amount of tickets that can be sold
1022
-                    $qty = $ticket->qty('saleable');
1023
-                    if ($qty === EE_INF) {
1024
-                        return EE_INF;
1025
-                    }
1026
-                    // no negative ticket quantities plz
1027
-                    if ($qty > 0) {
1028
-                        $sum += $qty;
1029
-                    }
1030
-                }
1031
-            }
1032
-        }
1033
-        return $sum;
1034
-    }
1035
-
1036
-
1037
-    /**
1038
-     * Gets the count of all the tickets available at this datetime (not ticket types)
1039
-     * before any were sold
1040
-     *
1041
-     * @param array $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
1042
-     * @return int
1043
-     * @throws ReflectionException
1044
-     * @throws InvalidArgumentException
1045
-     * @throws InvalidInterfaceException
1046
-     * @throws InvalidDataTypeException
1047
-     * @throws EE_Error
1048
-     */
1049
-    public function sum_tickets_initially_available($query_params = array())
1050
-    {
1051
-        return $this->sum_related('Ticket', $query_params, 'TKT_qty');
1052
-    }
1053
-
1054
-
1055
-    /**
1056
-     * Returns the lesser-of-the two: spaces remaining at this datetime, or
1057
-     * the total tickets remaining (a sum of the tickets remaining for each ticket type
1058
-     * that is available for this datetime).
1059
-     *
1060
-     * @return int
1061
-     * @throws ReflectionException
1062
-     * @throws InvalidArgumentException
1063
-     * @throws InvalidInterfaceException
1064
-     * @throws InvalidDataTypeException
1065
-     * @throws EE_Error
1066
-     */
1067
-    public function total_tickets_available_at_this_datetime()
1068
-    {
1069
-        return $this->spaces_remaining(true);
1070
-    }
1071
-
1072
-
1073
-    /**
1074
-     * This simply compares the internal dtt for the given string with NOW
1075
-     * and determines if the date is upcoming or not.
1076
-     *
1077
-     * @access public
1078
-     * @return boolean
1079
-     * @throws ReflectionException
1080
-     * @throws InvalidArgumentException
1081
-     * @throws InvalidInterfaceException
1082
-     * @throws InvalidDataTypeException
1083
-     * @throws EE_Error
1084
-     */
1085
-    public function is_upcoming()
1086
-    {
1087
-        return ($this->get_raw('DTT_EVT_start') > time());
1088
-    }
1089
-
1090
-
1091
-    /**
1092
-     * This simply compares the internal datetime for the given string with NOW
1093
-     * and returns if the date is active (i.e. start and end time)
1094
-     *
1095
-     * @return boolean
1096
-     * @throws ReflectionException
1097
-     * @throws InvalidArgumentException
1098
-     * @throws InvalidInterfaceException
1099
-     * @throws InvalidDataTypeException
1100
-     * @throws EE_Error
1101
-     */
1102
-    public function is_active()
1103
-    {
1104
-        return ($this->get_raw('DTT_EVT_start') < time() && $this->get_raw('DTT_EVT_end') > time());
1105
-    }
1106
-
1107
-
1108
-    /**
1109
-     * This simply compares the internal dtt for the given string with NOW
1110
-     * and determines if the date is expired or not.
1111
-     *
1112
-     * @return boolean
1113
-     * @throws ReflectionException
1114
-     * @throws InvalidArgumentException
1115
-     * @throws InvalidInterfaceException
1116
-     * @throws InvalidDataTypeException
1117
-     * @throws EE_Error
1118
-     */
1119
-    public function is_expired()
1120
-    {
1121
-        return ($this->get_raw('DTT_EVT_end') < time());
1122
-    }
1123
-
1124
-
1125
-    /**
1126
-     * This returns the active status for whether an event is active, upcoming, or expired
1127
-     *
1128
-     * @return int return value will be one of the EE_Datetime status constants.
1129
-     * @throws ReflectionException
1130
-     * @throws InvalidArgumentException
1131
-     * @throws InvalidInterfaceException
1132
-     * @throws InvalidDataTypeException
1133
-     * @throws EE_Error
1134
-     */
1135
-    public function get_active_status()
1136
-    {
1137
-        $total_tickets_for_this_dtt = $this->total_tickets_available_at_this_datetime();
1138
-        if ($total_tickets_for_this_dtt !== false && $total_tickets_for_this_dtt < 1) {
1139
-            return EE_Datetime::sold_out;
1140
-        }
1141
-        if ($this->is_expired()) {
1142
-            return EE_Datetime::expired;
1143
-        }
1144
-        if ($this->is_upcoming()) {
1145
-            return EE_Datetime::upcoming;
1146
-        }
1147
-        if ($this->is_active()) {
1148
-            return EE_Datetime::active;
1149
-        }
1150
-        return null;
1151
-    }
1152
-
1153
-
1154
-    /**
1155
-     * This returns a nice display name for the datetime that is contingent on the span between the dates and times.
1156
-     *
1157
-     * @param  boolean $use_dtt_name if TRUE then we'll use DTT->name() if its not empty.
1158
-     * @return string
1159
-     * @throws ReflectionException
1160
-     * @throws InvalidArgumentException
1161
-     * @throws InvalidInterfaceException
1162
-     * @throws InvalidDataTypeException
1163
-     * @throws EE_Error
1164
-     */
1165
-    public function get_dtt_display_name($use_dtt_name = false)
1166
-    {
1167
-        if ($use_dtt_name) {
1168
-            $dtt_name = $this->name();
1169
-            if (! empty($dtt_name)) {
1170
-                return $dtt_name;
1171
-            }
1172
-        }
1173
-        // first condition is to see if the months are different
1174
-        if (date('m', $this->get_raw('DTT_EVT_start')) !== date('m', $this->get_raw('DTT_EVT_end'))
1175
-        ) {
1176
-            $display_date = $this->start_date('M j\, Y g:i a') . ' - ' . $this->end_date('M j\, Y g:i a');
1177
-            // next condition is if its the same month but different day
1178
-        } else {
1179
-            if (date('m', $this->get_raw('DTT_EVT_start')) === date('m', $this->get_raw('DTT_EVT_end'))
1180
-                && date('d', $this->get_raw('DTT_EVT_start')) !== date('d', $this->get_raw('DTT_EVT_end'))
1181
-            ) {
1182
-                $display_date = $this->start_date('M j\, g:i a') . ' - ' . $this->end_date('M j\, g:i a Y');
1183
-            } else {
1184
-                $display_date = $this->start_date('F j\, Y')
1185
-                                . ' @ '
1186
-                                . $this->start_date('g:i a')
1187
-                                . ' - '
1188
-                                . $this->end_date('g:i a');
1189
-            }
1190
-        }
1191
-        return $display_date;
1192
-    }
1193
-
1194
-
1195
-    /**
1196
-     * Gets all the tickets for this datetime
1197
-     *
1198
-     * @param array $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
1199
-     * @return EE_Base_Class[]|EE_Ticket[]
1200
-     * @throws ReflectionException
1201
-     * @throws InvalidArgumentException
1202
-     * @throws InvalidInterfaceException
1203
-     * @throws InvalidDataTypeException
1204
-     * @throws EE_Error
1205
-     */
1206
-    public function tickets($query_params = array())
1207
-    {
1208
-        return $this->get_many_related('Ticket', $query_params);
1209
-    }
1210
-
1211
-
1212
-    /**
1213
-     * Gets all the ticket types currently available for purchase
1214
-     *
1215
-     * @param array $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
1216
-     * @return EE_Ticket[]
1217
-     * @throws ReflectionException
1218
-     * @throws InvalidArgumentException
1219
-     * @throws InvalidInterfaceException
1220
-     * @throws InvalidDataTypeException
1221
-     * @throws EE_Error
1222
-     */
1223
-    public function ticket_types_available_for_purchase($query_params = array())
1224
-    {
1225
-        // first check if datetime is valid
1226
-        if ($this->sold_out() || ! ($this->is_upcoming() || $this->is_active())) {
1227
-            return array();
1228
-        }
1229
-        if (empty($query_params)) {
1230
-            $query_params = array(
1231
-                array(
1232
-                    'TKT_start_date' => array('<=', EEM_Ticket::instance()->current_time_for_query('TKT_start_date')),
1233
-                    'TKT_end_date'   => array('>=', EEM_Ticket::instance()->current_time_for_query('TKT_end_date')),
1234
-                    'TKT_deleted'    => false,
1235
-                ),
1236
-            );
1237
-        }
1238
-        return $this->tickets($query_params);
1239
-    }
1240
-
1241
-
1242
-    /**
1243
-     * @return EE_Base_Class|EE_Event
1244
-     * @throws ReflectionException
1245
-     * @throws InvalidArgumentException
1246
-     * @throws InvalidInterfaceException
1247
-     * @throws InvalidDataTypeException
1248
-     * @throws EE_Error
1249
-     */
1250
-    public function event()
1251
-    {
1252
-        return $this->get_first_related('Event');
1253
-    }
1254
-
1255
-
1256
-    /**
1257
-     * Updates the DTT_sold attribute (and saves) based on the number of registrations for this datetime
1258
-     * (via the tickets). into account
1259
-     *
1260
-     * @return int
1261
-     * @throws ReflectionException
1262
-     * @throws InvalidArgumentException
1263
-     * @throws InvalidInterfaceException
1264
-     * @throws InvalidDataTypeException
1265
-     * @throws EE_Error
1266
-     */
1267
-    public function update_sold()
1268
-    {
1269
-        $count_regs_for_this_datetime = EEM_Registration::instance()->count(
1270
-            array(
1271
-                array(
1272
-                    'STS_ID'                 => EEM_Registration::status_id_approved,
1273
-                    'REG_deleted'            => 0,
1274
-                    'Ticket.Datetime.DTT_ID' => $this->ID(),
1275
-                ),
1276
-            )
1277
-        );
1278
-        $sold = $this->sold();
1279
-        if ($count_regs_for_this_datetime > $sold) {
1280
-            $this->increase_sold($count_regs_for_this_datetime - $sold);
1281
-            $this->save();
1282
-        } elseif ($count_regs_for_this_datetime < $sold) {
1283
-            $this->decrease_sold($count_regs_for_this_datetime - $sold);
1284
-            $this->save();
1285
-        }
1286
-        return $count_regs_for_this_datetime;
1287
-    }
16
+	/**
17
+	 * constant used by get_active_status, indicates datetime has no more available spaces
18
+	 */
19
+	const sold_out = 'DTS';
20
+
21
+	/**
22
+	 * constant used by get_active_status, indicating datetime is still active (even is not over, can be registered-for)
23
+	 */
24
+	const active = 'DTA';
25
+
26
+	/**
27
+	 * constant used by get_active_status, indicating the datetime cannot be used for registrations yet, but has not
28
+	 * expired
29
+	 */
30
+	const upcoming = 'DTU';
31
+
32
+	/**
33
+	 * Datetime is postponed
34
+	 */
35
+	const postponed = 'DTP';
36
+
37
+	/**
38
+	 * Datetime is cancelled
39
+	 */
40
+	const cancelled = 'DTC';
41
+
42
+	/**
43
+	 * constant used by get_active_status, indicates datetime has expired (event is over)
44
+	 */
45
+	const expired = 'DTE';
46
+
47
+	/**
48
+	 * constant used in various places indicating that an event is INACTIVE (not yet ready to be published)
49
+	 */
50
+	const inactive = 'DTI';
51
+
52
+
53
+	/**
54
+	 * @param array  $props_n_values    incoming values
55
+	 * @param string $timezone          incoming timezone (if not set the timezone set for the website will be used.)
56
+	 * @param array  $date_formats      incoming date_formats in an array where the first value is the date_format
57
+	 *                                  and the second value is the time format
58
+	 * @return EE_Datetime
59
+	 * @throws ReflectionException
60
+	 * @throws InvalidArgumentException
61
+	 * @throws InvalidInterfaceException
62
+	 * @throws InvalidDataTypeException
63
+	 * @throws EE_Error
64
+	 */
65
+	public static function new_instance($props_n_values = array(), $timezone = null, $date_formats = array())
66
+	{
67
+		$has_object = parent::_check_for_object(
68
+			$props_n_values,
69
+			__CLASS__,
70
+			$timezone,
71
+			$date_formats
72
+		);
73
+		return $has_object
74
+			? $has_object
75
+			: new self($props_n_values, false, $timezone, $date_formats);
76
+	}
77
+
78
+
79
+	/**
80
+	 * @param array  $props_n_values  incoming values from the database
81
+	 * @param string $timezone        incoming timezone as set by the model.  If not set the timezone for
82
+	 *                                the website will be used.
83
+	 * @return EE_Datetime
84
+	 * @throws ReflectionException
85
+	 * @throws InvalidArgumentException
86
+	 * @throws InvalidInterfaceException
87
+	 * @throws InvalidDataTypeException
88
+	 * @throws EE_Error
89
+	 */
90
+	public static function new_instance_from_db($props_n_values = array(), $timezone = null)
91
+	{
92
+		return new self($props_n_values, true, $timezone);
93
+	}
94
+
95
+
96
+	/**
97
+	 * @param $name
98
+	 * @throws ReflectionException
99
+	 * @throws InvalidArgumentException
100
+	 * @throws InvalidInterfaceException
101
+	 * @throws InvalidDataTypeException
102
+	 * @throws EE_Error
103
+	 */
104
+	public function set_name($name)
105
+	{
106
+		$this->set('DTT_name', $name);
107
+	}
108
+
109
+
110
+	/**
111
+	 * @param $description
112
+	 * @throws ReflectionException
113
+	 * @throws InvalidArgumentException
114
+	 * @throws InvalidInterfaceException
115
+	 * @throws InvalidDataTypeException
116
+	 * @throws EE_Error
117
+	 */
118
+	public function set_description($description)
119
+	{
120
+		$this->set('DTT_description', $description);
121
+	}
122
+
123
+
124
+	/**
125
+	 * Set event start date
126
+	 * set the start date for an event
127
+	 *
128
+	 * @param string $date a string representation of the event's date ex:  Dec. 25, 2025 or 12-25-2025
129
+	 * @throws ReflectionException
130
+	 * @throws InvalidArgumentException
131
+	 * @throws InvalidInterfaceException
132
+	 * @throws InvalidDataTypeException
133
+	 * @throws EE_Error
134
+	 */
135
+	public function set_start_date($date)
136
+	{
137
+		$this->_set_date_for($date, 'DTT_EVT_start');
138
+	}
139
+
140
+
141
+	/**
142
+	 * Set event start time
143
+	 * set the start time for an event
144
+	 *
145
+	 * @param string $time a string representation of the event time ex:  9am  or  7:30 PM
146
+	 * @throws ReflectionException
147
+	 * @throws InvalidArgumentException
148
+	 * @throws InvalidInterfaceException
149
+	 * @throws InvalidDataTypeException
150
+	 * @throws EE_Error
151
+	 */
152
+	public function set_start_time($time)
153
+	{
154
+		$this->_set_time_for($time, 'DTT_EVT_start');
155
+	}
156
+
157
+
158
+	/**
159
+	 * Set event end date
160
+	 * set the end date for an event
161
+	 *
162
+	 * @param string $date a string representation of the event's date ex:  Dec. 25, 2025 or 12-25-2025
163
+	 * @throws ReflectionException
164
+	 * @throws InvalidArgumentException
165
+	 * @throws InvalidInterfaceException
166
+	 * @throws InvalidDataTypeException
167
+	 * @throws EE_Error
168
+	 */
169
+	public function set_end_date($date)
170
+	{
171
+		$this->_set_date_for($date, 'DTT_EVT_end');
172
+	}
173
+
174
+
175
+	/**
176
+	 * Set event end time
177
+	 * set the end time for an event
178
+	 *
179
+	 * @param string $time a string representation of the event time ex:  9am  or  7:30 PM
180
+	 * @throws ReflectionException
181
+	 * @throws InvalidArgumentException
182
+	 * @throws InvalidInterfaceException
183
+	 * @throws InvalidDataTypeException
184
+	 * @throws EE_Error
185
+	 */
186
+	public function set_end_time($time)
187
+	{
188
+		$this->_set_time_for($time, 'DTT_EVT_end');
189
+	}
190
+
191
+
192
+	/**
193
+	 * Set registration limit
194
+	 * set the maximum number of attendees that can be registered for this datetime slot
195
+	 *
196
+	 * @param int $reg_limit
197
+	 * @throws ReflectionException
198
+	 * @throws InvalidArgumentException
199
+	 * @throws InvalidInterfaceException
200
+	 * @throws InvalidDataTypeException
201
+	 * @throws EE_Error
202
+	 */
203
+	public function set_reg_limit($reg_limit)
204
+	{
205
+		$this->set('DTT_reg_limit', $reg_limit);
206
+	}
207
+
208
+
209
+	/**
210
+	 * get the number of tickets sold for this datetime slot
211
+	 *
212
+	 * @return mixed int on success, FALSE on fail
213
+	 * @throws ReflectionException
214
+	 * @throws InvalidArgumentException
215
+	 * @throws InvalidInterfaceException
216
+	 * @throws InvalidDataTypeException
217
+	 * @throws EE_Error
218
+	 */
219
+	public function sold()
220
+	{
221
+		return $this->get_raw('DTT_sold');
222
+	}
223
+
224
+
225
+	/**
226
+	 * @param int $sold
227
+	 * @throws ReflectionException
228
+	 * @throws InvalidArgumentException
229
+	 * @throws InvalidInterfaceException
230
+	 * @throws InvalidDataTypeException
231
+	 * @throws EE_Error
232
+	 */
233
+	public function set_sold($sold)
234
+	{
235
+		// sold can not go below zero
236
+		$sold = max(0, $sold);
237
+		$this->set('DTT_sold', $sold);
238
+	}
239
+
240
+
241
+	/**
242
+	 * increments sold by amount passed by $qty
243
+	 *
244
+	 * @param int $qty
245
+	 * @throws ReflectionException
246
+	 * @throws InvalidArgumentException
247
+	 * @throws InvalidInterfaceException
248
+	 * @throws InvalidDataTypeException
249
+	 * @throws EE_Error
250
+	 */
251
+	public function increase_sold($qty = 1)
252
+	{
253
+		$sold = $this->sold() + $qty;
254
+		// remove ticket reservation
255
+		$this->decrease_reserved($qty);
256
+		$this->set_sold($sold);
257
+		do_action(
258
+			'AHEE__EE_Datetime__increase_sold',
259
+			$this,
260
+			$qty,
261
+			$sold
262
+		);
263
+	}
264
+
265
+
266
+	/**
267
+	 * decrements (subtracts) sold amount passed by $qty
268
+	 *
269
+	 * @param int $qty
270
+	 * @throws ReflectionException
271
+	 * @throws InvalidArgumentException
272
+	 * @throws InvalidInterfaceException
273
+	 * @throws InvalidDataTypeException
274
+	 * @throws EE_Error
275
+	 */
276
+	public function decrease_sold($qty = 1)
277
+	{
278
+		$sold = $this->sold() - $qty;
279
+		$this->set_sold($sold);
280
+		do_action(
281
+			'AHEE__EE_Datetime__decrease_sold',
282
+			$this,
283
+			$qty,
284
+			$sold
285
+		);
286
+	}
287
+
288
+
289
+	/**
290
+	 * Gets qty of reserved tickets for this datetime
291
+	 *
292
+	 * @return int
293
+	 * @throws ReflectionException
294
+	 * @throws InvalidArgumentException
295
+	 * @throws InvalidInterfaceException
296
+	 * @throws InvalidDataTypeException
297
+	 * @throws EE_Error
298
+	 */
299
+	public function reserved()
300
+	{
301
+		return $this->get_raw('DTT_reserved');
302
+	}
303
+
304
+
305
+	/**
306
+	 * Sets qty of reserved tickets for this datetime
307
+	 *
308
+	 * @param int $reserved
309
+	 * @throws ReflectionException
310
+	 * @throws InvalidArgumentException
311
+	 * @throws InvalidInterfaceException
312
+	 * @throws InvalidDataTypeException
313
+	 * @throws EE_Error
314
+	 */
315
+	public function set_reserved($reserved)
316
+	{
317
+		// reserved can not go below zero
318
+		$reserved = max(0, (int) $reserved);
319
+		$this->set('DTT_reserved', $reserved);
320
+	}
321
+
322
+
323
+	/**
324
+	 * increments reserved by amount passed by $qty
325
+	 *
326
+	 * @param int $qty
327
+	 * @return void
328
+	 * @throws ReflectionException
329
+	 * @throws InvalidArgumentException
330
+	 * @throws InvalidInterfaceException
331
+	 * @throws InvalidDataTypeException
332
+	 * @throws EE_Error
333
+	 */
334
+	public function increase_reserved($qty = 1)
335
+	{
336
+		$reserved = $this->reserved() + absint($qty);
337
+		do_action(
338
+			'AHEE__EE_Datetime__increase_reserved',
339
+			$this,
340
+			$qty,
341
+			$reserved
342
+		);
343
+		$this->set_reserved($reserved);
344
+	}
345
+
346
+
347
+	/**
348
+	 * decrements (subtracts) reserved by amount passed by $qty
349
+	 *
350
+	 * @param int $qty
351
+	 * @return void
352
+	 * @throws ReflectionException
353
+	 * @throws InvalidArgumentException
354
+	 * @throws InvalidInterfaceException
355
+	 * @throws InvalidDataTypeException
356
+	 * @throws EE_Error
357
+	 */
358
+	public function decrease_reserved($qty = 1)
359
+	{
360
+		$reserved = $this->reserved() - absint($qty);
361
+		do_action(
362
+			'AHEE__EE_Datetime__decrease_reserved',
363
+			$this,
364
+			$qty,
365
+			$reserved
366
+		);
367
+		$this->set_reserved($reserved);
368
+	}
369
+
370
+
371
+	/**
372
+	 * total sold and reserved tickets
373
+	 *
374
+	 * @return int
375
+	 * @throws ReflectionException
376
+	 * @throws InvalidArgumentException
377
+	 * @throws InvalidInterfaceException
378
+	 * @throws InvalidDataTypeException
379
+	 * @throws EE_Error
380
+	 */
381
+	public function sold_and_reserved()
382
+	{
383
+		return $this->sold() + $this->reserved();
384
+	}
385
+
386
+
387
+	/**
388
+	 * returns the datetime name
389
+	 *
390
+	 * @return string
391
+	 * @throws ReflectionException
392
+	 * @throws InvalidArgumentException
393
+	 * @throws InvalidInterfaceException
394
+	 * @throws InvalidDataTypeException
395
+	 * @throws EE_Error
396
+	 */
397
+	public function name()
398
+	{
399
+		return $this->get('DTT_name');
400
+	}
401
+
402
+
403
+	/**
404
+	 * returns the datetime description
405
+	 *
406
+	 * @return string
407
+	 * @throws ReflectionException
408
+	 * @throws InvalidArgumentException
409
+	 * @throws InvalidInterfaceException
410
+	 * @throws InvalidDataTypeException
411
+	 * @throws EE_Error
412
+	 */
413
+	public function description()
414
+	{
415
+		return $this->get('DTT_description');
416
+	}
417
+
418
+
419
+	/**
420
+	 * This helper simply returns whether the event_datetime for the current datetime is a primary datetime
421
+	 *
422
+	 * @return boolean  TRUE if is primary, FALSE if not.
423
+	 * @throws ReflectionException
424
+	 * @throws InvalidArgumentException
425
+	 * @throws InvalidInterfaceException
426
+	 * @throws InvalidDataTypeException
427
+	 * @throws EE_Error
428
+	 */
429
+	public function is_primary()
430
+	{
431
+		return $this->get('DTT_is_primary');
432
+	}
433
+
434
+
435
+	/**
436
+	 * This helper simply returns the order for the datetime
437
+	 *
438
+	 * @return int  The order of the datetime for this event.
439
+	 * @throws ReflectionException
440
+	 * @throws InvalidArgumentException
441
+	 * @throws InvalidInterfaceException
442
+	 * @throws InvalidDataTypeException
443
+	 * @throws EE_Error
444
+	 */
445
+	public function order()
446
+	{
447
+		return $this->get('DTT_order');
448
+	}
449
+
450
+
451
+	/**
452
+	 * This helper simply returns the parent id for the datetime
453
+	 *
454
+	 * @return int
455
+	 * @throws ReflectionException
456
+	 * @throws InvalidArgumentException
457
+	 * @throws InvalidInterfaceException
458
+	 * @throws InvalidDataTypeException
459
+	 * @throws EE_Error
460
+	 */
461
+	public function parent()
462
+	{
463
+		return $this->get('DTT_parent');
464
+	}
465
+
466
+
467
+	/**
468
+	 * show date and/or time
469
+	 *
470
+	 * @param string $date_or_time    whether to display a date or time or both
471
+	 * @param string $start_or_end    whether to display start or end datetimes
472
+	 * @param string $dt_frmt
473
+	 * @param string $tm_frmt
474
+	 * @param bool   $echo            whether we echo or return (note echoing uses "pretty" formats,
475
+	 *                                otherwise we use the standard formats)
476
+	 * @return string|bool  string on success, FALSE on fail
477
+	 * @throws ReflectionException
478
+	 * @throws InvalidArgumentException
479
+	 * @throws InvalidInterfaceException
480
+	 * @throws InvalidDataTypeException
481
+	 * @throws EE_Error
482
+	 */
483
+	private function _show_datetime(
484
+		$date_or_time = null,
485
+		$start_or_end = 'start',
486
+		$dt_frmt = '',
487
+		$tm_frmt = '',
488
+		$echo = false
489
+	) {
490
+		$field_name = "DTT_EVT_{$start_or_end}";
491
+		$dtt = $this->_get_datetime(
492
+			$field_name,
493
+			$dt_frmt,
494
+			$tm_frmt,
495
+			$date_or_time,
496
+			$echo
497
+		);
498
+		if (! $echo) {
499
+			return $dtt;
500
+		}
501
+		return '';
502
+	}
503
+
504
+
505
+	/**
506
+	 * get event start date.  Provide either the date format, or NULL to re-use the
507
+	 * last-used format, or '' to use the default date format
508
+	 *
509
+	 * @param string $dt_frmt string representation of date format defaults to 'F j, Y'
510
+	 * @return mixed            string on success, FALSE on fail
511
+	 * @throws ReflectionException
512
+	 * @throws InvalidArgumentException
513
+	 * @throws InvalidInterfaceException
514
+	 * @throws InvalidDataTypeException
515
+	 * @throws EE_Error
516
+	 */
517
+	public function start_date($dt_frmt = '')
518
+	{
519
+		return $this->_show_datetime('D', 'start', $dt_frmt);
520
+	}
521
+
522
+
523
+	/**
524
+	 * Echoes start_date()
525
+	 *
526
+	 * @param string $dt_frmt
527
+	 * @throws ReflectionException
528
+	 * @throws InvalidArgumentException
529
+	 * @throws InvalidInterfaceException
530
+	 * @throws InvalidDataTypeException
531
+	 * @throws EE_Error
532
+	 */
533
+	public function e_start_date($dt_frmt = '')
534
+	{
535
+		$this->_show_datetime('D', 'start', $dt_frmt, null, true);
536
+	}
537
+
538
+
539
+	/**
540
+	 * get end date. Provide either the date format, or NULL to re-use the
541
+	 * last-used format, or '' to use the default date format
542
+	 *
543
+	 * @param string $dt_frmt string representation of date format defaults to 'F j, Y'
544
+	 * @return mixed            string on success, FALSE on fail
545
+	 * @throws ReflectionException
546
+	 * @throws InvalidArgumentException
547
+	 * @throws InvalidInterfaceException
548
+	 * @throws InvalidDataTypeException
549
+	 * @throws EE_Error
550
+	 */
551
+	public function end_date($dt_frmt = '')
552
+	{
553
+		return $this->_show_datetime('D', 'end', $dt_frmt);
554
+	}
555
+
556
+
557
+	/**
558
+	 * Echoes the end date. See end_date()
559
+	 *
560
+	 * @param string $dt_frmt
561
+	 * @throws ReflectionException
562
+	 * @throws InvalidArgumentException
563
+	 * @throws InvalidInterfaceException
564
+	 * @throws InvalidDataTypeException
565
+	 * @throws EE_Error
566
+	 */
567
+	public function e_end_date($dt_frmt = '')
568
+	{
569
+		$this->_show_datetime('D', 'end', $dt_frmt, null, true);
570
+	}
571
+
572
+
573
+	/**
574
+	 * get date_range - meaning the start AND end date
575
+	 *
576
+	 * @access public
577
+	 * @param string $dt_frmt     string representation of date format defaults to WP settings
578
+	 * @param string $conjunction conjunction junction what's your function ?
579
+	 *                            this string joins the start date with the end date ie: Jan 01 "to" Dec 31
580
+	 * @return mixed              string on success, FALSE on fail
581
+	 * @throws ReflectionException
582
+	 * @throws InvalidArgumentException
583
+	 * @throws InvalidInterfaceException
584
+	 * @throws InvalidDataTypeException
585
+	 * @throws EE_Error
586
+	 */
587
+	public function date_range($dt_frmt = '', $conjunction = ' - ')
588
+	{
589
+		$dt_frmt = ! empty($dt_frmt) ? $dt_frmt : $this->_dt_frmt;
590
+		$start = str_replace(
591
+			' ',
592
+			'&nbsp;',
593
+			$this->get_i18n_datetime('DTT_EVT_start', $dt_frmt)
594
+		);
595
+		$end = str_replace(
596
+			' ',
597
+			'&nbsp;',
598
+			$this->get_i18n_datetime('DTT_EVT_end', $dt_frmt)
599
+		);
600
+		return $start !== $end ? $start . $conjunction . $end : $start;
601
+	}
602
+
603
+
604
+	/**
605
+	 * @param string $dt_frmt
606
+	 * @param string $conjunction
607
+	 * @throws ReflectionException
608
+	 * @throws InvalidArgumentException
609
+	 * @throws InvalidInterfaceException
610
+	 * @throws InvalidDataTypeException
611
+	 * @throws EE_Error
612
+	 */
613
+	public function e_date_range($dt_frmt = '', $conjunction = ' - ')
614
+	{
615
+		echo $this->date_range($dt_frmt, $conjunction);
616
+	}
617
+
618
+
619
+	/**
620
+	 * get start time
621
+	 *
622
+	 * @param string $tm_format - string representation of time format defaults to 'g:i a'
623
+	 * @return mixed        string on success, FALSE on fail
624
+	 * @throws ReflectionException
625
+	 * @throws InvalidArgumentException
626
+	 * @throws InvalidInterfaceException
627
+	 * @throws InvalidDataTypeException
628
+	 * @throws EE_Error
629
+	 */
630
+	public function start_time($tm_format = '')
631
+	{
632
+		return $this->_show_datetime('T', 'start', null, $tm_format);
633
+	}
634
+
635
+
636
+	/**
637
+	 * @param string $tm_format
638
+	 * @throws ReflectionException
639
+	 * @throws InvalidArgumentException
640
+	 * @throws InvalidInterfaceException
641
+	 * @throws InvalidDataTypeException
642
+	 * @throws EE_Error
643
+	 */
644
+	public function e_start_time($tm_format = '')
645
+	{
646
+		$this->_show_datetime('T', 'start', null, $tm_format, true);
647
+	}
648
+
649
+
650
+	/**
651
+	 * get end time
652
+	 *
653
+	 * @param string $tm_format string representation of time format defaults to 'g:i a'
654
+	 * @return mixed                string on success, FALSE on fail
655
+	 * @throws ReflectionException
656
+	 * @throws InvalidArgumentException
657
+	 * @throws InvalidInterfaceException
658
+	 * @throws InvalidDataTypeException
659
+	 * @throws EE_Error
660
+	 */
661
+	public function end_time($tm_format = '')
662
+	{
663
+		return $this->_show_datetime('T', 'end', null, $tm_format);
664
+	}
665
+
666
+
667
+	/**
668
+	 * @param string $tm_format
669
+	 * @throws ReflectionException
670
+	 * @throws InvalidArgumentException
671
+	 * @throws InvalidInterfaceException
672
+	 * @throws InvalidDataTypeException
673
+	 * @throws EE_Error
674
+	 */
675
+	public function e_end_time($tm_format = '')
676
+	{
677
+		$this->_show_datetime('T', 'end', null, $tm_format, true);
678
+	}
679
+
680
+
681
+	/**
682
+	 * get time_range
683
+	 *
684
+	 * @access public
685
+	 * @param string $tm_format   string representation of time format defaults to 'g:i a'
686
+	 * @param string $conjunction conjunction junction what's your function ?
687
+	 *                            this string joins the start date with the end date ie: Jan 01 "to" Dec 31
688
+	 * @return mixed              string on success, FALSE on fail
689
+	 * @throws ReflectionException
690
+	 * @throws InvalidArgumentException
691
+	 * @throws InvalidInterfaceException
692
+	 * @throws InvalidDataTypeException
693
+	 * @throws EE_Error
694
+	 */
695
+	public function time_range($tm_format = '', $conjunction = ' - ')
696
+	{
697
+		$tm_format = ! empty($tm_format) ? $tm_format : $this->_tm_frmt;
698
+		$start = str_replace(
699
+			' ',
700
+			'&nbsp;',
701
+			$this->get_i18n_datetime('DTT_EVT_start', $tm_format)
702
+		);
703
+		$end = str_replace(
704
+			' ',
705
+			'&nbsp;',
706
+			$this->get_i18n_datetime('DTT_EVT_end', $tm_format)
707
+		);
708
+		return $start !== $end ? $start . $conjunction . $end : $start;
709
+	}
710
+
711
+
712
+	/**
713
+	 * @param string $tm_format
714
+	 * @param string $conjunction
715
+	 * @throws ReflectionException
716
+	 * @throws InvalidArgumentException
717
+	 * @throws InvalidInterfaceException
718
+	 * @throws InvalidDataTypeException
719
+	 * @throws EE_Error
720
+	 */
721
+	public function e_time_range($tm_format = '', $conjunction = ' - ')
722
+	{
723
+		echo $this->time_range($tm_format, $conjunction);
724
+	}
725
+
726
+
727
+	/**
728
+	 * This returns a range representation of the date and times.
729
+	 * Output is dependent on the difference (or similarity) between DTT_EVT_start and DTT_EVT_end.
730
+	 * Also, the return value is localized.
731
+	 *
732
+	 * @param string $dt_format
733
+	 * @param string $tm_format
734
+	 * @param string $conjunction used between two different dates or times.
735
+	 *                            ex: Dec 1{$conjunction}}Dec 6, or 2pm{$conjunction}3pm
736
+	 * @param string $separator   used between the date and time formats.
737
+	 *                            ex: Dec 1, 2016{$separator}2pm
738
+	 * @return string
739
+	 * @throws ReflectionException
740
+	 * @throws InvalidArgumentException
741
+	 * @throws InvalidInterfaceException
742
+	 * @throws InvalidDataTypeException
743
+	 * @throws EE_Error
744
+	 */
745
+	public function date_and_time_range(
746
+		$dt_format = '',
747
+		$tm_format = '',
748
+		$conjunction = ' - ',
749
+		$separator = ' '
750
+	) {
751
+		$dt_format = ! empty($dt_format) ? $dt_format : $this->_dt_frmt;
752
+		$tm_format = ! empty($tm_format) ? $tm_format : $this->_tm_frmt;
753
+		$full_format = $dt_format . $separator . $tm_format;
754
+		// the range output depends on various conditions
755
+		switch (true) {
756
+			// start date timestamp and end date timestamp are the same.
757
+			case ($this->get_raw('DTT_EVT_start') === $this->get_raw('DTT_EVT_end')):
758
+				$output = $this->get_i18n_datetime('DTT_EVT_start', $full_format);
759
+				break;
760
+			// start and end date are the same but times are different
761
+			case ($this->start_date() === $this->end_date()):
762
+				$output = $this->get_i18n_datetime('DTT_EVT_start', $full_format)
763
+						  . $conjunction
764
+						  . $this->get_i18n_datetime('DTT_EVT_end', $tm_format);
765
+				break;
766
+			// all other conditions
767
+			default:
768
+				$output = $this->get_i18n_datetime('DTT_EVT_start', $full_format)
769
+						  . $conjunction
770
+						  . $this->get_i18n_datetime('DTT_EVT_end', $full_format);
771
+				break;
772
+		}
773
+		return $output;
774
+	}
775
+
776
+
777
+	/**
778
+	 * This echos the results of date and time range.
779
+	 *
780
+	 * @see date_and_time_range() for more details on purpose.
781
+	 * @param string $dt_format
782
+	 * @param string $tm_format
783
+	 * @param string $conjunction
784
+	 * @return void
785
+	 * @throws ReflectionException
786
+	 * @throws InvalidArgumentException
787
+	 * @throws InvalidInterfaceException
788
+	 * @throws InvalidDataTypeException
789
+	 * @throws EE_Error
790
+	 */
791
+	public function e_date_and_time_range($dt_format = '', $tm_format = '', $conjunction = ' - ')
792
+	{
793
+		echo $this->date_and_time_range($dt_format, $tm_format, $conjunction);
794
+	}
795
+
796
+
797
+	/**
798
+	 * get start date and start time
799
+	 *
800
+	 * @param    string $dt_format - string representation of date format defaults to 'F j, Y'
801
+	 * @param    string $tm_format - string representation of time format defaults to 'g:i a'
802
+	 * @return    mixed    string on success, FALSE on fail
803
+	 * @throws ReflectionException
804
+	 * @throws InvalidArgumentException
805
+	 * @throws InvalidInterfaceException
806
+	 * @throws InvalidDataTypeException
807
+	 * @throws EE_Error
808
+	 */
809
+	public function start_date_and_time($dt_format = '', $tm_format = '')
810
+	{
811
+		return $this->_show_datetime('', 'start', $dt_format, $tm_format);
812
+	}
813
+
814
+
815
+	/**
816
+	 * @param string $dt_frmt
817
+	 * @param string $tm_format
818
+	 * @throws ReflectionException
819
+	 * @throws InvalidArgumentException
820
+	 * @throws InvalidInterfaceException
821
+	 * @throws InvalidDataTypeException
822
+	 * @throws EE_Error
823
+	 */
824
+	public function e_start_date_and_time($dt_frmt = '', $tm_format = '')
825
+	{
826
+		$this->_show_datetime('', 'start', $dt_frmt, $tm_format, true);
827
+	}
828
+
829
+
830
+	/**
831
+	 * Shows the length of the event (start to end time).
832
+	 * Can be shown in 'seconds','minutes','hours', or 'days'.
833
+	 * By default, rounds up. (So if you use 'days', and then event
834
+	 * only occurs for 1 hour, it will return 1 day).
835
+	 *
836
+	 * @param string $units 'seconds','minutes','hours','days'
837
+	 * @param bool   $round_up
838
+	 * @return float|int|mixed
839
+	 * @throws ReflectionException
840
+	 * @throws InvalidArgumentException
841
+	 * @throws InvalidInterfaceException
842
+	 * @throws InvalidDataTypeException
843
+	 * @throws EE_Error
844
+	 */
845
+	public function length($units = 'seconds', $round_up = false)
846
+	{
847
+		$start = $this->get_raw('DTT_EVT_start');
848
+		$end = $this->get_raw('DTT_EVT_end');
849
+		$length_in_units = $end - $start;
850
+		switch ($units) {
851
+			// NOTE: We purposefully don't use "break;" in order to chain the divisions
852
+			/** @noinspection PhpMissingBreakStatementInspection */
853
+			// phpcs:disable PSR2.ControlStructures.SwitchDeclaration.TerminatingComment
854
+			case 'days':
855
+				$length_in_units /= 24;
856
+			/** @noinspection PhpMissingBreakStatementInspection */
857
+			case 'hours':
858
+				// fall through is intentional
859
+				$length_in_units /= 60;
860
+			/** @noinspection PhpMissingBreakStatementInspection */
861
+			case 'minutes':
862
+				// fall through is intentional
863
+				$length_in_units /= 60;
864
+			case 'seconds':
865
+			default:
866
+				$length_in_units = ceil($length_in_units);
867
+		}
868
+		// phpcs:enable
869
+		if ($round_up) {
870
+			$length_in_units = max($length_in_units, 1);
871
+		}
872
+		return $length_in_units;
873
+	}
874
+
875
+
876
+	/**
877
+	 *        get end date and time
878
+	 *
879
+	 * @param string $dt_frmt   - string representation of date format defaults to 'F j, Y'
880
+	 * @param string $tm_format - string representation of time format defaults to 'g:i a'
881
+	 * @return    mixed                string on success, FALSE on fail
882
+	 * @throws ReflectionException
883
+	 * @throws InvalidArgumentException
884
+	 * @throws InvalidInterfaceException
885
+	 * @throws InvalidDataTypeException
886
+	 * @throws EE_Error
887
+	 */
888
+	public function end_date_and_time($dt_frmt = '', $tm_format = '')
889
+	{
890
+		return $this->_show_datetime('', 'end', $dt_frmt, $tm_format);
891
+	}
892
+
893
+
894
+	/**
895
+	 * @param string $dt_frmt
896
+	 * @param string $tm_format
897
+	 * @throws ReflectionException
898
+	 * @throws InvalidArgumentException
899
+	 * @throws InvalidInterfaceException
900
+	 * @throws InvalidDataTypeException
901
+	 * @throws EE_Error
902
+	 */
903
+	public function e_end_date_and_time($dt_frmt = '', $tm_format = '')
904
+	{
905
+		$this->_show_datetime('', 'end', $dt_frmt, $tm_format, true);
906
+	}
907
+
908
+
909
+	/**
910
+	 *        get start timestamp
911
+	 *
912
+	 * @return        int
913
+	 * @throws ReflectionException
914
+	 * @throws InvalidArgumentException
915
+	 * @throws InvalidInterfaceException
916
+	 * @throws InvalidDataTypeException
917
+	 * @throws EE_Error
918
+	 */
919
+	public function start()
920
+	{
921
+		return $this->get_raw('DTT_EVT_start');
922
+	}
923
+
924
+
925
+	/**
926
+	 *        get end timestamp
927
+	 *
928
+	 * @return        int
929
+	 * @throws ReflectionException
930
+	 * @throws InvalidArgumentException
931
+	 * @throws InvalidInterfaceException
932
+	 * @throws InvalidDataTypeException
933
+	 * @throws EE_Error
934
+	 */
935
+	public function end()
936
+	{
937
+		return $this->get_raw('DTT_EVT_end');
938
+	}
939
+
940
+
941
+	/**
942
+	 *    get the registration limit for this datetime slot
943
+	 *
944
+	 * @return        mixed        int on success, FALSE on fail
945
+	 * @throws ReflectionException
946
+	 * @throws InvalidArgumentException
947
+	 * @throws InvalidInterfaceException
948
+	 * @throws InvalidDataTypeException
949
+	 * @throws EE_Error
950
+	 */
951
+	public function reg_limit()
952
+	{
953
+		return $this->get_raw('DTT_reg_limit');
954
+	}
955
+
956
+
957
+	/**
958
+	 *    have the tickets sold for this datetime, met or exceed the registration limit ?
959
+	 *
960
+	 * @return        boolean
961
+	 * @throws ReflectionException
962
+	 * @throws InvalidArgumentException
963
+	 * @throws InvalidInterfaceException
964
+	 * @throws InvalidDataTypeException
965
+	 * @throws EE_Error
966
+	 */
967
+	public function sold_out()
968
+	{
969
+		return $this->reg_limit() > 0 && $this->sold() >= $this->reg_limit();
970
+	}
971
+
972
+
973
+	/**
974
+	 * return the total number of spaces remaining at this venue.
975
+	 * This only takes the venue's capacity into account, NOT the tickets available for sale
976
+	 *
977
+	 * @param bool $consider_tickets Whether to consider tickets remaining when determining if there are any spaces left
978
+	 *                               Because if all tickets attached to this datetime have no spaces left,
979
+	 *                               then this datetime IS effectively sold out.
980
+	 *                               However, there are cases where we just want to know the spaces
981
+	 *                               remaining for this particular datetime, hence the flag.
982
+	 * @return int
983
+	 * @throws ReflectionException
984
+	 * @throws InvalidArgumentException
985
+	 * @throws InvalidInterfaceException
986
+	 * @throws InvalidDataTypeException
987
+	 * @throws EE_Error
988
+	 */
989
+	public function spaces_remaining($consider_tickets = false)
990
+	{
991
+		// tickets remaining available for purchase
992
+		// no need for special checks for infinite, because if DTT_reg_limit == EE_INF, then EE_INF - x = EE_INF
993
+		$dtt_remaining = $this->reg_limit() - $this->sold_and_reserved();
994
+		if (! $consider_tickets) {
995
+			return $dtt_remaining;
996
+		}
997
+		$tickets_remaining = $this->tickets_remaining();
998
+		return min($dtt_remaining, $tickets_remaining);
999
+	}
1000
+
1001
+
1002
+	/**
1003
+	 * Counts the total tickets available
1004
+	 * (from all the different types of tickets which are available for this datetime).
1005
+	 *
1006
+	 * @param array $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
1007
+	 * @return int
1008
+	 * @throws ReflectionException
1009
+	 * @throws InvalidArgumentException
1010
+	 * @throws InvalidInterfaceException
1011
+	 * @throws InvalidDataTypeException
1012
+	 * @throws EE_Error
1013
+	 */
1014
+	public function tickets_remaining($query_params = array())
1015
+	{
1016
+		$sum = 0;
1017
+		$tickets = $this->tickets($query_params);
1018
+		if (! empty($tickets)) {
1019
+			foreach ($tickets as $ticket) {
1020
+				if ($ticket instanceof EE_Ticket) {
1021
+					// get the actual amount of tickets that can be sold
1022
+					$qty = $ticket->qty('saleable');
1023
+					if ($qty === EE_INF) {
1024
+						return EE_INF;
1025
+					}
1026
+					// no negative ticket quantities plz
1027
+					if ($qty > 0) {
1028
+						$sum += $qty;
1029
+					}
1030
+				}
1031
+			}
1032
+		}
1033
+		return $sum;
1034
+	}
1035
+
1036
+
1037
+	/**
1038
+	 * Gets the count of all the tickets available at this datetime (not ticket types)
1039
+	 * before any were sold
1040
+	 *
1041
+	 * @param array $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
1042
+	 * @return int
1043
+	 * @throws ReflectionException
1044
+	 * @throws InvalidArgumentException
1045
+	 * @throws InvalidInterfaceException
1046
+	 * @throws InvalidDataTypeException
1047
+	 * @throws EE_Error
1048
+	 */
1049
+	public function sum_tickets_initially_available($query_params = array())
1050
+	{
1051
+		return $this->sum_related('Ticket', $query_params, 'TKT_qty');
1052
+	}
1053
+
1054
+
1055
+	/**
1056
+	 * Returns the lesser-of-the two: spaces remaining at this datetime, or
1057
+	 * the total tickets remaining (a sum of the tickets remaining for each ticket type
1058
+	 * that is available for this datetime).
1059
+	 *
1060
+	 * @return int
1061
+	 * @throws ReflectionException
1062
+	 * @throws InvalidArgumentException
1063
+	 * @throws InvalidInterfaceException
1064
+	 * @throws InvalidDataTypeException
1065
+	 * @throws EE_Error
1066
+	 */
1067
+	public function total_tickets_available_at_this_datetime()
1068
+	{
1069
+		return $this->spaces_remaining(true);
1070
+	}
1071
+
1072
+
1073
+	/**
1074
+	 * This simply compares the internal dtt for the given string with NOW
1075
+	 * and determines if the date is upcoming or not.
1076
+	 *
1077
+	 * @access public
1078
+	 * @return boolean
1079
+	 * @throws ReflectionException
1080
+	 * @throws InvalidArgumentException
1081
+	 * @throws InvalidInterfaceException
1082
+	 * @throws InvalidDataTypeException
1083
+	 * @throws EE_Error
1084
+	 */
1085
+	public function is_upcoming()
1086
+	{
1087
+		return ($this->get_raw('DTT_EVT_start') > time());
1088
+	}
1089
+
1090
+
1091
+	/**
1092
+	 * This simply compares the internal datetime for the given string with NOW
1093
+	 * and returns if the date is active (i.e. start and end time)
1094
+	 *
1095
+	 * @return boolean
1096
+	 * @throws ReflectionException
1097
+	 * @throws InvalidArgumentException
1098
+	 * @throws InvalidInterfaceException
1099
+	 * @throws InvalidDataTypeException
1100
+	 * @throws EE_Error
1101
+	 */
1102
+	public function is_active()
1103
+	{
1104
+		return ($this->get_raw('DTT_EVT_start') < time() && $this->get_raw('DTT_EVT_end') > time());
1105
+	}
1106
+
1107
+
1108
+	/**
1109
+	 * This simply compares the internal dtt for the given string with NOW
1110
+	 * and determines if the date is expired or not.
1111
+	 *
1112
+	 * @return boolean
1113
+	 * @throws ReflectionException
1114
+	 * @throws InvalidArgumentException
1115
+	 * @throws InvalidInterfaceException
1116
+	 * @throws InvalidDataTypeException
1117
+	 * @throws EE_Error
1118
+	 */
1119
+	public function is_expired()
1120
+	{
1121
+		return ($this->get_raw('DTT_EVT_end') < time());
1122
+	}
1123
+
1124
+
1125
+	/**
1126
+	 * This returns the active status for whether an event is active, upcoming, or expired
1127
+	 *
1128
+	 * @return int return value will be one of the EE_Datetime status constants.
1129
+	 * @throws ReflectionException
1130
+	 * @throws InvalidArgumentException
1131
+	 * @throws InvalidInterfaceException
1132
+	 * @throws InvalidDataTypeException
1133
+	 * @throws EE_Error
1134
+	 */
1135
+	public function get_active_status()
1136
+	{
1137
+		$total_tickets_for_this_dtt = $this->total_tickets_available_at_this_datetime();
1138
+		if ($total_tickets_for_this_dtt !== false && $total_tickets_for_this_dtt < 1) {
1139
+			return EE_Datetime::sold_out;
1140
+		}
1141
+		if ($this->is_expired()) {
1142
+			return EE_Datetime::expired;
1143
+		}
1144
+		if ($this->is_upcoming()) {
1145
+			return EE_Datetime::upcoming;
1146
+		}
1147
+		if ($this->is_active()) {
1148
+			return EE_Datetime::active;
1149
+		}
1150
+		return null;
1151
+	}
1152
+
1153
+
1154
+	/**
1155
+	 * This returns a nice display name for the datetime that is contingent on the span between the dates and times.
1156
+	 *
1157
+	 * @param  boolean $use_dtt_name if TRUE then we'll use DTT->name() if its not empty.
1158
+	 * @return string
1159
+	 * @throws ReflectionException
1160
+	 * @throws InvalidArgumentException
1161
+	 * @throws InvalidInterfaceException
1162
+	 * @throws InvalidDataTypeException
1163
+	 * @throws EE_Error
1164
+	 */
1165
+	public function get_dtt_display_name($use_dtt_name = false)
1166
+	{
1167
+		if ($use_dtt_name) {
1168
+			$dtt_name = $this->name();
1169
+			if (! empty($dtt_name)) {
1170
+				return $dtt_name;
1171
+			}
1172
+		}
1173
+		// first condition is to see if the months are different
1174
+		if (date('m', $this->get_raw('DTT_EVT_start')) !== date('m', $this->get_raw('DTT_EVT_end'))
1175
+		) {
1176
+			$display_date = $this->start_date('M j\, Y g:i a') . ' - ' . $this->end_date('M j\, Y g:i a');
1177
+			// next condition is if its the same month but different day
1178
+		} else {
1179
+			if (date('m', $this->get_raw('DTT_EVT_start')) === date('m', $this->get_raw('DTT_EVT_end'))
1180
+				&& date('d', $this->get_raw('DTT_EVT_start')) !== date('d', $this->get_raw('DTT_EVT_end'))
1181
+			) {
1182
+				$display_date = $this->start_date('M j\, g:i a') . ' - ' . $this->end_date('M j\, g:i a Y');
1183
+			} else {
1184
+				$display_date = $this->start_date('F j\, Y')
1185
+								. ' @ '
1186
+								. $this->start_date('g:i a')
1187
+								. ' - '
1188
+								. $this->end_date('g:i a');
1189
+			}
1190
+		}
1191
+		return $display_date;
1192
+	}
1193
+
1194
+
1195
+	/**
1196
+	 * Gets all the tickets for this datetime
1197
+	 *
1198
+	 * @param array $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
1199
+	 * @return EE_Base_Class[]|EE_Ticket[]
1200
+	 * @throws ReflectionException
1201
+	 * @throws InvalidArgumentException
1202
+	 * @throws InvalidInterfaceException
1203
+	 * @throws InvalidDataTypeException
1204
+	 * @throws EE_Error
1205
+	 */
1206
+	public function tickets($query_params = array())
1207
+	{
1208
+		return $this->get_many_related('Ticket', $query_params);
1209
+	}
1210
+
1211
+
1212
+	/**
1213
+	 * Gets all the ticket types currently available for purchase
1214
+	 *
1215
+	 * @param array $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
1216
+	 * @return EE_Ticket[]
1217
+	 * @throws ReflectionException
1218
+	 * @throws InvalidArgumentException
1219
+	 * @throws InvalidInterfaceException
1220
+	 * @throws InvalidDataTypeException
1221
+	 * @throws EE_Error
1222
+	 */
1223
+	public function ticket_types_available_for_purchase($query_params = array())
1224
+	{
1225
+		// first check if datetime is valid
1226
+		if ($this->sold_out() || ! ($this->is_upcoming() || $this->is_active())) {
1227
+			return array();
1228
+		}
1229
+		if (empty($query_params)) {
1230
+			$query_params = array(
1231
+				array(
1232
+					'TKT_start_date' => array('<=', EEM_Ticket::instance()->current_time_for_query('TKT_start_date')),
1233
+					'TKT_end_date'   => array('>=', EEM_Ticket::instance()->current_time_for_query('TKT_end_date')),
1234
+					'TKT_deleted'    => false,
1235
+				),
1236
+			);
1237
+		}
1238
+		return $this->tickets($query_params);
1239
+	}
1240
+
1241
+
1242
+	/**
1243
+	 * @return EE_Base_Class|EE_Event
1244
+	 * @throws ReflectionException
1245
+	 * @throws InvalidArgumentException
1246
+	 * @throws InvalidInterfaceException
1247
+	 * @throws InvalidDataTypeException
1248
+	 * @throws EE_Error
1249
+	 */
1250
+	public function event()
1251
+	{
1252
+		return $this->get_first_related('Event');
1253
+	}
1254
+
1255
+
1256
+	/**
1257
+	 * Updates the DTT_sold attribute (and saves) based on the number of registrations for this datetime
1258
+	 * (via the tickets). into account
1259
+	 *
1260
+	 * @return int
1261
+	 * @throws ReflectionException
1262
+	 * @throws InvalidArgumentException
1263
+	 * @throws InvalidInterfaceException
1264
+	 * @throws InvalidDataTypeException
1265
+	 * @throws EE_Error
1266
+	 */
1267
+	public function update_sold()
1268
+	{
1269
+		$count_regs_for_this_datetime = EEM_Registration::instance()->count(
1270
+			array(
1271
+				array(
1272
+					'STS_ID'                 => EEM_Registration::status_id_approved,
1273
+					'REG_deleted'            => 0,
1274
+					'Ticket.Datetime.DTT_ID' => $this->ID(),
1275
+				),
1276
+			)
1277
+		);
1278
+		$sold = $this->sold();
1279
+		if ($count_regs_for_this_datetime > $sold) {
1280
+			$this->increase_sold($count_regs_for_this_datetime - $sold);
1281
+			$this->save();
1282
+		} elseif ($count_regs_for_this_datetime < $sold) {
1283
+			$this->decrease_sold($count_regs_for_this_datetime - $sold);
1284
+			$this->save();
1285
+		}
1286
+		return $count_regs_for_this_datetime;
1287
+	}
1288 1288
 }
Please login to merge, or discard this patch.
core/libraries/rest_api/ModelDataTranslator.php 1 patch
Indentation   +639 added lines, -639 removed lines patch added patch discarded remove patch
@@ -36,643 +36,643 @@
 block discarded – undo
36 36
 class ModelDataTranslator
37 37
 {
38 38
 
39
-    /**
40
-     * We used to use -1 for infinity in the rest api, but that's ambiguous for
41
-     * fields that COULD contain -1; so we use null
42
-     */
43
-    const EE_INF_IN_REST = null;
44
-
45
-
46
-    /**
47
-     * Prepares a possible array of input values from JSON for use by the models
48
-     *
49
-     * @param EE_Model_Field_Base $field_obj
50
-     * @param mixed               $original_value_maybe_array
51
-     * @param string              $requested_version
52
-     * @param string              $timezone_string treat values as being in this timezone
53
-     * @return mixed
54
-     * @throws RestException
55
-     */
56
-    public static function prepareFieldValuesFromJson(
57
-        $field_obj,
58
-        $original_value_maybe_array,
59
-        $requested_version,
60
-        $timezone_string = 'UTC'
61
-    ) {
62
-        if (is_array($original_value_maybe_array)
63
-            && ! $field_obj instanceof EE_Serialized_Text_Field
64
-        ) {
65
-            $new_value_maybe_array = array();
66
-            foreach ($original_value_maybe_array as $array_key => $array_item) {
67
-                $new_value_maybe_array[ $array_key ] = ModelDataTranslator::prepareFieldValueFromJson(
68
-                    $field_obj,
69
-                    $array_item,
70
-                    $requested_version,
71
-                    $timezone_string
72
-                );
73
-            }
74
-        } else {
75
-            $new_value_maybe_array = ModelDataTranslator::prepareFieldValueFromJson(
76
-                $field_obj,
77
-                $original_value_maybe_array,
78
-                $requested_version,
79
-                $timezone_string
80
-            );
81
-        }
82
-        return $new_value_maybe_array;
83
-    }
84
-
85
-
86
-    /**
87
-     * Prepares an array of field values FOR use in JSON/REST API
88
-     *
89
-     * @param EE_Model_Field_Base $field_obj
90
-     * @param mixed               $original_value_maybe_array
91
-     * @param string              $request_version (eg 4.8.36)
92
-     * @return array
93
-     */
94
-    public static function prepareFieldValuesForJson($field_obj, $original_value_maybe_array, $request_version)
95
-    {
96
-        if (is_array($original_value_maybe_array)) {
97
-            $new_value = array();
98
-            foreach ($original_value_maybe_array as $key => $value) {
99
-                $new_value[ $key ] = ModelDataTranslator::prepareFieldValuesForJson(
100
-                    $field_obj,
101
-                    $value,
102
-                    $request_version
103
-                );
104
-            }
105
-        } else {
106
-            $new_value = ModelDataTranslator::prepareFieldValueForJson(
107
-                $field_obj,
108
-                $original_value_maybe_array,
109
-                $request_version
110
-            );
111
-        }
112
-        return $new_value;
113
-    }
114
-
115
-
116
-    /**
117
-     * Prepares incoming data from the json or $_REQUEST parameters for the models'
118
-     * "$query_params".
119
-     *
120
-     * @param EE_Model_Field_Base $field_obj
121
-     * @param mixed               $original_value
122
-     * @param string              $requested_version
123
-     * @param string              $timezone_string treat values as being in this timezone
124
-     * @return mixed
125
-     * @throws RestException
126
-     * @throws DomainException
127
-     * @throws EE_Error
128
-     */
129
-    public static function prepareFieldValueFromJson(
130
-        $field_obj,
131
-        $original_value,
132
-        $requested_version,
133
-        $timezone_string = 'UTC' // UTC
134
-    ) {
135
-        // check if they accidentally submitted an error value. If so throw an exception
136
-        if (is_array($original_value)
137
-            && isset($original_value['error_code'], $original_value['error_message'])) {
138
-            throw new RestException(
139
-                'rest_submitted_error_value',
140
-                sprintf(
141
-                    esc_html__(
142
-                        'You tried to submit a JSON error object as a value for %1$s. That\'s not allowed.',
143
-                        'event_espresso'
144
-                    ),
145
-                    $field_obj->get_name()
146
-                ),
147
-                array(
148
-                    'status' => 400,
149
-                )
150
-            );
151
-        }
152
-        // double-check for serialized PHP. We never accept serialized PHP. No way Jose.
153
-        ModelDataTranslator::throwExceptionIfContainsSerializedData($original_value);
154
-        $timezone_string = $timezone_string !== '' ? $timezone_string : get_option('timezone_string', '');
155
-        $new_value = null;
156
-        // walk through the submitted data and double-check for serialized PHP. We never accept serialized PHP. No
157
-        // way Jose.
158
-        ModelDataTranslator::throwExceptionIfContainsSerializedData($original_value);
159
-        if ($field_obj instanceof EE_Infinite_Integer_Field
160
-            && in_array($original_value, array(null, ''), true)
161
-        ) {
162
-            $new_value = EE_INF;
163
-        } elseif ($field_obj instanceof EE_Datetime_Field) {
164
-            $new_value = rest_parse_date(
165
-                self::getTimestampWithTimezoneOffset($original_value, $field_obj, $timezone_string)
166
-            );
167
-            if ($new_value === false) {
168
-                throw new RestException(
169
-                    'invalid_format_for_timestamp',
170
-                    sprintf(
171
-                        esc_html__(
172
-                            '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.',
173
-                            'event_espresso'
174
-                        ),
175
-                        'RFC3339',
176
-                        'ISO8601',
177
-                        $original_value
178
-                    ),
179
-                    array(
180
-                        'status' => 400,
181
-                    )
182
-                );
183
-            }
184
-        } else {
185
-            $new_value = $original_value;
186
-        }
187
-        return $new_value;
188
-    }
189
-
190
-
191
-    /**
192
-     * This checks if the incoming timestamp has timezone information already on it and if it doesn't then adds timezone
193
-     * information via details obtained from the host site.
194
-     *
195
-     * @param string            $original_timestamp
196
-     * @param EE_Datetime_Field $datetime_field
197
-     * @param                   $timezone_string
198
-     * @return string
199
-     * @throws DomainException
200
-     */
201
-    private static function getTimestampWithTimezoneOffset(
202
-        $original_timestamp,
203
-        EE_Datetime_Field $datetime_field,
204
-        $timezone_string
205
-    ) {
206
-        // already have timezone information?
207
-        if (preg_match('/Z|(\+|\-)(\d{2}:\d{2})/', $original_timestamp)) {
208
-            // yes, we're ignoring the timezone.
209
-            return $original_timestamp;
210
-        }
211
-        // need to append timezone
212
-        list($offset_sign, $offset_secs) = self::parseTimezoneOffset(
213
-            $datetime_field->get_timezone_offset(
214
-                new \DateTimeZone($timezone_string),
215
-                $original_timestamp
216
-            )
217
-        );
218
-        $offset_string =
219
-            str_pad(
220
-                floor($offset_secs / HOUR_IN_SECONDS),
221
-                2,
222
-                '0',
223
-                STR_PAD_LEFT
224
-            )
225
-            . ':'
226
-            . str_pad(
227
-                ($offset_secs % HOUR_IN_SECONDS) / MINUTE_IN_SECONDS,
228
-                2,
229
-                '0',
230
-                STR_PAD_LEFT
231
-            );
232
-        return $original_timestamp . $offset_sign . $offset_string;
233
-    }
234
-
235
-
236
-    /**
237
-     * Throws an exception if $data is a serialized PHP string (or somehow an actually PHP object, although I don't
238
-     * think that can happen). If $data is an array, recurses into its keys and values
239
-     *
240
-     * @param mixed $data
241
-     * @throws RestException
242
-     * @return void
243
-     */
244
-    public static function throwExceptionIfContainsSerializedData($data)
245
-    {
246
-        if (is_array($data)) {
247
-            foreach ($data as $key => $value) {
248
-                ModelDataTranslator::throwExceptionIfContainsSerializedData($key);
249
-                ModelDataTranslator::throwExceptionIfContainsSerializedData($value);
250
-            }
251
-        } else {
252
-            if (is_serialized($data) || is_object($data)) {
253
-                throw new RestException(
254
-                    'serialized_data_submission_prohibited',
255
-                    esc_html__(
256
-                    // @codingStandardsIgnoreStart
257
-                        'You tried to submit a string of serialized text. Serialized PHP is prohibited over the EE4 REST API.',
258
-                        // @codingStandardsIgnoreEnd
259
-                        'event_espresso'
260
-                    )
261
-                );
262
-            }
263
-        }
264
-    }
265
-
266
-
267
-    /**
268
-     * determines what's going on with them timezone strings
269
-     *
270
-     * @param int $timezone_offset
271
-     * @return array
272
-     */
273
-    private static function parseTimezoneOffset($timezone_offset)
274
-    {
275
-        $first_char = substr((string) $timezone_offset, 0, 1);
276
-        if ($first_char === '+' || $first_char === '-') {
277
-            $offset_sign = $first_char;
278
-            $offset_secs = substr((string) $timezone_offset, 1);
279
-        } else {
280
-            $offset_sign = '+';
281
-            $offset_secs = $timezone_offset;
282
-        }
283
-        return array($offset_sign, $offset_secs);
284
-    }
285
-
286
-
287
-    /**
288
-     * Prepares a field's value for display in the API
289
-     *
290
-     * @param EE_Model_Field_Base $field_obj
291
-     * @param mixed               $original_value
292
-     * @param string              $requested_version
293
-     * @return mixed
294
-     */
295
-    public static function prepareFieldValueForJson($field_obj, $original_value, $requested_version)
296
-    {
297
-        if ($original_value === EE_INF) {
298
-            $new_value = ModelDataTranslator::EE_INF_IN_REST;
299
-        } elseif ($field_obj instanceof EE_Datetime_Field) {
300
-            if (is_string($original_value)) {
301
-                // did they submit a string of a unix timestamp?
302
-                if (is_numeric($original_value)) {
303
-                    $datetime_obj = new \DateTime();
304
-                    $datetime_obj->setTimestamp((int) $original_value);
305
-                } else {
306
-                    // first, check if its a MySQL timestamp in GMT
307
-                    $datetime_obj = \DateTime::createFromFormat('Y-m-d H:i:s', $original_value);
308
-                }
309
-                if (! $datetime_obj instanceof \DateTime) {
310
-                    // so it's not a unix timestamp or a MySQL timestamp. Maybe its in the field's date/time format?
311
-                    $datetime_obj = $field_obj->prepare_for_set($original_value);
312
-                }
313
-                $original_value = $datetime_obj;
314
-            }
315
-            if ($original_value instanceof \DateTime) {
316
-                $new_value = $original_value->format('Y-m-d H:i:s');
317
-            } elseif (is_int($original_value) || is_float($original_value)) {
318
-                $new_value = date('Y-m-d H:i:s', $original_value);
319
-            } elseif ($original_value === null || $original_value === '') {
320
-                $new_value = null;
321
-            } else {
322
-                // so it's not a datetime object, unix timestamp (as string or int),
323
-                // MySQL timestamp, or even a string in the field object's format. So no idea what it is
324
-                throw new \EE_Error(
325
-                    sprintf(
326
-                        esc_html__(
327
-                        // @codingStandardsIgnoreStart
328
-                            '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".',
329
-                            // @codingStandardsIgnoreEnd
330
-                            'event_espresso'
331
-                        ),
332
-                        $original_value,
333
-                        $field_obj->get_name(),
334
-                        $field_obj->get_model_name(),
335
-                        $field_obj->get_time_format() . ' ' . $field_obj->get_time_format()
336
-                    )
337
-                );
338
-            }
339
-            if ($new_value !== null) {
340
-                $new_value = mysql_to_rfc3339($new_value);
341
-            }
342
-        } else {
343
-            $new_value = $original_value;
344
-        }
345
-        // are we about to send an object? just don't. We have no good way to represent it in JSON.
346
-        // can't just check using is_object() because that missed PHP incomplete objects
347
-        if (! ModelDataTranslator::isRepresentableInJson($new_value)) {
348
-            $new_value = array(
349
-                'error_code'    => 'php_object_not_return',
350
-                'error_message' => esc_html__(
351
-                    'The value of this field in the database is a PHP object, which can\'t be represented in JSON.',
352
-                    'event_espresso'
353
-                ),
354
-            );
355
-        }
356
-        return apply_filters(
357
-            'FHEE__EventEspresso\core\libraries\rest_api\Model_Data_Translator__prepare_field_for_rest_api',
358
-            $new_value,
359
-            $field_obj,
360
-            $original_value,
361
-            $requested_version
362
-        );
363
-    }
364
-
365
-
366
-    /**
367
-     * Prepares condition-query-parameters (like what's in where and having) from
368
-     * the format expected in the API to use in the models
369
-     *
370
-     * @param array $inputted_query_params_of_this_type
371
-     * @param EEM_Base $model
372
-     * @param string $requested_version
373
-     * @param boolean $writing whether this data will be written to the DB, or if we're just building a query.
374
-     *                          If we're writing to the DB, we don't expect any operators, or any logic query
375
-     *                          parameters, and we also won't accept serialized data unless the current user has
376
-     *                          unfiltered_html.
377
-     * @return array
378
-     * @throws DomainException
379
-     * @throws EE_Error
380
-     * @throws RestException
381
-     * @throws InvalidDataTypeException
382
-     * @throws InvalidInterfaceException
383
-     * @throws InvalidArgumentException
384
-     */
385
-    public static function prepareConditionsQueryParamsForModels(
386
-        $inputted_query_params_of_this_type,
387
-        EEM_Base $model,
388
-        $requested_version,
389
-        $writing = false
390
-    ) {
391
-        $query_param_for_models = array();
392
-        $context = new RestIncomingQueryParamContext($model, $requested_version, $writing);
393
-        foreach ($inputted_query_params_of_this_type as $query_param_key => $query_param_value) {
394
-            $query_param_meta = new RestIncomingQueryParamMetadata($query_param_key, $query_param_value, $context);
395
-            if ($query_param_meta->getField() instanceof EE_Model_Field_Base) {
396
-                $translated_value = $query_param_meta->determineConditionsQueryParameterValue();
397
-                if ((isset($query_param_for_models[ $query_param_meta->getQueryParamKey() ]) && $query_param_meta->isGmtField())
398
-                    || $translated_value === null
399
-                ) {
400
-                    // they have already provided a non-gmt field, ignore the gmt one. That's what WP core
401
-                    // currently does (they might change it though). See https://core.trac.wordpress.org/ticket/39954
402
-                    // OR we couldn't create a translated value from their input
403
-                    continue;
404
-                }
405
-                $query_param_for_models[ $query_param_meta->getQueryParamKey() ] = $translated_value;
406
-            } else {
407
-                $nested_query_params = $query_param_meta->determineNestedConditionQueryParameters();
408
-                if ($nested_query_params) {
409
-                    $query_param_for_models[ $query_param_meta->getQueryParamKey() ] = $nested_query_params;
410
-                }
411
-            }
412
-        }
413
-        return $query_param_for_models;
414
-    }
415
-
416
-    /**
417
-     * Mostly checks if the last 4 characters are "_gmt", indicating its a
418
-     * gmt date field name
419
-     *
420
-     * @param string $field_name
421
-     * @return boolean
422
-     */
423
-    public static function isGmtDateFieldName($field_name)
424
-    {
425
-        return substr(
426
-            ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey($field_name),
427
-            -4,
428
-            4
429
-        ) === '_gmt';
430
-    }
431
-
432
-
433
-    /**
434
-     * Removes the last "_gmt" part of a field name (and if there is no "_gmt" at the end, leave it alone)
435
-     *
436
-     * @param string $field_name
437
-     * @return string
438
-     */
439
-    public static function removeGmtFromFieldName($field_name)
440
-    {
441
-        if (! ModelDataTranslator::isGmtDateFieldName($field_name)) {
442
-            return $field_name;
443
-        }
444
-        $query_param_sans_stars = ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey(
445
-            $field_name
446
-        );
447
-        $query_param_sans_gmt_and_sans_stars = substr(
448
-            $query_param_sans_stars,
449
-            0,
450
-            strrpos(
451
-                $field_name,
452
-                '_gmt'
453
-            )
454
-        );
455
-        return str_replace($query_param_sans_stars, $query_param_sans_gmt_and_sans_stars, $field_name);
456
-    }
457
-
458
-
459
-    /**
460
-     * Takes a field name from the REST API and prepares it for the model querying
461
-     *
462
-     * @param string $field_name
463
-     * @return string
464
-     */
465
-    public static function prepareFieldNameFromJson($field_name)
466
-    {
467
-        if (ModelDataTranslator::isGmtDateFieldName($field_name)) {
468
-            return ModelDataTranslator::removeGmtFromFieldName($field_name);
469
-        }
470
-        return $field_name;
471
-    }
472
-
473
-
474
-    /**
475
-     * Takes array of field names from REST API and prepares for models
476
-     *
477
-     * @param array $field_names
478
-     * @return array of field names (possibly include model prefixes)
479
-     */
480
-    public static function prepareFieldNamesFromJson(array $field_names)
481
-    {
482
-        $new_array = array();
483
-        foreach ($field_names as $key => $field_name) {
484
-            $new_array[ $key ] = ModelDataTranslator::prepareFieldNameFromJson($field_name);
485
-        }
486
-        return $new_array;
487
-    }
488
-
489
-
490
-    /**
491
-     * Takes array where array keys are field names (possibly with model path prefixes)
492
-     * from the REST API and prepares them for model querying
493
-     *
494
-     * @param array $field_names_as_keys
495
-     * @return array
496
-     */
497
-    public static function prepareFieldNamesInArrayKeysFromJson(array $field_names_as_keys)
498
-    {
499
-        $new_array = array();
500
-        foreach ($field_names_as_keys as $field_name => $value) {
501
-            $new_array[ ModelDataTranslator::prepareFieldNameFromJson($field_name) ] = $value;
502
-        }
503
-        return $new_array;
504
-    }
505
-
506
-
507
-    /**
508
-     * Prepares an array of model query params for use in the REST API
509
-     *
510
-     * @param array    $model_query_params
511
-     * @param EEM_Base $model
512
-     * @param string   $requested_version  eg "4.8.36". If null is provided, defaults to the latest release of the EE4
513
-     *                                     REST API
514
-     * @return array which can be passed into the EE4 REST API when querying a model resource
515
-     * @throws EE_Error
516
-     */
517
-    public static function prepareQueryParamsForRestApi(
518
-        array $model_query_params,
519
-        EEM_Base $model,
520
-        $requested_version = null
521
-    ) {
522
-        if ($requested_version === null) {
523
-            $requested_version = EED_Core_Rest_Api::latest_rest_api_version();
524
-        }
525
-        $rest_query_params = $model_query_params;
526
-        if (isset($model_query_params[0])) {
527
-            $rest_query_params['where'] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
528
-                $model_query_params[0],
529
-                $model,
530
-                $requested_version
531
-            );
532
-            unset($rest_query_params[0]);
533
-        }
534
-        if (isset($model_query_params['having'])) {
535
-            $rest_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
536
-                $model_query_params['having'],
537
-                $model,
538
-                $requested_version
539
-            );
540
-        }
541
-        return apply_filters(
542
-            'FHEE__EventEspresso\core\libraries\rest_api\Model_Data_Translator__prepare_query_params_for_rest_api',
543
-            $rest_query_params,
544
-            $model_query_params,
545
-            $model,
546
-            $requested_version
547
-        );
548
-    }
549
-
550
-
551
-    /**
552
-     * Prepares all the sub-conditions query parameters (eg having or where conditions) for use in the rest api
553
-     *
554
-     * @param array    $inputted_query_params_of_this_type  eg like the "where" or "having" conditions query params
555
-     *                                                      @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions
556
-     * @param EEM_Base $model
557
-     * @param string   $requested_version                   eg "4.8.36"
558
-     * @return array ready for use in the rest api query params
559
-     * @throws EE_Error
560
-     * @throws ObjectDetectedException if somehow a PHP object were in the query params' values,
561
-     *                                                      (which would be really unusual)
562
-     */
563
-    public static function prepareConditionsQueryParamsForRestApi(
564
-        $inputted_query_params_of_this_type,
565
-        EEM_Base $model,
566
-        $requested_version
567
-    ) {
568
-        $query_param_for_models = array();
569
-        foreach ($inputted_query_params_of_this_type as $query_param_key => $query_param_value) {
570
-            $field = ModelDataTranslator::deduceFieldFromQueryParam(
571
-                ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey($query_param_key),
572
-                $model
573
-            );
574
-            if ($field instanceof EE_Model_Field_Base) {
575
-                // did they specify an operator?
576
-                if (is_array($query_param_value)) {
577
-                    $op = $query_param_value[0];
578
-                    $translated_value = array($op);
579
-                    if (isset($query_param_value[1])) {
580
-                        $value = $query_param_value[1];
581
-                        $translated_value[1] = ModelDataTranslator::prepareFieldValuesForJson(
582
-                            $field,
583
-                            $value,
584
-                            $requested_version
585
-                        );
586
-                    }
587
-                } else {
588
-                    $translated_value = ModelDataTranslator::prepareFieldValueForJson(
589
-                        $field,
590
-                        $query_param_value,
591
-                        $requested_version
592
-                    );
593
-                }
594
-                $query_param_for_models[ $query_param_key ] = $translated_value;
595
-            } else {
596
-                // so it's not for a field, assume it's a logic query param key
597
-                $query_param_for_models[ $query_param_key ] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
598
-                    $query_param_value,
599
-                    $model,
600
-                    $requested_version
601
-                );
602
-            }
603
-        }
604
-        return $query_param_for_models;
605
-    }
606
-
607
-
608
-    /**
609
-     * @param $condition_query_param_key
610
-     * @return string
611
-     */
612
-    public static function removeStarsAndAnythingAfterFromConditionQueryParamKey($condition_query_param_key)
613
-    {
614
-        $pos_of_star = strpos($condition_query_param_key, '*');
615
-        if ($pos_of_star === false) {
616
-            return $condition_query_param_key;
617
-        } else {
618
-            $condition_query_param_sans_star = substr($condition_query_param_key, 0, $pos_of_star);
619
-            return $condition_query_param_sans_star;
620
-        }
621
-    }
622
-
623
-
624
-    /**
625
-     * Takes the input parameter and finds the model field that it indicates.
626
-     *
627
-     * @param string   $query_param_name like Registration.Transaction.TXN_ID, Event.Datetime.start_time, or REG_ID
628
-     * @param EEM_Base $model
629
-     * @return EE_Model_Field_Base
630
-     * @throws EE_Error
631
-     */
632
-    public static function deduceFieldFromQueryParam($query_param_name, EEM_Base $model)
633
-    {
634
-        // ok, now proceed with deducing which part is the model's name, and which is the field's name
635
-        // which will help us find the database table and column
636
-        $query_param_parts = explode('.', $query_param_name);
637
-        if (empty($query_param_parts)) {
638
-            throw new EE_Error(
639
-                sprintf(
640
-                    __(
641
-                        '_extract_column_name is empty when trying to extract column and table name from %s',
642
-                        'event_espresso'
643
-                    ),
644
-                    $query_param_name
645
-                )
646
-            );
647
-        }
648
-        $number_of_parts = count($query_param_parts);
649
-        $last_query_param_part = $query_param_parts[ count($query_param_parts) - 1 ];
650
-        if ($number_of_parts === 1) {
651
-            $field_name = $last_query_param_part;
652
-        } else {// $number_of_parts >= 2
653
-            // the last part is the column name, and there are only 2parts. therefore...
654
-            $field_name = $last_query_param_part;
655
-            $model = \EE_Registry::instance()->load_model($query_param_parts[ $number_of_parts - 2 ]);
656
-        }
657
-        try {
658
-            return $model->field_settings_for($field_name, false);
659
-        } catch (EE_Error $e) {
660
-            return null;
661
-        }
662
-    }
663
-
664
-
665
-    /**
666
-     * Returns true if $data can be easily represented in JSON.
667
-     * Basically, objects and resources can't be represented in JSON easily.
668
-     *
669
-     * @param mixed $data
670
-     * @return bool
671
-     */
672
-    protected static function isRepresentableInJson($data)
673
-    {
674
-        return is_scalar($data)
675
-               || is_array($data)
676
-               || is_null($data);
677
-    }
39
+	/**
40
+	 * We used to use -1 for infinity in the rest api, but that's ambiguous for
41
+	 * fields that COULD contain -1; so we use null
42
+	 */
43
+	const EE_INF_IN_REST = null;
44
+
45
+
46
+	/**
47
+	 * Prepares a possible array of input values from JSON for use by the models
48
+	 *
49
+	 * @param EE_Model_Field_Base $field_obj
50
+	 * @param mixed               $original_value_maybe_array
51
+	 * @param string              $requested_version
52
+	 * @param string              $timezone_string treat values as being in this timezone
53
+	 * @return mixed
54
+	 * @throws RestException
55
+	 */
56
+	public static function prepareFieldValuesFromJson(
57
+		$field_obj,
58
+		$original_value_maybe_array,
59
+		$requested_version,
60
+		$timezone_string = 'UTC'
61
+	) {
62
+		if (is_array($original_value_maybe_array)
63
+			&& ! $field_obj instanceof EE_Serialized_Text_Field
64
+		) {
65
+			$new_value_maybe_array = array();
66
+			foreach ($original_value_maybe_array as $array_key => $array_item) {
67
+				$new_value_maybe_array[ $array_key ] = ModelDataTranslator::prepareFieldValueFromJson(
68
+					$field_obj,
69
+					$array_item,
70
+					$requested_version,
71
+					$timezone_string
72
+				);
73
+			}
74
+		} else {
75
+			$new_value_maybe_array = ModelDataTranslator::prepareFieldValueFromJson(
76
+				$field_obj,
77
+				$original_value_maybe_array,
78
+				$requested_version,
79
+				$timezone_string
80
+			);
81
+		}
82
+		return $new_value_maybe_array;
83
+	}
84
+
85
+
86
+	/**
87
+	 * Prepares an array of field values FOR use in JSON/REST API
88
+	 *
89
+	 * @param EE_Model_Field_Base $field_obj
90
+	 * @param mixed               $original_value_maybe_array
91
+	 * @param string              $request_version (eg 4.8.36)
92
+	 * @return array
93
+	 */
94
+	public static function prepareFieldValuesForJson($field_obj, $original_value_maybe_array, $request_version)
95
+	{
96
+		if (is_array($original_value_maybe_array)) {
97
+			$new_value = array();
98
+			foreach ($original_value_maybe_array as $key => $value) {
99
+				$new_value[ $key ] = ModelDataTranslator::prepareFieldValuesForJson(
100
+					$field_obj,
101
+					$value,
102
+					$request_version
103
+				);
104
+			}
105
+		} else {
106
+			$new_value = ModelDataTranslator::prepareFieldValueForJson(
107
+				$field_obj,
108
+				$original_value_maybe_array,
109
+				$request_version
110
+			);
111
+		}
112
+		return $new_value;
113
+	}
114
+
115
+
116
+	/**
117
+	 * Prepares incoming data from the json or $_REQUEST parameters for the models'
118
+	 * "$query_params".
119
+	 *
120
+	 * @param EE_Model_Field_Base $field_obj
121
+	 * @param mixed               $original_value
122
+	 * @param string              $requested_version
123
+	 * @param string              $timezone_string treat values as being in this timezone
124
+	 * @return mixed
125
+	 * @throws RestException
126
+	 * @throws DomainException
127
+	 * @throws EE_Error
128
+	 */
129
+	public static function prepareFieldValueFromJson(
130
+		$field_obj,
131
+		$original_value,
132
+		$requested_version,
133
+		$timezone_string = 'UTC' // UTC
134
+	) {
135
+		// check if they accidentally submitted an error value. If so throw an exception
136
+		if (is_array($original_value)
137
+			&& isset($original_value['error_code'], $original_value['error_message'])) {
138
+			throw new RestException(
139
+				'rest_submitted_error_value',
140
+				sprintf(
141
+					esc_html__(
142
+						'You tried to submit a JSON error object as a value for %1$s. That\'s not allowed.',
143
+						'event_espresso'
144
+					),
145
+					$field_obj->get_name()
146
+				),
147
+				array(
148
+					'status' => 400,
149
+				)
150
+			);
151
+		}
152
+		// double-check for serialized PHP. We never accept serialized PHP. No way Jose.
153
+		ModelDataTranslator::throwExceptionIfContainsSerializedData($original_value);
154
+		$timezone_string = $timezone_string !== '' ? $timezone_string : get_option('timezone_string', '');
155
+		$new_value = null;
156
+		// walk through the submitted data and double-check for serialized PHP. We never accept serialized PHP. No
157
+		// way Jose.
158
+		ModelDataTranslator::throwExceptionIfContainsSerializedData($original_value);
159
+		if ($field_obj instanceof EE_Infinite_Integer_Field
160
+			&& in_array($original_value, array(null, ''), true)
161
+		) {
162
+			$new_value = EE_INF;
163
+		} elseif ($field_obj instanceof EE_Datetime_Field) {
164
+			$new_value = rest_parse_date(
165
+				self::getTimestampWithTimezoneOffset($original_value, $field_obj, $timezone_string)
166
+			);
167
+			if ($new_value === false) {
168
+				throw new RestException(
169
+					'invalid_format_for_timestamp',
170
+					sprintf(
171
+						esc_html__(
172
+							'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.',
173
+							'event_espresso'
174
+						),
175
+						'RFC3339',
176
+						'ISO8601',
177
+						$original_value
178
+					),
179
+					array(
180
+						'status' => 400,
181
+					)
182
+				);
183
+			}
184
+		} else {
185
+			$new_value = $original_value;
186
+		}
187
+		return $new_value;
188
+	}
189
+
190
+
191
+	/**
192
+	 * This checks if the incoming timestamp has timezone information already on it and if it doesn't then adds timezone
193
+	 * information via details obtained from the host site.
194
+	 *
195
+	 * @param string            $original_timestamp
196
+	 * @param EE_Datetime_Field $datetime_field
197
+	 * @param                   $timezone_string
198
+	 * @return string
199
+	 * @throws DomainException
200
+	 */
201
+	private static function getTimestampWithTimezoneOffset(
202
+		$original_timestamp,
203
+		EE_Datetime_Field $datetime_field,
204
+		$timezone_string
205
+	) {
206
+		// already have timezone information?
207
+		if (preg_match('/Z|(\+|\-)(\d{2}:\d{2})/', $original_timestamp)) {
208
+			// yes, we're ignoring the timezone.
209
+			return $original_timestamp;
210
+		}
211
+		// need to append timezone
212
+		list($offset_sign, $offset_secs) = self::parseTimezoneOffset(
213
+			$datetime_field->get_timezone_offset(
214
+				new \DateTimeZone($timezone_string),
215
+				$original_timestamp
216
+			)
217
+		);
218
+		$offset_string =
219
+			str_pad(
220
+				floor($offset_secs / HOUR_IN_SECONDS),
221
+				2,
222
+				'0',
223
+				STR_PAD_LEFT
224
+			)
225
+			. ':'
226
+			. str_pad(
227
+				($offset_secs % HOUR_IN_SECONDS) / MINUTE_IN_SECONDS,
228
+				2,
229
+				'0',
230
+				STR_PAD_LEFT
231
+			);
232
+		return $original_timestamp . $offset_sign . $offset_string;
233
+	}
234
+
235
+
236
+	/**
237
+	 * Throws an exception if $data is a serialized PHP string (or somehow an actually PHP object, although I don't
238
+	 * think that can happen). If $data is an array, recurses into its keys and values
239
+	 *
240
+	 * @param mixed $data
241
+	 * @throws RestException
242
+	 * @return void
243
+	 */
244
+	public static function throwExceptionIfContainsSerializedData($data)
245
+	{
246
+		if (is_array($data)) {
247
+			foreach ($data as $key => $value) {
248
+				ModelDataTranslator::throwExceptionIfContainsSerializedData($key);
249
+				ModelDataTranslator::throwExceptionIfContainsSerializedData($value);
250
+			}
251
+		} else {
252
+			if (is_serialized($data) || is_object($data)) {
253
+				throw new RestException(
254
+					'serialized_data_submission_prohibited',
255
+					esc_html__(
256
+					// @codingStandardsIgnoreStart
257
+						'You tried to submit a string of serialized text. Serialized PHP is prohibited over the EE4 REST API.',
258
+						// @codingStandardsIgnoreEnd
259
+						'event_espresso'
260
+					)
261
+				);
262
+			}
263
+		}
264
+	}
265
+
266
+
267
+	/**
268
+	 * determines what's going on with them timezone strings
269
+	 *
270
+	 * @param int $timezone_offset
271
+	 * @return array
272
+	 */
273
+	private static function parseTimezoneOffset($timezone_offset)
274
+	{
275
+		$first_char = substr((string) $timezone_offset, 0, 1);
276
+		if ($first_char === '+' || $first_char === '-') {
277
+			$offset_sign = $first_char;
278
+			$offset_secs = substr((string) $timezone_offset, 1);
279
+		} else {
280
+			$offset_sign = '+';
281
+			$offset_secs = $timezone_offset;
282
+		}
283
+		return array($offset_sign, $offset_secs);
284
+	}
285
+
286
+
287
+	/**
288
+	 * Prepares a field's value for display in the API
289
+	 *
290
+	 * @param EE_Model_Field_Base $field_obj
291
+	 * @param mixed               $original_value
292
+	 * @param string              $requested_version
293
+	 * @return mixed
294
+	 */
295
+	public static function prepareFieldValueForJson($field_obj, $original_value, $requested_version)
296
+	{
297
+		if ($original_value === EE_INF) {
298
+			$new_value = ModelDataTranslator::EE_INF_IN_REST;
299
+		} elseif ($field_obj instanceof EE_Datetime_Field) {
300
+			if (is_string($original_value)) {
301
+				// did they submit a string of a unix timestamp?
302
+				if (is_numeric($original_value)) {
303
+					$datetime_obj = new \DateTime();
304
+					$datetime_obj->setTimestamp((int) $original_value);
305
+				} else {
306
+					// first, check if its a MySQL timestamp in GMT
307
+					$datetime_obj = \DateTime::createFromFormat('Y-m-d H:i:s', $original_value);
308
+				}
309
+				if (! $datetime_obj instanceof \DateTime) {
310
+					// so it's not a unix timestamp or a MySQL timestamp. Maybe its in the field's date/time format?
311
+					$datetime_obj = $field_obj->prepare_for_set($original_value);
312
+				}
313
+				$original_value = $datetime_obj;
314
+			}
315
+			if ($original_value instanceof \DateTime) {
316
+				$new_value = $original_value->format('Y-m-d H:i:s');
317
+			} elseif (is_int($original_value) || is_float($original_value)) {
318
+				$new_value = date('Y-m-d H:i:s', $original_value);
319
+			} elseif ($original_value === null || $original_value === '') {
320
+				$new_value = null;
321
+			} else {
322
+				// so it's not a datetime object, unix timestamp (as string or int),
323
+				// MySQL timestamp, or even a string in the field object's format. So no idea what it is
324
+				throw new \EE_Error(
325
+					sprintf(
326
+						esc_html__(
327
+						// @codingStandardsIgnoreStart
328
+							'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".',
329
+							// @codingStandardsIgnoreEnd
330
+							'event_espresso'
331
+						),
332
+						$original_value,
333
+						$field_obj->get_name(),
334
+						$field_obj->get_model_name(),
335
+						$field_obj->get_time_format() . ' ' . $field_obj->get_time_format()
336
+					)
337
+				);
338
+			}
339
+			if ($new_value !== null) {
340
+				$new_value = mysql_to_rfc3339($new_value);
341
+			}
342
+		} else {
343
+			$new_value = $original_value;
344
+		}
345
+		// are we about to send an object? just don't. We have no good way to represent it in JSON.
346
+		// can't just check using is_object() because that missed PHP incomplete objects
347
+		if (! ModelDataTranslator::isRepresentableInJson($new_value)) {
348
+			$new_value = array(
349
+				'error_code'    => 'php_object_not_return',
350
+				'error_message' => esc_html__(
351
+					'The value of this field in the database is a PHP object, which can\'t be represented in JSON.',
352
+					'event_espresso'
353
+				),
354
+			);
355
+		}
356
+		return apply_filters(
357
+			'FHEE__EventEspresso\core\libraries\rest_api\Model_Data_Translator__prepare_field_for_rest_api',
358
+			$new_value,
359
+			$field_obj,
360
+			$original_value,
361
+			$requested_version
362
+		);
363
+	}
364
+
365
+
366
+	/**
367
+	 * Prepares condition-query-parameters (like what's in where and having) from
368
+	 * the format expected in the API to use in the models
369
+	 *
370
+	 * @param array $inputted_query_params_of_this_type
371
+	 * @param EEM_Base $model
372
+	 * @param string $requested_version
373
+	 * @param boolean $writing whether this data will be written to the DB, or if we're just building a query.
374
+	 *                          If we're writing to the DB, we don't expect any operators, or any logic query
375
+	 *                          parameters, and we also won't accept serialized data unless the current user has
376
+	 *                          unfiltered_html.
377
+	 * @return array
378
+	 * @throws DomainException
379
+	 * @throws EE_Error
380
+	 * @throws RestException
381
+	 * @throws InvalidDataTypeException
382
+	 * @throws InvalidInterfaceException
383
+	 * @throws InvalidArgumentException
384
+	 */
385
+	public static function prepareConditionsQueryParamsForModels(
386
+		$inputted_query_params_of_this_type,
387
+		EEM_Base $model,
388
+		$requested_version,
389
+		$writing = false
390
+	) {
391
+		$query_param_for_models = array();
392
+		$context = new RestIncomingQueryParamContext($model, $requested_version, $writing);
393
+		foreach ($inputted_query_params_of_this_type as $query_param_key => $query_param_value) {
394
+			$query_param_meta = new RestIncomingQueryParamMetadata($query_param_key, $query_param_value, $context);
395
+			if ($query_param_meta->getField() instanceof EE_Model_Field_Base) {
396
+				$translated_value = $query_param_meta->determineConditionsQueryParameterValue();
397
+				if ((isset($query_param_for_models[ $query_param_meta->getQueryParamKey() ]) && $query_param_meta->isGmtField())
398
+					|| $translated_value === null
399
+				) {
400
+					// they have already provided a non-gmt field, ignore the gmt one. That's what WP core
401
+					// currently does (they might change it though). See https://core.trac.wordpress.org/ticket/39954
402
+					// OR we couldn't create a translated value from their input
403
+					continue;
404
+				}
405
+				$query_param_for_models[ $query_param_meta->getQueryParamKey() ] = $translated_value;
406
+			} else {
407
+				$nested_query_params = $query_param_meta->determineNestedConditionQueryParameters();
408
+				if ($nested_query_params) {
409
+					$query_param_for_models[ $query_param_meta->getQueryParamKey() ] = $nested_query_params;
410
+				}
411
+			}
412
+		}
413
+		return $query_param_for_models;
414
+	}
415
+
416
+	/**
417
+	 * Mostly checks if the last 4 characters are "_gmt", indicating its a
418
+	 * gmt date field name
419
+	 *
420
+	 * @param string $field_name
421
+	 * @return boolean
422
+	 */
423
+	public static function isGmtDateFieldName($field_name)
424
+	{
425
+		return substr(
426
+			ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey($field_name),
427
+			-4,
428
+			4
429
+		) === '_gmt';
430
+	}
431
+
432
+
433
+	/**
434
+	 * Removes the last "_gmt" part of a field name (and if there is no "_gmt" at the end, leave it alone)
435
+	 *
436
+	 * @param string $field_name
437
+	 * @return string
438
+	 */
439
+	public static function removeGmtFromFieldName($field_name)
440
+	{
441
+		if (! ModelDataTranslator::isGmtDateFieldName($field_name)) {
442
+			return $field_name;
443
+		}
444
+		$query_param_sans_stars = ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey(
445
+			$field_name
446
+		);
447
+		$query_param_sans_gmt_and_sans_stars = substr(
448
+			$query_param_sans_stars,
449
+			0,
450
+			strrpos(
451
+				$field_name,
452
+				'_gmt'
453
+			)
454
+		);
455
+		return str_replace($query_param_sans_stars, $query_param_sans_gmt_and_sans_stars, $field_name);
456
+	}
457
+
458
+
459
+	/**
460
+	 * Takes a field name from the REST API and prepares it for the model querying
461
+	 *
462
+	 * @param string $field_name
463
+	 * @return string
464
+	 */
465
+	public static function prepareFieldNameFromJson($field_name)
466
+	{
467
+		if (ModelDataTranslator::isGmtDateFieldName($field_name)) {
468
+			return ModelDataTranslator::removeGmtFromFieldName($field_name);
469
+		}
470
+		return $field_name;
471
+	}
472
+
473
+
474
+	/**
475
+	 * Takes array of field names from REST API and prepares for models
476
+	 *
477
+	 * @param array $field_names
478
+	 * @return array of field names (possibly include model prefixes)
479
+	 */
480
+	public static function prepareFieldNamesFromJson(array $field_names)
481
+	{
482
+		$new_array = array();
483
+		foreach ($field_names as $key => $field_name) {
484
+			$new_array[ $key ] = ModelDataTranslator::prepareFieldNameFromJson($field_name);
485
+		}
486
+		return $new_array;
487
+	}
488
+
489
+
490
+	/**
491
+	 * Takes array where array keys are field names (possibly with model path prefixes)
492
+	 * from the REST API and prepares them for model querying
493
+	 *
494
+	 * @param array $field_names_as_keys
495
+	 * @return array
496
+	 */
497
+	public static function prepareFieldNamesInArrayKeysFromJson(array $field_names_as_keys)
498
+	{
499
+		$new_array = array();
500
+		foreach ($field_names_as_keys as $field_name => $value) {
501
+			$new_array[ ModelDataTranslator::prepareFieldNameFromJson($field_name) ] = $value;
502
+		}
503
+		return $new_array;
504
+	}
505
+
506
+
507
+	/**
508
+	 * Prepares an array of model query params for use in the REST API
509
+	 *
510
+	 * @param array    $model_query_params
511
+	 * @param EEM_Base $model
512
+	 * @param string   $requested_version  eg "4.8.36". If null is provided, defaults to the latest release of the EE4
513
+	 *                                     REST API
514
+	 * @return array which can be passed into the EE4 REST API when querying a model resource
515
+	 * @throws EE_Error
516
+	 */
517
+	public static function prepareQueryParamsForRestApi(
518
+		array $model_query_params,
519
+		EEM_Base $model,
520
+		$requested_version = null
521
+	) {
522
+		if ($requested_version === null) {
523
+			$requested_version = EED_Core_Rest_Api::latest_rest_api_version();
524
+		}
525
+		$rest_query_params = $model_query_params;
526
+		if (isset($model_query_params[0])) {
527
+			$rest_query_params['where'] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
528
+				$model_query_params[0],
529
+				$model,
530
+				$requested_version
531
+			);
532
+			unset($rest_query_params[0]);
533
+		}
534
+		if (isset($model_query_params['having'])) {
535
+			$rest_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
536
+				$model_query_params['having'],
537
+				$model,
538
+				$requested_version
539
+			);
540
+		}
541
+		return apply_filters(
542
+			'FHEE__EventEspresso\core\libraries\rest_api\Model_Data_Translator__prepare_query_params_for_rest_api',
543
+			$rest_query_params,
544
+			$model_query_params,
545
+			$model,
546
+			$requested_version
547
+		);
548
+	}
549
+
550
+
551
+	/**
552
+	 * Prepares all the sub-conditions query parameters (eg having or where conditions) for use in the rest api
553
+	 *
554
+	 * @param array    $inputted_query_params_of_this_type  eg like the "where" or "having" conditions query params
555
+	 *                                                      @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions
556
+	 * @param EEM_Base $model
557
+	 * @param string   $requested_version                   eg "4.8.36"
558
+	 * @return array ready for use in the rest api query params
559
+	 * @throws EE_Error
560
+	 * @throws ObjectDetectedException if somehow a PHP object were in the query params' values,
561
+	 *                                                      (which would be really unusual)
562
+	 */
563
+	public static function prepareConditionsQueryParamsForRestApi(
564
+		$inputted_query_params_of_this_type,
565
+		EEM_Base $model,
566
+		$requested_version
567
+	) {
568
+		$query_param_for_models = array();
569
+		foreach ($inputted_query_params_of_this_type as $query_param_key => $query_param_value) {
570
+			$field = ModelDataTranslator::deduceFieldFromQueryParam(
571
+				ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey($query_param_key),
572
+				$model
573
+			);
574
+			if ($field instanceof EE_Model_Field_Base) {
575
+				// did they specify an operator?
576
+				if (is_array($query_param_value)) {
577
+					$op = $query_param_value[0];
578
+					$translated_value = array($op);
579
+					if (isset($query_param_value[1])) {
580
+						$value = $query_param_value[1];
581
+						$translated_value[1] = ModelDataTranslator::prepareFieldValuesForJson(
582
+							$field,
583
+							$value,
584
+							$requested_version
585
+						);
586
+					}
587
+				} else {
588
+					$translated_value = ModelDataTranslator::prepareFieldValueForJson(
589
+						$field,
590
+						$query_param_value,
591
+						$requested_version
592
+					);
593
+				}
594
+				$query_param_for_models[ $query_param_key ] = $translated_value;
595
+			} else {
596
+				// so it's not for a field, assume it's a logic query param key
597
+				$query_param_for_models[ $query_param_key ] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
598
+					$query_param_value,
599
+					$model,
600
+					$requested_version
601
+				);
602
+			}
603
+		}
604
+		return $query_param_for_models;
605
+	}
606
+
607
+
608
+	/**
609
+	 * @param $condition_query_param_key
610
+	 * @return string
611
+	 */
612
+	public static function removeStarsAndAnythingAfterFromConditionQueryParamKey($condition_query_param_key)
613
+	{
614
+		$pos_of_star = strpos($condition_query_param_key, '*');
615
+		if ($pos_of_star === false) {
616
+			return $condition_query_param_key;
617
+		} else {
618
+			$condition_query_param_sans_star = substr($condition_query_param_key, 0, $pos_of_star);
619
+			return $condition_query_param_sans_star;
620
+		}
621
+	}
622
+
623
+
624
+	/**
625
+	 * Takes the input parameter and finds the model field that it indicates.
626
+	 *
627
+	 * @param string   $query_param_name like Registration.Transaction.TXN_ID, Event.Datetime.start_time, or REG_ID
628
+	 * @param EEM_Base $model
629
+	 * @return EE_Model_Field_Base
630
+	 * @throws EE_Error
631
+	 */
632
+	public static function deduceFieldFromQueryParam($query_param_name, EEM_Base $model)
633
+	{
634
+		// ok, now proceed with deducing which part is the model's name, and which is the field's name
635
+		// which will help us find the database table and column
636
+		$query_param_parts = explode('.', $query_param_name);
637
+		if (empty($query_param_parts)) {
638
+			throw new EE_Error(
639
+				sprintf(
640
+					__(
641
+						'_extract_column_name is empty when trying to extract column and table name from %s',
642
+						'event_espresso'
643
+					),
644
+					$query_param_name
645
+				)
646
+			);
647
+		}
648
+		$number_of_parts = count($query_param_parts);
649
+		$last_query_param_part = $query_param_parts[ count($query_param_parts) - 1 ];
650
+		if ($number_of_parts === 1) {
651
+			$field_name = $last_query_param_part;
652
+		} else {// $number_of_parts >= 2
653
+			// the last part is the column name, and there are only 2parts. therefore...
654
+			$field_name = $last_query_param_part;
655
+			$model = \EE_Registry::instance()->load_model($query_param_parts[ $number_of_parts - 2 ]);
656
+		}
657
+		try {
658
+			return $model->field_settings_for($field_name, false);
659
+		} catch (EE_Error $e) {
660
+			return null;
661
+		}
662
+	}
663
+
664
+
665
+	/**
666
+	 * Returns true if $data can be easily represented in JSON.
667
+	 * Basically, objects and resources can't be represented in JSON easily.
668
+	 *
669
+	 * @param mixed $data
670
+	 * @return bool
671
+	 */
672
+	protected static function isRepresentableInJson($data)
673
+	{
674
+		return is_scalar($data)
675
+			   || is_array($data)
676
+			   || is_null($data);
677
+	}
678 678
 }
Please login to merge, or discard this patch.
core/libraries/rest_api/controllers/model/Read.php 1 patch
Indentation   +1346 added lines, -1346 removed lines patch added patch discarded remove patch
@@ -40,1350 +40,1350 @@
 block discarded – undo
40 40
 {
41 41
 
42 42
 
43
-    /**
44
-     * @var CalculatedModelFields
45
-     */
46
-    protected $fields_calculator;
47
-
48
-
49
-    /**
50
-     * Read constructor.
51
-     * @param CalculatedModelFields $fields_calculator
52
-     */
53
-    public function __construct(CalculatedModelFields $fields_calculator)
54
-    {
55
-        parent::__construct();
56
-        $this->fields_calculator = $fields_calculator;
57
-    }
58
-
59
-
60
-    /**
61
-     * Handles requests to get all (or a filtered subset) of entities for a particular model
62
-     *
63
-     * @param WP_REST_Request $request
64
-     * @param string $version
65
-     * @param string $model_name
66
-     * @return WP_REST_Response|WP_Error
67
-     * @throws InvalidArgumentException
68
-     * @throws InvalidDataTypeException
69
-     * @throws InvalidInterfaceException
70
-     */
71
-    public static function handleRequestGetAll(WP_REST_Request $request, $version, $model_name)
72
-    {
73
-        $controller = LoaderFactory::getLoader()->getNew('EventEspresso\core\libraries\rest_api\controllers\model\Read');
74
-        try {
75
-            $controller->setRequestedVersion($version);
76
-            if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
77
-                return $controller->sendResponse(
78
-                    new WP_Error(
79
-                        'endpoint_parsing_error',
80
-                        sprintf(
81
-                            __(
82
-                                'There is no model for endpoint %s. Please contact event espresso support',
83
-                                'event_espresso'
84
-                            ),
85
-                            $model_name
86
-                        )
87
-                    )
88
-                );
89
-            }
90
-            return $controller->sendResponse(
91
-                $controller->getEntitiesFromModel(
92
-                    $controller->getModelVersionInfo()->loadModel($model_name),
93
-                    $request
94
-                )
95
-            );
96
-        } catch (Exception $e) {
97
-            return $controller->sendResponse($e);
98
-        }
99
-    }
100
-
101
-
102
-    /**
103
-     * Prepares and returns schema for any OPTIONS request.
104
-     *
105
-     * @param string $version The API endpoint version being used.
106
-     * @param string $model_name Something like `Event` or `Registration`
107
-     * @return array
108
-     * @throws InvalidArgumentException
109
-     * @throws InvalidDataTypeException
110
-     * @throws InvalidInterfaceException
111
-     */
112
-    public static function handleSchemaRequest($version, $model_name)
113
-    {
114
-        $controller = LoaderFactory::getLoader()->getNew('EventEspresso\core\libraries\rest_api\controllers\model\Read');
115
-        try {
116
-            $controller->setRequestedVersion($version);
117
-            if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
118
-                return array();
119
-            }
120
-            // get the model for this version
121
-            $model = $controller->getModelVersionInfo()->loadModel($model_name);
122
-            $model_schema = new JsonModelSchema($model, LoaderFactory::getLoader()->getShared('EventEspresso\core\libraries\rest_api\CalculatedModelFields'));
123
-            return $model_schema->getModelSchemaForRelations(
124
-                $controller->getModelVersionInfo()->relationSettings($model),
125
-                $controller->customizeSchemaForRestResponse(
126
-                    $model,
127
-                    $model_schema->getModelSchemaForFields(
128
-                        $controller->getModelVersionInfo()->fieldsOnModelInThisVersion($model),
129
-                        $model_schema->getInitialSchemaStructure()
130
-                    )
131
-                )
132
-            );
133
-        } catch (Exception $e) {
134
-            return array();
135
-        }
136
-    }
137
-
138
-
139
-    /**
140
-     * This loops through each field in the given schema for the model and does the following:
141
-     * - add any extra fields that are REST API specific and related to existing fields.
142
-     * - transform default values into the correct format for a REST API response.
143
-     *
144
-     * @param EEM_Base $model
145
-     * @param array    $schema
146
-     * @return array  The final schema.
147
-     */
148
-    protected function customizeSchemaForRestResponse(EEM_Base $model, array $schema)
149
-    {
150
-        foreach ($this->getModelVersionInfo()->fieldsOnModelInThisVersion($model) as $field_name => $field) {
151
-            $schema = $this->translateDefaultsForRestResponse(
152
-                $field_name,
153
-                $field,
154
-                $this->maybeAddExtraFieldsToSchema($field_name, $field, $schema)
155
-            );
156
-        }
157
-        return $schema;
158
-    }
159
-
160
-
161
-    /**
162
-     * This is used to ensure that the 'default' value set in the schema response is formatted correctly for the REST
163
-     * response.
164
-     *
165
-     * @param                      $field_name
166
-     * @param EE_Model_Field_Base  $field
167
-     * @param array                $schema
168
-     * @return array
169
-     * @throws ObjectDetectedException if a default value has a PHP object, which should never do (and if we
170
-     * did, let's know about it ASAP, so let the exception bubble up)
171
-     */
172
-    protected function translateDefaultsForRestResponse($field_name, EE_Model_Field_Base $field, array $schema)
173
-    {
174
-        if (isset($schema['properties'][ $field_name ]['default'])) {
175
-            if (is_array($schema['properties'][ $field_name ]['default'])) {
176
-                foreach ($schema['properties'][ $field_name ]['default'] as $default_key => $default_value) {
177
-                    if ($default_key === 'raw') {
178
-                        $schema['properties'][ $field_name ]['default'][ $default_key ] =
179
-                            ModelDataTranslator::prepareFieldValueForJson(
180
-                                $field,
181
-                                $default_value,
182
-                                $this->getModelVersionInfo()->requestedVersion()
183
-                            );
184
-                    }
185
-                }
186
-            } else {
187
-                $schema['properties'][ $field_name ]['default'] = ModelDataTranslator::prepareFieldValueForJson(
188
-                    $field,
189
-                    $schema['properties'][ $field_name ]['default'],
190
-                    $this->getModelVersionInfo()->requestedVersion()
191
-                );
192
-            }
193
-        }
194
-        return $schema;
195
-    }
196
-
197
-
198
-    /**
199
-     * Adds additional fields to the schema
200
-     * The REST API returns a GMT value field for each datetime field in the resource.  Thus the description about this
201
-     * needs to be added to the schema.
202
-     *
203
-     * @param                      $field_name
204
-     * @param EE_Model_Field_Base  $field
205
-     * @param array                $schema
206
-     * @return array
207
-     */
208
-    protected function maybeAddExtraFieldsToSchema($field_name, EE_Model_Field_Base $field, array $schema)
209
-    {
210
-        if ($field instanceof EE_Datetime_Field) {
211
-            $schema['properties'][ $field_name . '_gmt' ] = $field->getSchema();
212
-            // modify the description
213
-            $schema['properties'][ $field_name . '_gmt' ]['description'] = sprintf(
214
-                esc_html__('%s - the value for this field is in GMT.', 'event_espresso'),
215
-                wp_specialchars_decode($field->get_nicename(), ENT_QUOTES)
216
-            );
217
-        }
218
-        return $schema;
219
-    }
220
-
221
-
222
-    /**
223
-     * Used to figure out the route from the request when a `WP_REST_Request` object is not available
224
-     *
225
-     * @return string
226
-     */
227
-    protected function getRouteFromRequest()
228
-    {
229
-        if (isset($GLOBALS['wp'])
230
-            && $GLOBALS['wp'] instanceof \WP
231
-            && isset($GLOBALS['wp']->query_vars['rest_route'])
232
-        ) {
233
-            return $GLOBALS['wp']->query_vars['rest_route'];
234
-        } else {
235
-            return isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '/';
236
-        }
237
-    }
238
-
239
-
240
-    /**
241
-     * Gets a single entity related to the model indicated in the path and its id
242
-     *
243
-     * @param WP_REST_Request $request
244
-     * @param string $version
245
-     * @param string $model_name
246
-     * @return WP_REST_Response|WP_Error
247
-     * @throws InvalidDataTypeException
248
-     * @throws InvalidInterfaceException
249
-     * @throws InvalidArgumentException
250
-     */
251
-    public static function handleRequestGetOne(WP_REST_Request $request, $version, $model_name)
252
-    {
253
-        $controller = LoaderFactory::getLoader()->getNew('EventEspresso\core\libraries\rest_api\controllers\model\Read');
254
-        try {
255
-            $controller->setRequestedVersion($version);
256
-            if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
257
-                return $controller->sendResponse(
258
-                    new WP_Error(
259
-                        'endpoint_parsing_error',
260
-                        sprintf(
261
-                            __(
262
-                                'There is no model for endpoint %s. Please contact event espresso support',
263
-                                'event_espresso'
264
-                            ),
265
-                            $model_name
266
-                        )
267
-                    )
268
-                );
269
-            }
270
-            return $controller->sendResponse(
271
-                $controller->getEntityFromModel(
272
-                    $controller->getModelVersionInfo()->loadModel($model_name),
273
-                    $request
274
-                )
275
-            );
276
-        } catch (Exception $e) {
277
-            return $controller->sendResponse($e);
278
-        }
279
-    }
280
-
281
-
282
-    /**
283
-     * Gets all the related entities (or if its a belongs-to relation just the one)
284
-     * to the item with the given id
285
-     *
286
-     * @param WP_REST_Request $request
287
-     * @param string $version
288
-     * @param string $model_name
289
-     * @param string $related_model_name
290
-     * @return WP_REST_Response|WP_Error
291
-     * @throws InvalidDataTypeException
292
-     * @throws InvalidInterfaceException
293
-     * @throws InvalidArgumentException
294
-     */
295
-    public static function handleRequestGetRelated(
296
-        WP_REST_Request $request,
297
-        $version,
298
-        $model_name,
299
-        $related_model_name
300
-    ) {
301
-        $controller = LoaderFactory::getLoader()->getNew('EventEspresso\core\libraries\rest_api\controllers\model\Read');
302
-        try {
303
-            $controller->setRequestedVersion($version);
304
-            if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
305
-                return $controller->sendResponse(
306
-                    new WP_Error(
307
-                        'endpoint_parsing_error',
308
-                        sprintf(
309
-                            __(
310
-                                'There is no model for endpoint %s. Please contact event espresso support',
311
-                                'event_espresso'
312
-                            ),
313
-                            $model_name
314
-                        )
315
-                    )
316
-                );
317
-            }
318
-            $main_model = $controller->getModelVersionInfo()->loadModel($model_name);
319
-            if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($related_model_name)) {
320
-                return $controller->sendResponse(
321
-                    new WP_Error(
322
-                        'endpoint_parsing_error',
323
-                        sprintf(
324
-                            __(
325
-                                'There is no model for endpoint %s. Please contact event espresso support',
326
-                                'event_espresso'
327
-                            ),
328
-                            $related_model_name
329
-                        )
330
-                    )
331
-                );
332
-            }
333
-            return $controller->sendResponse(
334
-                $controller->getEntitiesFromRelation(
335
-                    $request->get_param('id'),
336
-                    $main_model->related_settings_for($related_model_name),
337
-                    $request
338
-                )
339
-            );
340
-        } catch (Exception $e) {
341
-            return $controller->sendResponse($e);
342
-        }
343
-    }
344
-
345
-
346
-    /**
347
-     * Gets a collection for the given model and filters
348
-     *
349
-     * @param EEM_Base        $model
350
-     * @param WP_REST_Request $request
351
-     * @return array|WP_Error
352
-     */
353
-    public function getEntitiesFromModel($model, $request)
354
-    {
355
-        $query_params = $this->createModelQueryParams($model, $request->get_params());
356
-        if (! Capabilities::currentUserHasPartialAccessTo($model, $query_params['caps'])) {
357
-            $model_name_plural = EEH_Inflector::pluralize_and_lower($model->get_this_model_name());
358
-            return new WP_Error(
359
-                sprintf('rest_%s_cannot_list', $model_name_plural),
360
-                sprintf(
361
-                    __('Sorry, you are not allowed to list %1$s. Missing permissions: %2$s', 'event_espresso'),
362
-                    $model_name_plural,
363
-                    Capabilities::getMissingPermissionsString($model, $query_params['caps'])
364
-                ),
365
-                array('status' => 403)
366
-            );
367
-        }
368
-        if (! $request->get_header('no_rest_headers')) {
369
-            $this->setHeadersFromQueryParams($model, $query_params);
370
-        }
371
-        /** @type array $results */
372
-        $results = $model->get_all_wpdb_results($query_params);
373
-        $nice_results = array();
374
-        foreach ($results as $result) {
375
-            $nice_results[] = $this->createEntityFromWpdbResult(
376
-                $model,
377
-                $result,
378
-                $request
379
-            );
380
-        }
381
-        return $nice_results;
382
-    }
383
-
384
-
385
-    /**
386
-     * Gets the collection for given relation object
387
-     * The same as Read::get_entities_from_model(), except if the relation
388
-     * is a HABTM relation, in which case it merges any non-foreign-key fields from
389
-     * the join-model-object into the results
390
-     *
391
-     * @param array                   $primary_model_query_params query params for finding the item from which
392
-     *                                                            relations will be based
393
-     * @param \EE_Model_Relation_Base $relation
394
-     * @param WP_REST_Request         $request
395
-     * @return WP_Error|array
396
-     * @throws RestException
397
-     */
398
-    protected function getEntitiesFromRelationUsingModelQueryParams($primary_model_query_params, $relation, $request)
399
-    {
400
-        $context = $this->validateContext($request->get_param('caps'));
401
-        $model = $relation->get_this_model();
402
-        $related_model = $relation->get_other_model();
403
-        if (! isset($primary_model_query_params[0])) {
404
-            $primary_model_query_params[0] = array();
405
-        }
406
-        // check if they can access the 1st model object
407
-        $primary_model_query_params = array(
408
-            0       => $primary_model_query_params[0],
409
-            'limit' => 1,
410
-        );
411
-        if ($model instanceof \EEM_Soft_Delete_Base) {
412
-            $primary_model_query_params = $model->alter_query_params_so_deleted_and_undeleted_items_included(
413
-                $primary_model_query_params
414
-            );
415
-        }
416
-        $restricted_query_params = $primary_model_query_params;
417
-        $restricted_query_params['caps'] = $context;
418
-        $this->setDebugInfo('main model query params', $restricted_query_params);
419
-        $this->setDebugInfo('missing caps', Capabilities::getMissingPermissionsString($related_model, $context));
420
-        if (! (
421
-            Capabilities::currentUserHasPartialAccessTo($related_model, $context)
422
-            && $model->exists($restricted_query_params)
423
-        )
424
-        ) {
425
-            if ($relation instanceof EE_Belongs_To_Relation) {
426
-                $related_model_name_maybe_plural = strtolower($related_model->get_this_model_name());
427
-            } else {
428
-                $related_model_name_maybe_plural = EEH_Inflector::pluralize_and_lower(
429
-                    $related_model->get_this_model_name()
430
-                );
431
-            }
432
-            return new WP_Error(
433
-                sprintf('rest_%s_cannot_list', $related_model_name_maybe_plural),
434
-                sprintf(
435
-                    __(
436
-                        'Sorry, you are not allowed to list %1$s related to %2$s. Missing permissions: %3$s',
437
-                        'event_espresso'
438
-                    ),
439
-                    $related_model_name_maybe_plural,
440
-                    $relation->get_this_model()->get_this_model_name(),
441
-                    implode(
442
-                        ',',
443
-                        array_keys(
444
-                            Capabilities::getMissingPermissions($related_model, $context)
445
-                        )
446
-                    )
447
-                ),
448
-                array('status' => 403)
449
-            );
450
-        }
451
-        $query_params = $this->createModelQueryParams($relation->get_other_model(), $request->get_params());
452
-        foreach ($primary_model_query_params[0] as $where_condition_key => $where_condition_value) {
453
-            $query_params[0][ $relation->get_this_model()->get_this_model_name()
454
-                              . '.'
455
-                              . $where_condition_key ] = $where_condition_value;
456
-        }
457
-        $query_params['default_where_conditions'] = 'none';
458
-        $query_params['caps'] = $context;
459
-        if (! $request->get_header('no_rest_headers')) {
460
-            $this->setHeadersFromQueryParams($relation->get_other_model(), $query_params);
461
-        }
462
-        /** @type array $results */
463
-        $results = $relation->get_other_model()->get_all_wpdb_results($query_params);
464
-        $nice_results = array();
465
-        foreach ($results as $result) {
466
-            $nice_result = $this->createEntityFromWpdbResult(
467
-                $relation->get_other_model(),
468
-                $result,
469
-                $request
470
-            );
471
-            if ($relation instanceof \EE_HABTM_Relation) {
472
-                // put the unusual stuff (properties from the HABTM relation) first, and make sure
473
-                // if there are conflicts we prefer the properties from the main model
474
-                $join_model_result = $this->createEntityFromWpdbResult(
475
-                    $relation->get_join_model(),
476
-                    $result,
477
-                    $request
478
-                );
479
-                $joined_result = array_merge($nice_result, $join_model_result);
480
-                // but keep the meta stuff from the main model
481
-                if (isset($nice_result['meta'])) {
482
-                    $joined_result['meta'] = $nice_result['meta'];
483
-                }
484
-                $nice_result = $joined_result;
485
-            }
486
-            $nice_results[] = $nice_result;
487
-        }
488
-        if ($relation instanceof EE_Belongs_To_Relation) {
489
-            return array_shift($nice_results);
490
-        } else {
491
-            return $nice_results;
492
-        }
493
-    }
494
-
495
-
496
-    /**
497
-     * Gets the collection for given relation object
498
-     * The same as Read::get_entities_from_model(), except if the relation
499
-     * is a HABTM relation, in which case it merges any non-foreign-key fields from
500
-     * the join-model-object into the results
501
-     *
502
-     * @param string                  $id the ID of the thing we are fetching related stuff from
503
-     * @param \EE_Model_Relation_Base $relation
504
-     * @param WP_REST_Request         $request
505
-     * @return array|WP_Error
506
-     * @throws EE_Error
507
-     */
508
-    public function getEntitiesFromRelation($id, $relation, $request)
509
-    {
510
-        if (! $relation->get_this_model()->has_primary_key_field()) {
511
-            throw new EE_Error(
512
-                sprintf(
513
-                    __(
514
-                    // @codingStandardsIgnoreStart
515
-                        'Read::get_entities_from_relation should only be called from a model with a primary key, it was called from %1$s',
516
-                        // @codingStandardsIgnoreEnd
517
-                        'event_espresso'
518
-                    ),
519
-                    $relation->get_this_model()->get_this_model_name()
520
-                )
521
-            );
522
-        }
523
-        return $this->getEntitiesFromRelationUsingModelQueryParams(
524
-            array(
525
-                array(
526
-                    $relation->get_this_model()->primary_key_name() => $id,
527
-                ),
528
-            ),
529
-            $relation,
530
-            $request
531
-        );
532
-    }
533
-
534
-
535
-    /**
536
-     * Sets the headers that are based on the model and query params,
537
-     * like the total records. This should only be called on the original request
538
-     * from the client, not on subsequent internal
539
-     *
540
-     * @param EEM_Base $model
541
-     * @param array    $query_params
542
-     * @return void
543
-     */
544
-    protected function setHeadersFromQueryParams($model, $query_params)
545
-    {
546
-        $this->setDebugInfo('model query params', $query_params);
547
-        $this->setDebugInfo(
548
-            'missing caps',
549
-            Capabilities::getMissingPermissionsString($model, $query_params['caps'])
550
-        );
551
-        // normally the limit to a 2-part array, where the 2nd item is the limit
552
-        if (! isset($query_params['limit'])) {
553
-            $query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
554
-        }
555
-        if (is_array($query_params['limit'])) {
556
-            $limit_parts = $query_params['limit'];
557
-        } else {
558
-            $limit_parts = explode(',', $query_params['limit']);
559
-            if (count($limit_parts) == 1) {
560
-                $limit_parts = array(0, $limit_parts[0]);
561
-            }
562
-        }
563
-        // remove the group by and having parts of the query, as those will
564
-        // make the sql query return an array of values, instead of just a single value
565
-        unset($query_params['group_by'], $query_params['having'], $query_params['limit']);
566
-        $count = $model->count($query_params, null, true);
567
-        $pages = $count / $limit_parts[1];
568
-        $this->setResponseHeader('Total', $count, false);
569
-        $this->setResponseHeader('PageSize', $limit_parts[1], false);
570
-        $this->setResponseHeader('TotalPages', ceil($pages), false);
571
-    }
572
-
573
-
574
-    /**
575
-     * Changes database results into REST API entities
576
-     *
577
-     * @param EEM_Base        $model
578
-     * @param array           $db_row     like results from $wpdb->get_results()
579
-     * @param WP_REST_Request $rest_request
580
-     * @param string          $deprecated no longer used
581
-     * @return array ready for being converted into json for sending to client
582
-     */
583
-    public function createEntityFromWpdbResult($model, $db_row, $rest_request, $deprecated = null)
584
-    {
585
-        if (! $rest_request instanceof WP_REST_Request) {
586
-            // ok so this was called in the old style, where the 3rd arg was
587
-            // $include, and the 4th arg was $context
588
-            // now setup the request just to avoid fatal errors, although we won't be able
589
-            // to truly make use of it because it's kinda devoid of info
590
-            $rest_request = new WP_REST_Request();
591
-            $rest_request->set_param('include', $rest_request);
592
-            $rest_request->set_param('caps', $deprecated);
593
-        }
594
-        if ($rest_request->get_param('caps') == null) {
595
-            $rest_request->set_param('caps', EEM_Base::caps_read);
596
-        }
597
-        $entity_array = $this->createBareEntityFromWpdbResults($model, $db_row);
598
-        $entity_array = $this->addExtraFields($model, $db_row, $entity_array);
599
-        $entity_array['_links'] = $this->getEntityLinks($model, $db_row, $entity_array);
600
-        $entity_array['_calculated_fields'] = $this->getEntityCalculations($model, $db_row, $rest_request);
601
-        $entity_array = apply_filters(
602
-            'FHEE__Read__create_entity_from_wpdb_results__entity_before_including_requested_models',
603
-            $entity_array,
604
-            $model,
605
-            $rest_request->get_param('caps'),
606
-            $rest_request,
607
-            $this
608
-        );
609
-        $entity_array = $this->includeRequestedModels($model, $rest_request, $entity_array, $db_row);
610
-        $entity_array = apply_filters(
611
-            'FHEE__Read__create_entity_from_wpdb_results__entity_before_inaccessible_field_removal',
612
-            $entity_array,
613
-            $model,
614
-            $rest_request->get_param('caps'),
615
-            $rest_request,
616
-            $this
617
-        );
618
-        $result_without_inaccessible_fields = Capabilities::filterOutInaccessibleEntityFields(
619
-            $entity_array,
620
-            $model,
621
-            $rest_request->get_param('caps'),
622
-            $this->getModelVersionInfo(),
623
-            $model->get_index_primary_key_string(
624
-                $model->deduce_fields_n_values_from_cols_n_values($db_row)
625
-            )
626
-        );
627
-        $this->setDebugInfo(
628
-            'inaccessible fields',
629
-            array_keys(array_diff_key($entity_array, $result_without_inaccessible_fields))
630
-        );
631
-        return apply_filters(
632
-            'FHEE__Read__create_entity_from_wpdb_results__entity_return',
633
-            $result_without_inaccessible_fields,
634
-            $model,
635
-            $rest_request->get_param('caps')
636
-        );
637
-    }
638
-
639
-
640
-    /**
641
-     * Creates a REST entity array (JSON object we're going to return in the response, but
642
-     * for now still a PHP array, but soon enough we'll call json_encode on it, don't worry),
643
-     * from $wpdb->get_row( $sql, ARRAY_A)
644
-     *
645
-     * @param EEM_Base $model
646
-     * @param array    $db_row
647
-     * @return array entity mostly ready for converting to JSON and sending in the response
648
-     */
649
-    protected function createBareEntityFromWpdbResults(EEM_Base $model, $db_row)
650
-    {
651
-        $result = $model->deduce_fields_n_values_from_cols_n_values($db_row);
652
-        $result = array_intersect_key(
653
-            $result,
654
-            $this->getModelVersionInfo()->fieldsOnModelInThisVersion($model)
655
-        );
656
-        // if this is a CPT, we need to set the global $post to it,
657
-        // otherwise shortcodes etc won't work properly while rendering it
658
-        if ($model instanceof \EEM_CPT_Base) {
659
-            $do_chevy_shuffle = true;
660
-        } else {
661
-            $do_chevy_shuffle = false;
662
-        }
663
-        if ($do_chevy_shuffle) {
664
-            global $post;
665
-            $old_post = $post;
666
-            $post = get_post($result[ $model->primary_key_name() ]);
667
-            if (! $post instanceof \WP_Post) {
668
-                // well that's weird, because $result is what we JUST fetched from the database
669
-                throw new RestException(
670
-                    'error_fetching_post_from_database_results',
671
-                    esc_html__(
672
-                        'An item was retrieved from the database but it\'s not a WP_Post like it should be.',
673
-                        'event_espresso'
674
-                    )
675
-                );
676
-            }
677
-            $model_object_classname = 'EE_' . $model->get_this_model_name();
678
-            $post->{$model_object_classname} = \EE_Registry::instance()->load_class(
679
-                $model_object_classname,
680
-                $result,
681
-                false,
682
-                false
683
-            );
684
-        }
685
-        foreach ($result as $field_name => $field_value) {
686
-            $field_obj = $model->field_settings_for($field_name);
687
-            if ($this->isSubclassOfOne($field_obj, $this->getModelVersionInfo()->fieldsIgnored())) {
688
-                unset($result[ $field_name ]);
689
-            } elseif ($this->isSubclassOfOne(
690
-                $field_obj,
691
-                $this->getModelVersionInfo()->fieldsThatHaveRenderedFormat()
692
-            )
693
-            ) {
694
-                $result[ $field_name ] = array(
695
-                    'raw'      => $this->prepareFieldObjValueForJson($field_obj, $field_value),
696
-                    'rendered' => $this->prepareFieldObjValueForJson($field_obj, $field_value, 'pretty'),
697
-                );
698
-            } elseif ($this->isSubclassOfOne(
699
-                $field_obj,
700
-                $this->getModelVersionInfo()->fieldsThatHavePrettyFormat()
701
-            )
702
-            ) {
703
-                $result[ $field_name ] = array(
704
-                    'raw'    => $this->prepareFieldObjValueForJson($field_obj, $field_value),
705
-                    'pretty' => $this->prepareFieldObjValueForJson($field_obj, $field_value, 'pretty'),
706
-                );
707
-            } elseif ($field_obj instanceof \EE_Datetime_Field) {
708
-                $field_value = $field_obj->prepare_for_set_from_db($field_value);
709
-                // if the value is null, but we're not supposed to permit null, then set to the field's default
710
-                if (is_null($field_value)) {
711
-                    $field_value = $field_obj->getDefaultDateTimeObj();
712
-                }
713
-                if (is_null($field_value)) {
714
-                    $gmt_date = $local_date = ModelDataTranslator::prepareFieldValuesForJson(
715
-                        $field_obj,
716
-                        $field_value,
717
-                        $this->getModelVersionInfo()->requestedVersion()
718
-                    );
719
-                } else {
720
-                    $timezone = $field_value->getTimezone();
721
-                    EEH_DTT_Helper::setTimezone($field_value, new DateTimeZone('UTC'));
722
-                    $gmt_date = ModelDataTranslator::prepareFieldValuesForJson(
723
-                        $field_obj,
724
-                        $field_value,
725
-                        $this->getModelVersionInfo()->requestedVersion()
726
-                    );
727
-                    EEH_DTT_Helper::setTimezone($field_value, $timezone);
728
-                    $local_date = ModelDataTranslator::prepareFieldValuesForJson(
729
-                        $field_obj,
730
-                        $field_value,
731
-                        $this->getModelVersionInfo()->requestedVersion()
732
-                    );
733
-                }
734
-                $result[ $field_name . '_gmt' ] = $gmt_date;
735
-                $result[ $field_name ] = $local_date;
736
-            } else {
737
-                $result[ $field_name ] = $this->prepareFieldObjValueForJson($field_obj, $field_value);
738
-            }
739
-        }
740
-        if ($do_chevy_shuffle) {
741
-            $post = $old_post;
742
-        }
743
-        return $result;
744
-    }
745
-
746
-
747
-    /**
748
-     * Takes a value all the way from the DB representation, to the model object's representation, to the
749
-     * user-facing PHP representation, to the REST API representation. (Assumes you've already taken from the DB
750
-     * representation using $field_obj->prepare_for_set_from_db())
751
-     *
752
-     * @param EE_Model_Field_Base $field_obj
753
-     * @param mixed               $value  as it's stored on a model object
754
-     * @param string              $format valid values are 'normal' (default), 'pretty', 'datetime_obj'
755
-     * @return mixed
756
-     * @throws ObjectDetectedException if $value contains a PHP object
757
-     */
758
-    protected function prepareFieldObjValueForJson(EE_Model_Field_Base $field_obj, $value, $format = 'normal')
759
-    {
760
-        $value = $field_obj->prepare_for_set_from_db($value);
761
-        switch ($format) {
762
-            case 'pretty':
763
-                $value = $field_obj->prepare_for_pretty_echoing($value);
764
-                break;
765
-            case 'normal':
766
-            default:
767
-                $value = $field_obj->prepare_for_get($value);
768
-                break;
769
-        }
770
-        return ModelDataTranslator::prepareFieldValuesForJson(
771
-            $field_obj,
772
-            $value,
773
-            $this->getModelVersionInfo()->requestedVersion()
774
-        );
775
-    }
776
-
777
-
778
-    /**
779
-     * Adds a few extra fields to the entity response
780
-     *
781
-     * @param EEM_Base $model
782
-     * @param array    $db_row
783
-     * @param array    $entity_array
784
-     * @return array modified entity
785
-     */
786
-    protected function addExtraFields(EEM_Base $model, $db_row, $entity_array)
787
-    {
788
-        if ($model instanceof EEM_CPT_Base) {
789
-            $entity_array['link'] = get_permalink($db_row[ $model->get_primary_key_field()->get_qualified_column() ]);
790
-        }
791
-        return $entity_array;
792
-    }
793
-
794
-
795
-    /**
796
-     * Gets links we want to add to the response
797
-     *
798
-     * @global \WP_REST_Server $wp_rest_server
799
-     * @param EEM_Base         $model
800
-     * @param array            $db_row
801
-     * @param array            $entity_array
802
-     * @return array the _links item in the entity
803
-     */
804
-    protected function getEntityLinks($model, $db_row, $entity_array)
805
-    {
806
-        // add basic links
807
-        $links = array();
808
-        if ($model->has_primary_key_field()) {
809
-            $links['self'] = array(
810
-                array(
811
-                    'href' => $this->getVersionedLinkTo(
812
-                        EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
813
-                        . '/'
814
-                        . $entity_array[ $model->primary_key_name() ]
815
-                    ),
816
-                ),
817
-            );
818
-        }
819
-        $links['collection'] = array(
820
-            array(
821
-                'href' => $this->getVersionedLinkTo(
822
-                    EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
823
-                ),
824
-            ),
825
-        );
826
-        // add links to related models
827
-        if ($model->has_primary_key_field()) {
828
-            foreach ($this->getModelVersionInfo()->relationSettings($model) as $relation_name => $relation_obj) {
829
-                $related_model_part = Read::getRelatedEntityName($relation_name, $relation_obj);
830
-                $links[ EED_Core_Rest_Api::ee_api_link_namespace . $related_model_part ] = array(
831
-                    array(
832
-                        'href'   => $this->getVersionedLinkTo(
833
-                            EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
834
-                            . '/'
835
-                            . $entity_array[ $model->primary_key_name() ]
836
-                            . '/'
837
-                            . $related_model_part
838
-                        ),
839
-                        'single' => $relation_obj instanceof EE_Belongs_To_Relation ? true : false,
840
-                    ),
841
-                );
842
-            }
843
-        }
844
-        return $links;
845
-    }
846
-
847
-
848
-    /**
849
-     * Adds the included models indicated in the request to the entity provided
850
-     *
851
-     * @param EEM_Base        $model
852
-     * @param WP_REST_Request $rest_request
853
-     * @param array           $entity_array
854
-     * @param array           $db_row
855
-     * @return array the modified entity
856
-     */
857
-    protected function includeRequestedModels(
858
-        EEM_Base $model,
859
-        WP_REST_Request $rest_request,
860
-        $entity_array,
861
-        $db_row = array()
862
-    ) {
863
-        // if $db_row not included, hope the entity array has what we need
864
-        if (! $db_row) {
865
-            $db_row = $entity_array;
866
-        }
867
-        $includes_for_this_model = $this->explodeAndGetItemsPrefixedWith($rest_request->get_param('include'), '');
868
-        $includes_for_this_model = $this->removeModelNamesFromArray($includes_for_this_model);
869
-        // if they passed in * or didn't specify any includes, return everything
870
-        if (! in_array('*', $includes_for_this_model)
871
-            && ! empty($includes_for_this_model)
872
-        ) {
873
-            if ($model->has_primary_key_field()) {
874
-                // always include the primary key. ya just gotta know that at least
875
-                $includes_for_this_model[] = $model->primary_key_name();
876
-            }
877
-            if ($this->explodeAndGetItemsPrefixedWith($rest_request->get_param('calculate'), '')) {
878
-                $includes_for_this_model[] = '_calculated_fields';
879
-            }
880
-            $entity_array = array_intersect_key($entity_array, array_flip($includes_for_this_model));
881
-        }
882
-        $relation_settings = $this->getModelVersionInfo()->relationSettings($model);
883
-        foreach ($relation_settings as $relation_name => $relation_obj) {
884
-            $related_fields_to_include = $this->explodeAndGetItemsPrefixedWith(
885
-                $rest_request->get_param('include'),
886
-                $relation_name
887
-            );
888
-            $related_fields_to_calculate = $this->explodeAndGetItemsPrefixedWith(
889
-                $rest_request->get_param('calculate'),
890
-                $relation_name
891
-            );
892
-            // did they specify they wanted to include a related model, or
893
-            // specific fields from a related model?
894
-            // or did they specify to calculate a field from a related model?
895
-            if ($related_fields_to_include || $related_fields_to_calculate) {
896
-                // if so, we should include at least some part of the related model
897
-                $pretend_related_request = new WP_REST_Request();
898
-                $pretend_related_request->set_query_params(
899
-                    array(
900
-                        'caps'      => $rest_request->get_param('caps'),
901
-                        'include'   => $related_fields_to_include,
902
-                        'calculate' => $related_fields_to_calculate,
903
-                    )
904
-                );
905
-                $pretend_related_request->add_header('no_rest_headers', true);
906
-                $primary_model_query_params = $model->alter_query_params_to_restrict_by_ID(
907
-                    $model->get_index_primary_key_string(
908
-                        $model->deduce_fields_n_values_from_cols_n_values($db_row)
909
-                    )
910
-                );
911
-                $related_results = $this->getEntitiesFromRelationUsingModelQueryParams(
912
-                    $primary_model_query_params,
913
-                    $relation_obj,
914
-                    $pretend_related_request
915
-                );
916
-                $entity_array[ Read::getRelatedEntityName($relation_name, $relation_obj) ] = $related_results
917
-                                                                                             instanceof
918
-                                                                                             WP_Error
919
-                    ? null
920
-                    : $related_results;
921
-            }
922
-        }
923
-        return $entity_array;
924
-    }
925
-
926
-
927
-    /**
928
-     * Returns a new array with all the names of models removed. Eg
929
-     * array( 'Event', 'Datetime.*', 'foobar' ) would become array( 'Datetime.*', 'foobar' )
930
-     *
931
-     * @param array $arr
932
-     * @return array
933
-     */
934
-    private function removeModelNamesFromArray($arr)
935
-    {
936
-        return array_diff($arr, array_keys(EE_Registry::instance()->non_abstract_db_models));
937
-    }
938
-
939
-
940
-    /**
941
-     * Gets the calculated fields for the response
942
-     *
943
-     * @param EEM_Base        $model
944
-     * @param array           $wpdb_row
945
-     * @param WP_REST_Request $rest_request
946
-     * @return \stdClass the _calculations item in the entity
947
-     * @throws ObjectDetectedException if a default value has a PHP object, which should never do (and if we
948
-     * did, let's know about it ASAP, so let the exception bubble up)
949
-     */
950
-    protected function getEntityCalculations($model, $wpdb_row, $rest_request)
951
-    {
952
-        $calculated_fields = $this->explodeAndGetItemsPrefixedWith(
953
-            $rest_request->get_param('calculate'),
954
-            ''
955
-        );
956
-        // note: setting calculate=* doesn't do anything
957
-        $calculated_fields_to_return = new \stdClass();
958
-        foreach ($calculated_fields as $field_to_calculate) {
959
-            try {
960
-                $calculated_fields_to_return->$field_to_calculate = ModelDataTranslator::prepareFieldValueForJson(
961
-                    null,
962
-                    $this->fields_calculator->retrieveCalculatedFieldValue(
963
-                        $model,
964
-                        $field_to_calculate,
965
-                        $wpdb_row,
966
-                        $rest_request,
967
-                        $this
968
-                    ),
969
-                    $this->getModelVersionInfo()->requestedVersion()
970
-                );
971
-            } catch (RestException $e) {
972
-                // if we don't have permission to read it, just leave it out. but let devs know about the problem
973
-                $this->setResponseHeader(
974
-                    'Notices-Field-Calculation-Errors['
975
-                    . $e->getStringCode()
976
-                    . ']['
977
-                    . $model->get_this_model_name()
978
-                    . ']['
979
-                    . $field_to_calculate
980
-                    . ']',
981
-                    $e->getMessage(),
982
-                    true
983
-                );
984
-            }
985
-        }
986
-        return $calculated_fields_to_return;
987
-    }
988
-
989
-
990
-    /**
991
-     * Gets the full URL to the resource, taking the requested version into account
992
-     *
993
-     * @param string $link_part_after_version_and_slash eg "events/10/datetimes"
994
-     * @return string url eg "http://mysite.com/wp-json/ee/v4.6/events/10/datetimes"
995
-     */
996
-    public function getVersionedLinkTo($link_part_after_version_and_slash)
997
-    {
998
-        return rest_url(
999
-            EED_Core_Rest_Api::get_versioned_route_to(
1000
-                $link_part_after_version_and_slash,
1001
-                $this->getModelVersionInfo()->requestedVersion()
1002
-            )
1003
-        );
1004
-    }
1005
-
1006
-
1007
-    /**
1008
-     * Gets the correct lowercase name for the relation in the API according
1009
-     * to the relation's type
1010
-     *
1011
-     * @param string                  $relation_name
1012
-     * @param \EE_Model_Relation_Base $relation_obj
1013
-     * @return string
1014
-     */
1015
-    public static function getRelatedEntityName($relation_name, $relation_obj)
1016
-    {
1017
-        if ($relation_obj instanceof EE_Belongs_To_Relation) {
1018
-            return strtolower($relation_name);
1019
-        } else {
1020
-            return EEH_Inflector::pluralize_and_lower($relation_name);
1021
-        }
1022
-    }
1023
-
1024
-
1025
-    /**
1026
-     * Gets the one model object with the specified id for the specified model
1027
-     *
1028
-     * @param EEM_Base        $model
1029
-     * @param WP_REST_Request $request
1030
-     * @return array|WP_Error
1031
-     */
1032
-    public function getEntityFromModel($model, $request)
1033
-    {
1034
-        $context = $this->validateContext($request->get_param('caps'));
1035
-        return $this->getOneOrReportPermissionError($model, $request, $context);
1036
-    }
1037
-
1038
-
1039
-    /**
1040
-     * If a context is provided which isn't valid, maybe it was added in a future
1041
-     * version so just treat it as a default read
1042
-     *
1043
-     * @param string $context
1044
-     * @return string array key of EEM_Base::cap_contexts_to_cap_action_map()
1045
-     */
1046
-    public function validateContext($context)
1047
-    {
1048
-        if (! $context) {
1049
-            $context = EEM_Base::caps_read;
1050
-        }
1051
-        $valid_contexts = EEM_Base::valid_cap_contexts();
1052
-        if (in_array($context, $valid_contexts)) {
1053
-            return $context;
1054
-        } else {
1055
-            return EEM_Base::caps_read;
1056
-        }
1057
-    }
1058
-
1059
-
1060
-    /**
1061
-     * Verifies the passed in value is an allowable default where conditions value.
1062
-     *
1063
-     * @param $default_query_params
1064
-     * @return string
1065
-     */
1066
-    public function validateDefaultQueryParams($default_query_params)
1067
-    {
1068
-        $valid_default_where_conditions_for_api_calls = array(
1069
-            EEM_Base::default_where_conditions_all,
1070
-            EEM_Base::default_where_conditions_minimum_all,
1071
-            EEM_Base::default_where_conditions_minimum_others,
1072
-        );
1073
-        if (! $default_query_params) {
1074
-            $default_query_params = EEM_Base::default_where_conditions_all;
1075
-        }
1076
-        if (in_array(
1077
-            $default_query_params,
1078
-            $valid_default_where_conditions_for_api_calls,
1079
-            true
1080
-        )) {
1081
-            return $default_query_params;
1082
-        } else {
1083
-            return EEM_Base::default_where_conditions_all;
1084
-        }
1085
-    }
1086
-
1087
-
1088
-    /**
1089
-     * Translates API filter get parameter into model query params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions.
1090
-     * Note: right now the query parameter keys for fields (and related fields)
1091
-     * can be left as-is, but it's quite possible this will change someday.
1092
-     * Also, this method's contents might be candidate for moving to Model_Data_Translator
1093
-     *
1094
-     * @param EEM_Base $model
1095
-     * @param array    $query_parameters  from $_GET parameter @see Read:handle_request_get_all
1096
-     * @return array model query params (@see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions)
1097
-     *                                    or FALSE to indicate that absolutely no results should be returned
1098
-     * @throws EE_Error
1099
-     * @throws RestException
1100
-     */
1101
-    public function createModelQueryParams($model, $query_parameters)
1102
-    {
1103
-        $model_query_params = array();
1104
-        if (isset($query_parameters['where'])) {
1105
-            $model_query_params[0] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1106
-                $query_parameters['where'],
1107
-                $model,
1108
-                $this->getModelVersionInfo()->requestedVersion()
1109
-            );
1110
-        }
1111
-        if (isset($query_parameters['order_by'])) {
1112
-            $order_by = $query_parameters['order_by'];
1113
-        } elseif (isset($query_parameters['orderby'])) {
1114
-            $order_by = $query_parameters['orderby'];
1115
-        } else {
1116
-            $order_by = null;
1117
-        }
1118
-        if ($order_by !== null) {
1119
-            if (is_array($order_by)) {
1120
-                $order_by = ModelDataTranslator::prepareFieldNamesInArrayKeysFromJson($order_by);
1121
-            } else {
1122
-                // it's a single item
1123
-                $order_by = ModelDataTranslator::prepareFieldNameFromJson($order_by);
1124
-            }
1125
-            $model_query_params['order_by'] = $order_by;
1126
-        }
1127
-        if (isset($query_parameters['group_by'])) {
1128
-            $group_by = $query_parameters['group_by'];
1129
-        } elseif (isset($query_parameters['groupby'])) {
1130
-            $group_by = $query_parameters['groupby'];
1131
-        } else {
1132
-            $group_by = array_keys($model->get_combined_primary_key_fields());
1133
-        }
1134
-        // make sure they're all real names
1135
-        if (is_array($group_by)) {
1136
-            $group_by = ModelDataTranslator::prepareFieldNamesFromJson($group_by);
1137
-        }
1138
-        if ($group_by !== null) {
1139
-            $model_query_params['group_by'] = $group_by;
1140
-        }
1141
-        if (isset($query_parameters['having'])) {
1142
-            $model_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1143
-                $query_parameters['having'],
1144
-                $model,
1145
-                $this->getModelVersionInfo()->requestedVersion()
1146
-            );
1147
-        }
1148
-        if (isset($query_parameters['order'])) {
1149
-            $model_query_params['order'] = $query_parameters['order'];
1150
-        }
1151
-        if (isset($query_parameters['mine'])) {
1152
-            $model_query_params = $model->alter_query_params_to_only_include_mine($model_query_params);
1153
-        }
1154
-        if (isset($query_parameters['limit'])) {
1155
-            // limit should be either a string like '23' or '23,43', or an array with two items in it
1156
-            if (! is_array($query_parameters['limit'])) {
1157
-                $limit_array = explode(',', (string) $query_parameters['limit']);
1158
-            } else {
1159
-                $limit_array = $query_parameters['limit'];
1160
-            }
1161
-            $sanitized_limit = array();
1162
-            foreach ($limit_array as $key => $limit_part) {
1163
-                if ($this->debug_mode && (! is_numeric($limit_part) || count($sanitized_limit) > 2)) {
1164
-                    throw new EE_Error(
1165
-                        sprintf(
1166
-                            __(
1167
-                            // @codingStandardsIgnoreStart
1168
-                                '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.',
1169
-                                // @codingStandardsIgnoreEnd
1170
-                                'event_espresso'
1171
-                            ),
1172
-                            wp_json_encode($query_parameters['limit'])
1173
-                        )
1174
-                    );
1175
-                }
1176
-                $sanitized_limit[] = (int) $limit_part;
1177
-            }
1178
-            $model_query_params['limit'] = implode(',', $sanitized_limit);
1179
-        } else {
1180
-            $model_query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
1181
-        }
1182
-        if (isset($query_parameters['caps'])) {
1183
-            $model_query_params['caps'] = $this->validateContext($query_parameters['caps']);
1184
-        } else {
1185
-            $model_query_params['caps'] = EEM_Base::caps_read;
1186
-        }
1187
-        if (isset($query_parameters['default_where_conditions'])) {
1188
-            $model_query_params['default_where_conditions'] = $this->validateDefaultQueryParams(
1189
-                $query_parameters['default_where_conditions']
1190
-            );
1191
-        }
1192
-        return apply_filters('FHEE__Read__create_model_query_params', $model_query_params, $query_parameters, $model);
1193
-    }
1194
-
1195
-
1196
-    /**
1197
-     * Changes the REST-style query params for use in the models
1198
-     *
1199
-     * @deprecated
1200
-     * @param EEM_Base $model
1201
-     * @param array    $query_params sub-array from @see EEM_Base::get_all()
1202
-     * @return array
1203
-     */
1204
-    public function prepareRestQueryParamsKeyForModels($model, $query_params)
1205
-    {
1206
-        $model_ready_query_params = array();
1207
-        foreach ($query_params as $key => $value) {
1208
-            if (is_array($value)) {
1209
-                $model_ready_query_params[ $key ] = $this->prepareRestQueryParamsKeyForModels($model, $value);
1210
-            } else {
1211
-                $model_ready_query_params[ $key ] = $value;
1212
-            }
1213
-        }
1214
-        return $model_ready_query_params;
1215
-    }
1216
-
1217
-
1218
-    /**
1219
-     * @deprecated instead use ModelDataTranslator::prepareFieldValuesFromJson()
1220
-     * @param $model
1221
-     * @param $query_params
1222
-     * @return array
1223
-     */
1224
-    public function prepareRestQueryParamsValuesForModels($model, $query_params)
1225
-    {
1226
-        $model_ready_query_params = array();
1227
-        foreach ($query_params as $key => $value) {
1228
-            if (is_array($value)) {
1229
-                $model_ready_query_params[ $key ] = $this->prepareRestQueryParamsValuesForModels($model, $value);
1230
-            } else {
1231
-                $model_ready_query_params[ $key ] = $value;
1232
-            }
1233
-        }
1234
-        return $model_ready_query_params;
1235
-    }
1236
-
1237
-
1238
-    /**
1239
-     * Explodes the string on commas, and only returns items with $prefix followed by a period.
1240
-     * If no prefix is specified, returns items with no period.
1241
-     *
1242
-     * @param string|array $string_to_explode eg "jibba,jabba, blah, blah, blah" or array('jibba', 'jabba' )
1243
-     * @param string       $prefix            "Event" or "foobar"
1244
-     * @return array $string_to_exploded exploded on COMMAS, and if a prefix was specified
1245
-     *                                        we only return strings starting with that and a period; if no prefix was
1246
-     *                                        specified we return all items containing NO periods
1247
-     */
1248
-    public function explodeAndGetItemsPrefixedWith($string_to_explode, $prefix)
1249
-    {
1250
-        if (is_string($string_to_explode)) {
1251
-            $exploded_contents = explode(',', $string_to_explode);
1252
-        } elseif (is_array($string_to_explode)) {
1253
-            $exploded_contents = $string_to_explode;
1254
-        } else {
1255
-            $exploded_contents = array();
1256
-        }
1257
-        // if the string was empty, we want an empty array
1258
-        $exploded_contents = array_filter($exploded_contents);
1259
-        $contents_with_prefix = array();
1260
-        foreach ($exploded_contents as $item) {
1261
-            $item = trim($item);
1262
-            // if no prefix was provided, so we look for items with no "." in them
1263
-            if (! $prefix) {
1264
-                // does this item have a period?
1265
-                if (strpos($item, '.') === false) {
1266
-                    // if not, then its what we're looking for
1267
-                    $contents_with_prefix[] = $item;
1268
-                }
1269
-            } elseif (strpos($item, $prefix . '.') === 0) {
1270
-                // this item has the prefix and a period, grab it
1271
-                $contents_with_prefix[] = substr(
1272
-                    $item,
1273
-                    strpos($item, $prefix . '.') + strlen($prefix . '.')
1274
-                );
1275
-            } elseif ($item === $prefix) {
1276
-                // this item is JUST the prefix
1277
-                // so let's grab everything after, which is a blank string
1278
-                $contents_with_prefix[] = '';
1279
-            }
1280
-        }
1281
-        return $contents_with_prefix;
1282
-    }
1283
-
1284
-
1285
-    /**
1286
-     * @deprecated since 4.8.36.rc.001 You should instead use Read::explode_and_get_items_prefixed_with.
1287
-     * Deprecated because its return values were really quite confusing- sometimes it returned
1288
-     * an empty array (when the include string was blank or '*') or sometimes it returned
1289
-     * array('*') (when you provided a model and a model of that kind was found).
1290
-     * Parses the $include_string so we fetch all the field names relating to THIS model
1291
-     * (ie have NO period in them), or for the provided model (ie start with the model
1292
-     * name and then a period).
1293
-     * @param string $include_string @see Read:handle_request_get_all
1294
-     * @param string $model_name
1295
-     * @return array of fields for this model. If $model_name is provided, then
1296
-     *                               the fields for that model, with the model's name removed from each.
1297
-     *                               If $include_string was blank or '*' returns an empty array
1298
-     */
1299
-    public function extractIncludesForThisModel($include_string, $model_name = null)
1300
-    {
1301
-        if (is_array($include_string)) {
1302
-            $include_string = implode(',', $include_string);
1303
-        }
1304
-        if ($include_string === '*' || $include_string === '') {
1305
-            return array();
1306
-        }
1307
-        $includes = explode(',', $include_string);
1308
-        $extracted_fields_to_include = array();
1309
-        if ($model_name) {
1310
-            foreach ($includes as $field_to_include) {
1311
-                $field_to_include = trim($field_to_include);
1312
-                if (strpos($field_to_include, $model_name . '.') === 0) {
1313
-                    // found the model name at the exact start
1314
-                    $field_sans_model_name = str_replace($model_name . '.', '', $field_to_include);
1315
-                    $extracted_fields_to_include[] = $field_sans_model_name;
1316
-                } elseif ($field_to_include == $model_name) {
1317
-                    $extracted_fields_to_include[] = '*';
1318
-                }
1319
-            }
1320
-        } else {
1321
-            // look for ones with no period
1322
-            foreach ($includes as $field_to_include) {
1323
-                $field_to_include = trim($field_to_include);
1324
-                if (strpos($field_to_include, '.') === false
1325
-                    && ! $this->getModelVersionInfo()->isModelNameInThisVersion($field_to_include)
1326
-                ) {
1327
-                    $extracted_fields_to_include[] = $field_to_include;
1328
-                }
1329
-            }
1330
-        }
1331
-        return $extracted_fields_to_include;
1332
-    }
1333
-
1334
-
1335
-    /**
1336
-     * Gets the single item using the model according to the request in the context given, otherwise
1337
-     * returns that it's inaccessible to the current user
1338
-     *
1339
-     * @param EEM_Base        $model
1340
-     * @param WP_REST_Request $request
1341
-     * @param null            $context
1342
-     * @return array|WP_Error
1343
-     */
1344
-    public function getOneOrReportPermissionError(EEM_Base $model, WP_REST_Request $request, $context = null)
1345
-    {
1346
-        $query_params = array(array($model->primary_key_name() => $request->get_param('id')), 'limit' => 1);
1347
-        if ($model instanceof \EEM_Soft_Delete_Base) {
1348
-            $query_params = $model->alter_query_params_so_deleted_and_undeleted_items_included($query_params);
1349
-        }
1350
-        $restricted_query_params = $query_params;
1351
-        $restricted_query_params['caps'] = $context;
1352
-        $this->setDebugInfo('model query params', $restricted_query_params);
1353
-        $model_rows = $model->get_all_wpdb_results($restricted_query_params);
1354
-        if (! empty($model_rows)) {
1355
-            return $this->createEntityFromWpdbResult(
1356
-                $model,
1357
-                array_shift($model_rows),
1358
-                $request
1359
-            );
1360
-        } else {
1361
-            // ok let's test to see if we WOULD have found it, had we not had restrictions from missing capabilities
1362
-            $lowercase_model_name = strtolower($model->get_this_model_name());
1363
-            $model_rows_found_sans_restrictions = $model->get_all_wpdb_results($query_params);
1364
-            if (! empty($model_rows_found_sans_restrictions)) {
1365
-                // you got shafted- it existed but we didn't want to tell you!
1366
-                return new WP_Error(
1367
-                    'rest_user_cannot_' . $context,
1368
-                    sprintf(
1369
-                        __('Sorry, you cannot %1$s this %2$s. Missing permissions are: %3$s', 'event_espresso'),
1370
-                        $context,
1371
-                        strtolower($model->get_this_model_name()),
1372
-                        Capabilities::getMissingPermissionsString(
1373
-                            $model,
1374
-                            $context
1375
-                        )
1376
-                    ),
1377
-                    array('status' => 403)
1378
-                );
1379
-            } else {
1380
-                // it's not you. It just doesn't exist
1381
-                return new WP_Error(
1382
-                    sprintf('rest_%s_invalid_id', $lowercase_model_name),
1383
-                    sprintf(__('Invalid %s ID.', 'event_espresso'), $lowercase_model_name),
1384
-                    array('status' => 404)
1385
-                );
1386
-            }
1387
-        }
1388
-    }
43
+	/**
44
+	 * @var CalculatedModelFields
45
+	 */
46
+	protected $fields_calculator;
47
+
48
+
49
+	/**
50
+	 * Read constructor.
51
+	 * @param CalculatedModelFields $fields_calculator
52
+	 */
53
+	public function __construct(CalculatedModelFields $fields_calculator)
54
+	{
55
+		parent::__construct();
56
+		$this->fields_calculator = $fields_calculator;
57
+	}
58
+
59
+
60
+	/**
61
+	 * Handles requests to get all (or a filtered subset) of entities for a particular model
62
+	 *
63
+	 * @param WP_REST_Request $request
64
+	 * @param string $version
65
+	 * @param string $model_name
66
+	 * @return WP_REST_Response|WP_Error
67
+	 * @throws InvalidArgumentException
68
+	 * @throws InvalidDataTypeException
69
+	 * @throws InvalidInterfaceException
70
+	 */
71
+	public static function handleRequestGetAll(WP_REST_Request $request, $version, $model_name)
72
+	{
73
+		$controller = LoaderFactory::getLoader()->getNew('EventEspresso\core\libraries\rest_api\controllers\model\Read');
74
+		try {
75
+			$controller->setRequestedVersion($version);
76
+			if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
77
+				return $controller->sendResponse(
78
+					new WP_Error(
79
+						'endpoint_parsing_error',
80
+						sprintf(
81
+							__(
82
+								'There is no model for endpoint %s. Please contact event espresso support',
83
+								'event_espresso'
84
+							),
85
+							$model_name
86
+						)
87
+					)
88
+				);
89
+			}
90
+			return $controller->sendResponse(
91
+				$controller->getEntitiesFromModel(
92
+					$controller->getModelVersionInfo()->loadModel($model_name),
93
+					$request
94
+				)
95
+			);
96
+		} catch (Exception $e) {
97
+			return $controller->sendResponse($e);
98
+		}
99
+	}
100
+
101
+
102
+	/**
103
+	 * Prepares and returns schema for any OPTIONS request.
104
+	 *
105
+	 * @param string $version The API endpoint version being used.
106
+	 * @param string $model_name Something like `Event` or `Registration`
107
+	 * @return array
108
+	 * @throws InvalidArgumentException
109
+	 * @throws InvalidDataTypeException
110
+	 * @throws InvalidInterfaceException
111
+	 */
112
+	public static function handleSchemaRequest($version, $model_name)
113
+	{
114
+		$controller = LoaderFactory::getLoader()->getNew('EventEspresso\core\libraries\rest_api\controllers\model\Read');
115
+		try {
116
+			$controller->setRequestedVersion($version);
117
+			if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
118
+				return array();
119
+			}
120
+			// get the model for this version
121
+			$model = $controller->getModelVersionInfo()->loadModel($model_name);
122
+			$model_schema = new JsonModelSchema($model, LoaderFactory::getLoader()->getShared('EventEspresso\core\libraries\rest_api\CalculatedModelFields'));
123
+			return $model_schema->getModelSchemaForRelations(
124
+				$controller->getModelVersionInfo()->relationSettings($model),
125
+				$controller->customizeSchemaForRestResponse(
126
+					$model,
127
+					$model_schema->getModelSchemaForFields(
128
+						$controller->getModelVersionInfo()->fieldsOnModelInThisVersion($model),
129
+						$model_schema->getInitialSchemaStructure()
130
+					)
131
+				)
132
+			);
133
+		} catch (Exception $e) {
134
+			return array();
135
+		}
136
+	}
137
+
138
+
139
+	/**
140
+	 * This loops through each field in the given schema for the model and does the following:
141
+	 * - add any extra fields that are REST API specific and related to existing fields.
142
+	 * - transform default values into the correct format for a REST API response.
143
+	 *
144
+	 * @param EEM_Base $model
145
+	 * @param array    $schema
146
+	 * @return array  The final schema.
147
+	 */
148
+	protected function customizeSchemaForRestResponse(EEM_Base $model, array $schema)
149
+	{
150
+		foreach ($this->getModelVersionInfo()->fieldsOnModelInThisVersion($model) as $field_name => $field) {
151
+			$schema = $this->translateDefaultsForRestResponse(
152
+				$field_name,
153
+				$field,
154
+				$this->maybeAddExtraFieldsToSchema($field_name, $field, $schema)
155
+			);
156
+		}
157
+		return $schema;
158
+	}
159
+
160
+
161
+	/**
162
+	 * This is used to ensure that the 'default' value set in the schema response is formatted correctly for the REST
163
+	 * response.
164
+	 *
165
+	 * @param                      $field_name
166
+	 * @param EE_Model_Field_Base  $field
167
+	 * @param array                $schema
168
+	 * @return array
169
+	 * @throws ObjectDetectedException if a default value has a PHP object, which should never do (and if we
170
+	 * did, let's know about it ASAP, so let the exception bubble up)
171
+	 */
172
+	protected function translateDefaultsForRestResponse($field_name, EE_Model_Field_Base $field, array $schema)
173
+	{
174
+		if (isset($schema['properties'][ $field_name ]['default'])) {
175
+			if (is_array($schema['properties'][ $field_name ]['default'])) {
176
+				foreach ($schema['properties'][ $field_name ]['default'] as $default_key => $default_value) {
177
+					if ($default_key === 'raw') {
178
+						$schema['properties'][ $field_name ]['default'][ $default_key ] =
179
+							ModelDataTranslator::prepareFieldValueForJson(
180
+								$field,
181
+								$default_value,
182
+								$this->getModelVersionInfo()->requestedVersion()
183
+							);
184
+					}
185
+				}
186
+			} else {
187
+				$schema['properties'][ $field_name ]['default'] = ModelDataTranslator::prepareFieldValueForJson(
188
+					$field,
189
+					$schema['properties'][ $field_name ]['default'],
190
+					$this->getModelVersionInfo()->requestedVersion()
191
+				);
192
+			}
193
+		}
194
+		return $schema;
195
+	}
196
+
197
+
198
+	/**
199
+	 * Adds additional fields to the schema
200
+	 * The REST API returns a GMT value field for each datetime field in the resource.  Thus the description about this
201
+	 * needs to be added to the schema.
202
+	 *
203
+	 * @param                      $field_name
204
+	 * @param EE_Model_Field_Base  $field
205
+	 * @param array                $schema
206
+	 * @return array
207
+	 */
208
+	protected function maybeAddExtraFieldsToSchema($field_name, EE_Model_Field_Base $field, array $schema)
209
+	{
210
+		if ($field instanceof EE_Datetime_Field) {
211
+			$schema['properties'][ $field_name . '_gmt' ] = $field->getSchema();
212
+			// modify the description
213
+			$schema['properties'][ $field_name . '_gmt' ]['description'] = sprintf(
214
+				esc_html__('%s - the value for this field is in GMT.', 'event_espresso'),
215
+				wp_specialchars_decode($field->get_nicename(), ENT_QUOTES)
216
+			);
217
+		}
218
+		return $schema;
219
+	}
220
+
221
+
222
+	/**
223
+	 * Used to figure out the route from the request when a `WP_REST_Request` object is not available
224
+	 *
225
+	 * @return string
226
+	 */
227
+	protected function getRouteFromRequest()
228
+	{
229
+		if (isset($GLOBALS['wp'])
230
+			&& $GLOBALS['wp'] instanceof \WP
231
+			&& isset($GLOBALS['wp']->query_vars['rest_route'])
232
+		) {
233
+			return $GLOBALS['wp']->query_vars['rest_route'];
234
+		} else {
235
+			return isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '/';
236
+		}
237
+	}
238
+
239
+
240
+	/**
241
+	 * Gets a single entity related to the model indicated in the path and its id
242
+	 *
243
+	 * @param WP_REST_Request $request
244
+	 * @param string $version
245
+	 * @param string $model_name
246
+	 * @return WP_REST_Response|WP_Error
247
+	 * @throws InvalidDataTypeException
248
+	 * @throws InvalidInterfaceException
249
+	 * @throws InvalidArgumentException
250
+	 */
251
+	public static function handleRequestGetOne(WP_REST_Request $request, $version, $model_name)
252
+	{
253
+		$controller = LoaderFactory::getLoader()->getNew('EventEspresso\core\libraries\rest_api\controllers\model\Read');
254
+		try {
255
+			$controller->setRequestedVersion($version);
256
+			if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
257
+				return $controller->sendResponse(
258
+					new WP_Error(
259
+						'endpoint_parsing_error',
260
+						sprintf(
261
+							__(
262
+								'There is no model for endpoint %s. Please contact event espresso support',
263
+								'event_espresso'
264
+							),
265
+							$model_name
266
+						)
267
+					)
268
+				);
269
+			}
270
+			return $controller->sendResponse(
271
+				$controller->getEntityFromModel(
272
+					$controller->getModelVersionInfo()->loadModel($model_name),
273
+					$request
274
+				)
275
+			);
276
+		} catch (Exception $e) {
277
+			return $controller->sendResponse($e);
278
+		}
279
+	}
280
+
281
+
282
+	/**
283
+	 * Gets all the related entities (or if its a belongs-to relation just the one)
284
+	 * to the item with the given id
285
+	 *
286
+	 * @param WP_REST_Request $request
287
+	 * @param string $version
288
+	 * @param string $model_name
289
+	 * @param string $related_model_name
290
+	 * @return WP_REST_Response|WP_Error
291
+	 * @throws InvalidDataTypeException
292
+	 * @throws InvalidInterfaceException
293
+	 * @throws InvalidArgumentException
294
+	 */
295
+	public static function handleRequestGetRelated(
296
+		WP_REST_Request $request,
297
+		$version,
298
+		$model_name,
299
+		$related_model_name
300
+	) {
301
+		$controller = LoaderFactory::getLoader()->getNew('EventEspresso\core\libraries\rest_api\controllers\model\Read');
302
+		try {
303
+			$controller->setRequestedVersion($version);
304
+			if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
305
+				return $controller->sendResponse(
306
+					new WP_Error(
307
+						'endpoint_parsing_error',
308
+						sprintf(
309
+							__(
310
+								'There is no model for endpoint %s. Please contact event espresso support',
311
+								'event_espresso'
312
+							),
313
+							$model_name
314
+						)
315
+					)
316
+				);
317
+			}
318
+			$main_model = $controller->getModelVersionInfo()->loadModel($model_name);
319
+			if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($related_model_name)) {
320
+				return $controller->sendResponse(
321
+					new WP_Error(
322
+						'endpoint_parsing_error',
323
+						sprintf(
324
+							__(
325
+								'There is no model for endpoint %s. Please contact event espresso support',
326
+								'event_espresso'
327
+							),
328
+							$related_model_name
329
+						)
330
+					)
331
+				);
332
+			}
333
+			return $controller->sendResponse(
334
+				$controller->getEntitiesFromRelation(
335
+					$request->get_param('id'),
336
+					$main_model->related_settings_for($related_model_name),
337
+					$request
338
+				)
339
+			);
340
+		} catch (Exception $e) {
341
+			return $controller->sendResponse($e);
342
+		}
343
+	}
344
+
345
+
346
+	/**
347
+	 * Gets a collection for the given model and filters
348
+	 *
349
+	 * @param EEM_Base        $model
350
+	 * @param WP_REST_Request $request
351
+	 * @return array|WP_Error
352
+	 */
353
+	public function getEntitiesFromModel($model, $request)
354
+	{
355
+		$query_params = $this->createModelQueryParams($model, $request->get_params());
356
+		if (! Capabilities::currentUserHasPartialAccessTo($model, $query_params['caps'])) {
357
+			$model_name_plural = EEH_Inflector::pluralize_and_lower($model->get_this_model_name());
358
+			return new WP_Error(
359
+				sprintf('rest_%s_cannot_list', $model_name_plural),
360
+				sprintf(
361
+					__('Sorry, you are not allowed to list %1$s. Missing permissions: %2$s', 'event_espresso'),
362
+					$model_name_plural,
363
+					Capabilities::getMissingPermissionsString($model, $query_params['caps'])
364
+				),
365
+				array('status' => 403)
366
+			);
367
+		}
368
+		if (! $request->get_header('no_rest_headers')) {
369
+			$this->setHeadersFromQueryParams($model, $query_params);
370
+		}
371
+		/** @type array $results */
372
+		$results = $model->get_all_wpdb_results($query_params);
373
+		$nice_results = array();
374
+		foreach ($results as $result) {
375
+			$nice_results[] = $this->createEntityFromWpdbResult(
376
+				$model,
377
+				$result,
378
+				$request
379
+			);
380
+		}
381
+		return $nice_results;
382
+	}
383
+
384
+
385
+	/**
386
+	 * Gets the collection for given relation object
387
+	 * The same as Read::get_entities_from_model(), except if the relation
388
+	 * is a HABTM relation, in which case it merges any non-foreign-key fields from
389
+	 * the join-model-object into the results
390
+	 *
391
+	 * @param array                   $primary_model_query_params query params for finding the item from which
392
+	 *                                                            relations will be based
393
+	 * @param \EE_Model_Relation_Base $relation
394
+	 * @param WP_REST_Request         $request
395
+	 * @return WP_Error|array
396
+	 * @throws RestException
397
+	 */
398
+	protected function getEntitiesFromRelationUsingModelQueryParams($primary_model_query_params, $relation, $request)
399
+	{
400
+		$context = $this->validateContext($request->get_param('caps'));
401
+		$model = $relation->get_this_model();
402
+		$related_model = $relation->get_other_model();
403
+		if (! isset($primary_model_query_params[0])) {
404
+			$primary_model_query_params[0] = array();
405
+		}
406
+		// check if they can access the 1st model object
407
+		$primary_model_query_params = array(
408
+			0       => $primary_model_query_params[0],
409
+			'limit' => 1,
410
+		);
411
+		if ($model instanceof \EEM_Soft_Delete_Base) {
412
+			$primary_model_query_params = $model->alter_query_params_so_deleted_and_undeleted_items_included(
413
+				$primary_model_query_params
414
+			);
415
+		}
416
+		$restricted_query_params = $primary_model_query_params;
417
+		$restricted_query_params['caps'] = $context;
418
+		$this->setDebugInfo('main model query params', $restricted_query_params);
419
+		$this->setDebugInfo('missing caps', Capabilities::getMissingPermissionsString($related_model, $context));
420
+		if (! (
421
+			Capabilities::currentUserHasPartialAccessTo($related_model, $context)
422
+			&& $model->exists($restricted_query_params)
423
+		)
424
+		) {
425
+			if ($relation instanceof EE_Belongs_To_Relation) {
426
+				$related_model_name_maybe_plural = strtolower($related_model->get_this_model_name());
427
+			} else {
428
+				$related_model_name_maybe_plural = EEH_Inflector::pluralize_and_lower(
429
+					$related_model->get_this_model_name()
430
+				);
431
+			}
432
+			return new WP_Error(
433
+				sprintf('rest_%s_cannot_list', $related_model_name_maybe_plural),
434
+				sprintf(
435
+					__(
436
+						'Sorry, you are not allowed to list %1$s related to %2$s. Missing permissions: %3$s',
437
+						'event_espresso'
438
+					),
439
+					$related_model_name_maybe_plural,
440
+					$relation->get_this_model()->get_this_model_name(),
441
+					implode(
442
+						',',
443
+						array_keys(
444
+							Capabilities::getMissingPermissions($related_model, $context)
445
+						)
446
+					)
447
+				),
448
+				array('status' => 403)
449
+			);
450
+		}
451
+		$query_params = $this->createModelQueryParams($relation->get_other_model(), $request->get_params());
452
+		foreach ($primary_model_query_params[0] as $where_condition_key => $where_condition_value) {
453
+			$query_params[0][ $relation->get_this_model()->get_this_model_name()
454
+							  . '.'
455
+							  . $where_condition_key ] = $where_condition_value;
456
+		}
457
+		$query_params['default_where_conditions'] = 'none';
458
+		$query_params['caps'] = $context;
459
+		if (! $request->get_header('no_rest_headers')) {
460
+			$this->setHeadersFromQueryParams($relation->get_other_model(), $query_params);
461
+		}
462
+		/** @type array $results */
463
+		$results = $relation->get_other_model()->get_all_wpdb_results($query_params);
464
+		$nice_results = array();
465
+		foreach ($results as $result) {
466
+			$nice_result = $this->createEntityFromWpdbResult(
467
+				$relation->get_other_model(),
468
+				$result,
469
+				$request
470
+			);
471
+			if ($relation instanceof \EE_HABTM_Relation) {
472
+				// put the unusual stuff (properties from the HABTM relation) first, and make sure
473
+				// if there are conflicts we prefer the properties from the main model
474
+				$join_model_result = $this->createEntityFromWpdbResult(
475
+					$relation->get_join_model(),
476
+					$result,
477
+					$request
478
+				);
479
+				$joined_result = array_merge($nice_result, $join_model_result);
480
+				// but keep the meta stuff from the main model
481
+				if (isset($nice_result['meta'])) {
482
+					$joined_result['meta'] = $nice_result['meta'];
483
+				}
484
+				$nice_result = $joined_result;
485
+			}
486
+			$nice_results[] = $nice_result;
487
+		}
488
+		if ($relation instanceof EE_Belongs_To_Relation) {
489
+			return array_shift($nice_results);
490
+		} else {
491
+			return $nice_results;
492
+		}
493
+	}
494
+
495
+
496
+	/**
497
+	 * Gets the collection for given relation object
498
+	 * The same as Read::get_entities_from_model(), except if the relation
499
+	 * is a HABTM relation, in which case it merges any non-foreign-key fields from
500
+	 * the join-model-object into the results
501
+	 *
502
+	 * @param string                  $id the ID of the thing we are fetching related stuff from
503
+	 * @param \EE_Model_Relation_Base $relation
504
+	 * @param WP_REST_Request         $request
505
+	 * @return array|WP_Error
506
+	 * @throws EE_Error
507
+	 */
508
+	public function getEntitiesFromRelation($id, $relation, $request)
509
+	{
510
+		if (! $relation->get_this_model()->has_primary_key_field()) {
511
+			throw new EE_Error(
512
+				sprintf(
513
+					__(
514
+					// @codingStandardsIgnoreStart
515
+						'Read::get_entities_from_relation should only be called from a model with a primary key, it was called from %1$s',
516
+						// @codingStandardsIgnoreEnd
517
+						'event_espresso'
518
+					),
519
+					$relation->get_this_model()->get_this_model_name()
520
+				)
521
+			);
522
+		}
523
+		return $this->getEntitiesFromRelationUsingModelQueryParams(
524
+			array(
525
+				array(
526
+					$relation->get_this_model()->primary_key_name() => $id,
527
+				),
528
+			),
529
+			$relation,
530
+			$request
531
+		);
532
+	}
533
+
534
+
535
+	/**
536
+	 * Sets the headers that are based on the model and query params,
537
+	 * like the total records. This should only be called on the original request
538
+	 * from the client, not on subsequent internal
539
+	 *
540
+	 * @param EEM_Base $model
541
+	 * @param array    $query_params
542
+	 * @return void
543
+	 */
544
+	protected function setHeadersFromQueryParams($model, $query_params)
545
+	{
546
+		$this->setDebugInfo('model query params', $query_params);
547
+		$this->setDebugInfo(
548
+			'missing caps',
549
+			Capabilities::getMissingPermissionsString($model, $query_params['caps'])
550
+		);
551
+		// normally the limit to a 2-part array, where the 2nd item is the limit
552
+		if (! isset($query_params['limit'])) {
553
+			$query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
554
+		}
555
+		if (is_array($query_params['limit'])) {
556
+			$limit_parts = $query_params['limit'];
557
+		} else {
558
+			$limit_parts = explode(',', $query_params['limit']);
559
+			if (count($limit_parts) == 1) {
560
+				$limit_parts = array(0, $limit_parts[0]);
561
+			}
562
+		}
563
+		// remove the group by and having parts of the query, as those will
564
+		// make the sql query return an array of values, instead of just a single value
565
+		unset($query_params['group_by'], $query_params['having'], $query_params['limit']);
566
+		$count = $model->count($query_params, null, true);
567
+		$pages = $count / $limit_parts[1];
568
+		$this->setResponseHeader('Total', $count, false);
569
+		$this->setResponseHeader('PageSize', $limit_parts[1], false);
570
+		$this->setResponseHeader('TotalPages', ceil($pages), false);
571
+	}
572
+
573
+
574
+	/**
575
+	 * Changes database results into REST API entities
576
+	 *
577
+	 * @param EEM_Base        $model
578
+	 * @param array           $db_row     like results from $wpdb->get_results()
579
+	 * @param WP_REST_Request $rest_request
580
+	 * @param string          $deprecated no longer used
581
+	 * @return array ready for being converted into json for sending to client
582
+	 */
583
+	public function createEntityFromWpdbResult($model, $db_row, $rest_request, $deprecated = null)
584
+	{
585
+		if (! $rest_request instanceof WP_REST_Request) {
586
+			// ok so this was called in the old style, where the 3rd arg was
587
+			// $include, and the 4th arg was $context
588
+			// now setup the request just to avoid fatal errors, although we won't be able
589
+			// to truly make use of it because it's kinda devoid of info
590
+			$rest_request = new WP_REST_Request();
591
+			$rest_request->set_param('include', $rest_request);
592
+			$rest_request->set_param('caps', $deprecated);
593
+		}
594
+		if ($rest_request->get_param('caps') == null) {
595
+			$rest_request->set_param('caps', EEM_Base::caps_read);
596
+		}
597
+		$entity_array = $this->createBareEntityFromWpdbResults($model, $db_row);
598
+		$entity_array = $this->addExtraFields($model, $db_row, $entity_array);
599
+		$entity_array['_links'] = $this->getEntityLinks($model, $db_row, $entity_array);
600
+		$entity_array['_calculated_fields'] = $this->getEntityCalculations($model, $db_row, $rest_request);
601
+		$entity_array = apply_filters(
602
+			'FHEE__Read__create_entity_from_wpdb_results__entity_before_including_requested_models',
603
+			$entity_array,
604
+			$model,
605
+			$rest_request->get_param('caps'),
606
+			$rest_request,
607
+			$this
608
+		);
609
+		$entity_array = $this->includeRequestedModels($model, $rest_request, $entity_array, $db_row);
610
+		$entity_array = apply_filters(
611
+			'FHEE__Read__create_entity_from_wpdb_results__entity_before_inaccessible_field_removal',
612
+			$entity_array,
613
+			$model,
614
+			$rest_request->get_param('caps'),
615
+			$rest_request,
616
+			$this
617
+		);
618
+		$result_without_inaccessible_fields = Capabilities::filterOutInaccessibleEntityFields(
619
+			$entity_array,
620
+			$model,
621
+			$rest_request->get_param('caps'),
622
+			$this->getModelVersionInfo(),
623
+			$model->get_index_primary_key_string(
624
+				$model->deduce_fields_n_values_from_cols_n_values($db_row)
625
+			)
626
+		);
627
+		$this->setDebugInfo(
628
+			'inaccessible fields',
629
+			array_keys(array_diff_key($entity_array, $result_without_inaccessible_fields))
630
+		);
631
+		return apply_filters(
632
+			'FHEE__Read__create_entity_from_wpdb_results__entity_return',
633
+			$result_without_inaccessible_fields,
634
+			$model,
635
+			$rest_request->get_param('caps')
636
+		);
637
+	}
638
+
639
+
640
+	/**
641
+	 * Creates a REST entity array (JSON object we're going to return in the response, but
642
+	 * for now still a PHP array, but soon enough we'll call json_encode on it, don't worry),
643
+	 * from $wpdb->get_row( $sql, ARRAY_A)
644
+	 *
645
+	 * @param EEM_Base $model
646
+	 * @param array    $db_row
647
+	 * @return array entity mostly ready for converting to JSON and sending in the response
648
+	 */
649
+	protected function createBareEntityFromWpdbResults(EEM_Base $model, $db_row)
650
+	{
651
+		$result = $model->deduce_fields_n_values_from_cols_n_values($db_row);
652
+		$result = array_intersect_key(
653
+			$result,
654
+			$this->getModelVersionInfo()->fieldsOnModelInThisVersion($model)
655
+		);
656
+		// if this is a CPT, we need to set the global $post to it,
657
+		// otherwise shortcodes etc won't work properly while rendering it
658
+		if ($model instanceof \EEM_CPT_Base) {
659
+			$do_chevy_shuffle = true;
660
+		} else {
661
+			$do_chevy_shuffle = false;
662
+		}
663
+		if ($do_chevy_shuffle) {
664
+			global $post;
665
+			$old_post = $post;
666
+			$post = get_post($result[ $model->primary_key_name() ]);
667
+			if (! $post instanceof \WP_Post) {
668
+				// well that's weird, because $result is what we JUST fetched from the database
669
+				throw new RestException(
670
+					'error_fetching_post_from_database_results',
671
+					esc_html__(
672
+						'An item was retrieved from the database but it\'s not a WP_Post like it should be.',
673
+						'event_espresso'
674
+					)
675
+				);
676
+			}
677
+			$model_object_classname = 'EE_' . $model->get_this_model_name();
678
+			$post->{$model_object_classname} = \EE_Registry::instance()->load_class(
679
+				$model_object_classname,
680
+				$result,
681
+				false,
682
+				false
683
+			);
684
+		}
685
+		foreach ($result as $field_name => $field_value) {
686
+			$field_obj = $model->field_settings_for($field_name);
687
+			if ($this->isSubclassOfOne($field_obj, $this->getModelVersionInfo()->fieldsIgnored())) {
688
+				unset($result[ $field_name ]);
689
+			} elseif ($this->isSubclassOfOne(
690
+				$field_obj,
691
+				$this->getModelVersionInfo()->fieldsThatHaveRenderedFormat()
692
+			)
693
+			) {
694
+				$result[ $field_name ] = array(
695
+					'raw'      => $this->prepareFieldObjValueForJson($field_obj, $field_value),
696
+					'rendered' => $this->prepareFieldObjValueForJson($field_obj, $field_value, 'pretty'),
697
+				);
698
+			} elseif ($this->isSubclassOfOne(
699
+				$field_obj,
700
+				$this->getModelVersionInfo()->fieldsThatHavePrettyFormat()
701
+			)
702
+			) {
703
+				$result[ $field_name ] = array(
704
+					'raw'    => $this->prepareFieldObjValueForJson($field_obj, $field_value),
705
+					'pretty' => $this->prepareFieldObjValueForJson($field_obj, $field_value, 'pretty'),
706
+				);
707
+			} elseif ($field_obj instanceof \EE_Datetime_Field) {
708
+				$field_value = $field_obj->prepare_for_set_from_db($field_value);
709
+				// if the value is null, but we're not supposed to permit null, then set to the field's default
710
+				if (is_null($field_value)) {
711
+					$field_value = $field_obj->getDefaultDateTimeObj();
712
+				}
713
+				if (is_null($field_value)) {
714
+					$gmt_date = $local_date = ModelDataTranslator::prepareFieldValuesForJson(
715
+						$field_obj,
716
+						$field_value,
717
+						$this->getModelVersionInfo()->requestedVersion()
718
+					);
719
+				} else {
720
+					$timezone = $field_value->getTimezone();
721
+					EEH_DTT_Helper::setTimezone($field_value, new DateTimeZone('UTC'));
722
+					$gmt_date = ModelDataTranslator::prepareFieldValuesForJson(
723
+						$field_obj,
724
+						$field_value,
725
+						$this->getModelVersionInfo()->requestedVersion()
726
+					);
727
+					EEH_DTT_Helper::setTimezone($field_value, $timezone);
728
+					$local_date = ModelDataTranslator::prepareFieldValuesForJson(
729
+						$field_obj,
730
+						$field_value,
731
+						$this->getModelVersionInfo()->requestedVersion()
732
+					);
733
+				}
734
+				$result[ $field_name . '_gmt' ] = $gmt_date;
735
+				$result[ $field_name ] = $local_date;
736
+			} else {
737
+				$result[ $field_name ] = $this->prepareFieldObjValueForJson($field_obj, $field_value);
738
+			}
739
+		}
740
+		if ($do_chevy_shuffle) {
741
+			$post = $old_post;
742
+		}
743
+		return $result;
744
+	}
745
+
746
+
747
+	/**
748
+	 * Takes a value all the way from the DB representation, to the model object's representation, to the
749
+	 * user-facing PHP representation, to the REST API representation. (Assumes you've already taken from the DB
750
+	 * representation using $field_obj->prepare_for_set_from_db())
751
+	 *
752
+	 * @param EE_Model_Field_Base $field_obj
753
+	 * @param mixed               $value  as it's stored on a model object
754
+	 * @param string              $format valid values are 'normal' (default), 'pretty', 'datetime_obj'
755
+	 * @return mixed
756
+	 * @throws ObjectDetectedException if $value contains a PHP object
757
+	 */
758
+	protected function prepareFieldObjValueForJson(EE_Model_Field_Base $field_obj, $value, $format = 'normal')
759
+	{
760
+		$value = $field_obj->prepare_for_set_from_db($value);
761
+		switch ($format) {
762
+			case 'pretty':
763
+				$value = $field_obj->prepare_for_pretty_echoing($value);
764
+				break;
765
+			case 'normal':
766
+			default:
767
+				$value = $field_obj->prepare_for_get($value);
768
+				break;
769
+		}
770
+		return ModelDataTranslator::prepareFieldValuesForJson(
771
+			$field_obj,
772
+			$value,
773
+			$this->getModelVersionInfo()->requestedVersion()
774
+		);
775
+	}
776
+
777
+
778
+	/**
779
+	 * Adds a few extra fields to the entity response
780
+	 *
781
+	 * @param EEM_Base $model
782
+	 * @param array    $db_row
783
+	 * @param array    $entity_array
784
+	 * @return array modified entity
785
+	 */
786
+	protected function addExtraFields(EEM_Base $model, $db_row, $entity_array)
787
+	{
788
+		if ($model instanceof EEM_CPT_Base) {
789
+			$entity_array['link'] = get_permalink($db_row[ $model->get_primary_key_field()->get_qualified_column() ]);
790
+		}
791
+		return $entity_array;
792
+	}
793
+
794
+
795
+	/**
796
+	 * Gets links we want to add to the response
797
+	 *
798
+	 * @global \WP_REST_Server $wp_rest_server
799
+	 * @param EEM_Base         $model
800
+	 * @param array            $db_row
801
+	 * @param array            $entity_array
802
+	 * @return array the _links item in the entity
803
+	 */
804
+	protected function getEntityLinks($model, $db_row, $entity_array)
805
+	{
806
+		// add basic links
807
+		$links = array();
808
+		if ($model->has_primary_key_field()) {
809
+			$links['self'] = array(
810
+				array(
811
+					'href' => $this->getVersionedLinkTo(
812
+						EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
813
+						. '/'
814
+						. $entity_array[ $model->primary_key_name() ]
815
+					),
816
+				),
817
+			);
818
+		}
819
+		$links['collection'] = array(
820
+			array(
821
+				'href' => $this->getVersionedLinkTo(
822
+					EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
823
+				),
824
+			),
825
+		);
826
+		// add links to related models
827
+		if ($model->has_primary_key_field()) {
828
+			foreach ($this->getModelVersionInfo()->relationSettings($model) as $relation_name => $relation_obj) {
829
+				$related_model_part = Read::getRelatedEntityName($relation_name, $relation_obj);
830
+				$links[ EED_Core_Rest_Api::ee_api_link_namespace . $related_model_part ] = array(
831
+					array(
832
+						'href'   => $this->getVersionedLinkTo(
833
+							EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
834
+							. '/'
835
+							. $entity_array[ $model->primary_key_name() ]
836
+							. '/'
837
+							. $related_model_part
838
+						),
839
+						'single' => $relation_obj instanceof EE_Belongs_To_Relation ? true : false,
840
+					),
841
+				);
842
+			}
843
+		}
844
+		return $links;
845
+	}
846
+
847
+
848
+	/**
849
+	 * Adds the included models indicated in the request to the entity provided
850
+	 *
851
+	 * @param EEM_Base        $model
852
+	 * @param WP_REST_Request $rest_request
853
+	 * @param array           $entity_array
854
+	 * @param array           $db_row
855
+	 * @return array the modified entity
856
+	 */
857
+	protected function includeRequestedModels(
858
+		EEM_Base $model,
859
+		WP_REST_Request $rest_request,
860
+		$entity_array,
861
+		$db_row = array()
862
+	) {
863
+		// if $db_row not included, hope the entity array has what we need
864
+		if (! $db_row) {
865
+			$db_row = $entity_array;
866
+		}
867
+		$includes_for_this_model = $this->explodeAndGetItemsPrefixedWith($rest_request->get_param('include'), '');
868
+		$includes_for_this_model = $this->removeModelNamesFromArray($includes_for_this_model);
869
+		// if they passed in * or didn't specify any includes, return everything
870
+		if (! in_array('*', $includes_for_this_model)
871
+			&& ! empty($includes_for_this_model)
872
+		) {
873
+			if ($model->has_primary_key_field()) {
874
+				// always include the primary key. ya just gotta know that at least
875
+				$includes_for_this_model[] = $model->primary_key_name();
876
+			}
877
+			if ($this->explodeAndGetItemsPrefixedWith($rest_request->get_param('calculate'), '')) {
878
+				$includes_for_this_model[] = '_calculated_fields';
879
+			}
880
+			$entity_array = array_intersect_key($entity_array, array_flip($includes_for_this_model));
881
+		}
882
+		$relation_settings = $this->getModelVersionInfo()->relationSettings($model);
883
+		foreach ($relation_settings as $relation_name => $relation_obj) {
884
+			$related_fields_to_include = $this->explodeAndGetItemsPrefixedWith(
885
+				$rest_request->get_param('include'),
886
+				$relation_name
887
+			);
888
+			$related_fields_to_calculate = $this->explodeAndGetItemsPrefixedWith(
889
+				$rest_request->get_param('calculate'),
890
+				$relation_name
891
+			);
892
+			// did they specify they wanted to include a related model, or
893
+			// specific fields from a related model?
894
+			// or did they specify to calculate a field from a related model?
895
+			if ($related_fields_to_include || $related_fields_to_calculate) {
896
+				// if so, we should include at least some part of the related model
897
+				$pretend_related_request = new WP_REST_Request();
898
+				$pretend_related_request->set_query_params(
899
+					array(
900
+						'caps'      => $rest_request->get_param('caps'),
901
+						'include'   => $related_fields_to_include,
902
+						'calculate' => $related_fields_to_calculate,
903
+					)
904
+				);
905
+				$pretend_related_request->add_header('no_rest_headers', true);
906
+				$primary_model_query_params = $model->alter_query_params_to_restrict_by_ID(
907
+					$model->get_index_primary_key_string(
908
+						$model->deduce_fields_n_values_from_cols_n_values($db_row)
909
+					)
910
+				);
911
+				$related_results = $this->getEntitiesFromRelationUsingModelQueryParams(
912
+					$primary_model_query_params,
913
+					$relation_obj,
914
+					$pretend_related_request
915
+				);
916
+				$entity_array[ Read::getRelatedEntityName($relation_name, $relation_obj) ] = $related_results
917
+																							 instanceof
918
+																							 WP_Error
919
+					? null
920
+					: $related_results;
921
+			}
922
+		}
923
+		return $entity_array;
924
+	}
925
+
926
+
927
+	/**
928
+	 * Returns a new array with all the names of models removed. Eg
929
+	 * array( 'Event', 'Datetime.*', 'foobar' ) would become array( 'Datetime.*', 'foobar' )
930
+	 *
931
+	 * @param array $arr
932
+	 * @return array
933
+	 */
934
+	private function removeModelNamesFromArray($arr)
935
+	{
936
+		return array_diff($arr, array_keys(EE_Registry::instance()->non_abstract_db_models));
937
+	}
938
+
939
+
940
+	/**
941
+	 * Gets the calculated fields for the response
942
+	 *
943
+	 * @param EEM_Base        $model
944
+	 * @param array           $wpdb_row
945
+	 * @param WP_REST_Request $rest_request
946
+	 * @return \stdClass the _calculations item in the entity
947
+	 * @throws ObjectDetectedException if a default value has a PHP object, which should never do (and if we
948
+	 * did, let's know about it ASAP, so let the exception bubble up)
949
+	 */
950
+	protected function getEntityCalculations($model, $wpdb_row, $rest_request)
951
+	{
952
+		$calculated_fields = $this->explodeAndGetItemsPrefixedWith(
953
+			$rest_request->get_param('calculate'),
954
+			''
955
+		);
956
+		// note: setting calculate=* doesn't do anything
957
+		$calculated_fields_to_return = new \stdClass();
958
+		foreach ($calculated_fields as $field_to_calculate) {
959
+			try {
960
+				$calculated_fields_to_return->$field_to_calculate = ModelDataTranslator::prepareFieldValueForJson(
961
+					null,
962
+					$this->fields_calculator->retrieveCalculatedFieldValue(
963
+						$model,
964
+						$field_to_calculate,
965
+						$wpdb_row,
966
+						$rest_request,
967
+						$this
968
+					),
969
+					$this->getModelVersionInfo()->requestedVersion()
970
+				);
971
+			} catch (RestException $e) {
972
+				// if we don't have permission to read it, just leave it out. but let devs know about the problem
973
+				$this->setResponseHeader(
974
+					'Notices-Field-Calculation-Errors['
975
+					. $e->getStringCode()
976
+					. ']['
977
+					. $model->get_this_model_name()
978
+					. ']['
979
+					. $field_to_calculate
980
+					. ']',
981
+					$e->getMessage(),
982
+					true
983
+				);
984
+			}
985
+		}
986
+		return $calculated_fields_to_return;
987
+	}
988
+
989
+
990
+	/**
991
+	 * Gets the full URL to the resource, taking the requested version into account
992
+	 *
993
+	 * @param string $link_part_after_version_and_slash eg "events/10/datetimes"
994
+	 * @return string url eg "http://mysite.com/wp-json/ee/v4.6/events/10/datetimes"
995
+	 */
996
+	public function getVersionedLinkTo($link_part_after_version_and_slash)
997
+	{
998
+		return rest_url(
999
+			EED_Core_Rest_Api::get_versioned_route_to(
1000
+				$link_part_after_version_and_slash,
1001
+				$this->getModelVersionInfo()->requestedVersion()
1002
+			)
1003
+		);
1004
+	}
1005
+
1006
+
1007
+	/**
1008
+	 * Gets the correct lowercase name for the relation in the API according
1009
+	 * to the relation's type
1010
+	 *
1011
+	 * @param string                  $relation_name
1012
+	 * @param \EE_Model_Relation_Base $relation_obj
1013
+	 * @return string
1014
+	 */
1015
+	public static function getRelatedEntityName($relation_name, $relation_obj)
1016
+	{
1017
+		if ($relation_obj instanceof EE_Belongs_To_Relation) {
1018
+			return strtolower($relation_name);
1019
+		} else {
1020
+			return EEH_Inflector::pluralize_and_lower($relation_name);
1021
+		}
1022
+	}
1023
+
1024
+
1025
+	/**
1026
+	 * Gets the one model object with the specified id for the specified model
1027
+	 *
1028
+	 * @param EEM_Base        $model
1029
+	 * @param WP_REST_Request $request
1030
+	 * @return array|WP_Error
1031
+	 */
1032
+	public function getEntityFromModel($model, $request)
1033
+	{
1034
+		$context = $this->validateContext($request->get_param('caps'));
1035
+		return $this->getOneOrReportPermissionError($model, $request, $context);
1036
+	}
1037
+
1038
+
1039
+	/**
1040
+	 * If a context is provided which isn't valid, maybe it was added in a future
1041
+	 * version so just treat it as a default read
1042
+	 *
1043
+	 * @param string $context
1044
+	 * @return string array key of EEM_Base::cap_contexts_to_cap_action_map()
1045
+	 */
1046
+	public function validateContext($context)
1047
+	{
1048
+		if (! $context) {
1049
+			$context = EEM_Base::caps_read;
1050
+		}
1051
+		$valid_contexts = EEM_Base::valid_cap_contexts();
1052
+		if (in_array($context, $valid_contexts)) {
1053
+			return $context;
1054
+		} else {
1055
+			return EEM_Base::caps_read;
1056
+		}
1057
+	}
1058
+
1059
+
1060
+	/**
1061
+	 * Verifies the passed in value is an allowable default where conditions value.
1062
+	 *
1063
+	 * @param $default_query_params
1064
+	 * @return string
1065
+	 */
1066
+	public function validateDefaultQueryParams($default_query_params)
1067
+	{
1068
+		$valid_default_where_conditions_for_api_calls = array(
1069
+			EEM_Base::default_where_conditions_all,
1070
+			EEM_Base::default_where_conditions_minimum_all,
1071
+			EEM_Base::default_where_conditions_minimum_others,
1072
+		);
1073
+		if (! $default_query_params) {
1074
+			$default_query_params = EEM_Base::default_where_conditions_all;
1075
+		}
1076
+		if (in_array(
1077
+			$default_query_params,
1078
+			$valid_default_where_conditions_for_api_calls,
1079
+			true
1080
+		)) {
1081
+			return $default_query_params;
1082
+		} else {
1083
+			return EEM_Base::default_where_conditions_all;
1084
+		}
1085
+	}
1086
+
1087
+
1088
+	/**
1089
+	 * Translates API filter get parameter into model query params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions.
1090
+	 * Note: right now the query parameter keys for fields (and related fields)
1091
+	 * can be left as-is, but it's quite possible this will change someday.
1092
+	 * Also, this method's contents might be candidate for moving to Model_Data_Translator
1093
+	 *
1094
+	 * @param EEM_Base $model
1095
+	 * @param array    $query_parameters  from $_GET parameter @see Read:handle_request_get_all
1096
+	 * @return array model query params (@see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions)
1097
+	 *                                    or FALSE to indicate that absolutely no results should be returned
1098
+	 * @throws EE_Error
1099
+	 * @throws RestException
1100
+	 */
1101
+	public function createModelQueryParams($model, $query_parameters)
1102
+	{
1103
+		$model_query_params = array();
1104
+		if (isset($query_parameters['where'])) {
1105
+			$model_query_params[0] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1106
+				$query_parameters['where'],
1107
+				$model,
1108
+				$this->getModelVersionInfo()->requestedVersion()
1109
+			);
1110
+		}
1111
+		if (isset($query_parameters['order_by'])) {
1112
+			$order_by = $query_parameters['order_by'];
1113
+		} elseif (isset($query_parameters['orderby'])) {
1114
+			$order_by = $query_parameters['orderby'];
1115
+		} else {
1116
+			$order_by = null;
1117
+		}
1118
+		if ($order_by !== null) {
1119
+			if (is_array($order_by)) {
1120
+				$order_by = ModelDataTranslator::prepareFieldNamesInArrayKeysFromJson($order_by);
1121
+			} else {
1122
+				// it's a single item
1123
+				$order_by = ModelDataTranslator::prepareFieldNameFromJson($order_by);
1124
+			}
1125
+			$model_query_params['order_by'] = $order_by;
1126
+		}
1127
+		if (isset($query_parameters['group_by'])) {
1128
+			$group_by = $query_parameters['group_by'];
1129
+		} elseif (isset($query_parameters['groupby'])) {
1130
+			$group_by = $query_parameters['groupby'];
1131
+		} else {
1132
+			$group_by = array_keys($model->get_combined_primary_key_fields());
1133
+		}
1134
+		// make sure they're all real names
1135
+		if (is_array($group_by)) {
1136
+			$group_by = ModelDataTranslator::prepareFieldNamesFromJson($group_by);
1137
+		}
1138
+		if ($group_by !== null) {
1139
+			$model_query_params['group_by'] = $group_by;
1140
+		}
1141
+		if (isset($query_parameters['having'])) {
1142
+			$model_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1143
+				$query_parameters['having'],
1144
+				$model,
1145
+				$this->getModelVersionInfo()->requestedVersion()
1146
+			);
1147
+		}
1148
+		if (isset($query_parameters['order'])) {
1149
+			$model_query_params['order'] = $query_parameters['order'];
1150
+		}
1151
+		if (isset($query_parameters['mine'])) {
1152
+			$model_query_params = $model->alter_query_params_to_only_include_mine($model_query_params);
1153
+		}
1154
+		if (isset($query_parameters['limit'])) {
1155
+			// limit should be either a string like '23' or '23,43', or an array with two items in it
1156
+			if (! is_array($query_parameters['limit'])) {
1157
+				$limit_array = explode(',', (string) $query_parameters['limit']);
1158
+			} else {
1159
+				$limit_array = $query_parameters['limit'];
1160
+			}
1161
+			$sanitized_limit = array();
1162
+			foreach ($limit_array as $key => $limit_part) {
1163
+				if ($this->debug_mode && (! is_numeric($limit_part) || count($sanitized_limit) > 2)) {
1164
+					throw new EE_Error(
1165
+						sprintf(
1166
+							__(
1167
+							// @codingStandardsIgnoreStart
1168
+								'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.',
1169
+								// @codingStandardsIgnoreEnd
1170
+								'event_espresso'
1171
+							),
1172
+							wp_json_encode($query_parameters['limit'])
1173
+						)
1174
+					);
1175
+				}
1176
+				$sanitized_limit[] = (int) $limit_part;
1177
+			}
1178
+			$model_query_params['limit'] = implode(',', $sanitized_limit);
1179
+		} else {
1180
+			$model_query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
1181
+		}
1182
+		if (isset($query_parameters['caps'])) {
1183
+			$model_query_params['caps'] = $this->validateContext($query_parameters['caps']);
1184
+		} else {
1185
+			$model_query_params['caps'] = EEM_Base::caps_read;
1186
+		}
1187
+		if (isset($query_parameters['default_where_conditions'])) {
1188
+			$model_query_params['default_where_conditions'] = $this->validateDefaultQueryParams(
1189
+				$query_parameters['default_where_conditions']
1190
+			);
1191
+		}
1192
+		return apply_filters('FHEE__Read__create_model_query_params', $model_query_params, $query_parameters, $model);
1193
+	}
1194
+
1195
+
1196
+	/**
1197
+	 * Changes the REST-style query params for use in the models
1198
+	 *
1199
+	 * @deprecated
1200
+	 * @param EEM_Base $model
1201
+	 * @param array    $query_params sub-array from @see EEM_Base::get_all()
1202
+	 * @return array
1203
+	 */
1204
+	public function prepareRestQueryParamsKeyForModels($model, $query_params)
1205
+	{
1206
+		$model_ready_query_params = array();
1207
+		foreach ($query_params as $key => $value) {
1208
+			if (is_array($value)) {
1209
+				$model_ready_query_params[ $key ] = $this->prepareRestQueryParamsKeyForModels($model, $value);
1210
+			} else {
1211
+				$model_ready_query_params[ $key ] = $value;
1212
+			}
1213
+		}
1214
+		return $model_ready_query_params;
1215
+	}
1216
+
1217
+
1218
+	/**
1219
+	 * @deprecated instead use ModelDataTranslator::prepareFieldValuesFromJson()
1220
+	 * @param $model
1221
+	 * @param $query_params
1222
+	 * @return array
1223
+	 */
1224
+	public function prepareRestQueryParamsValuesForModels($model, $query_params)
1225
+	{
1226
+		$model_ready_query_params = array();
1227
+		foreach ($query_params as $key => $value) {
1228
+			if (is_array($value)) {
1229
+				$model_ready_query_params[ $key ] = $this->prepareRestQueryParamsValuesForModels($model, $value);
1230
+			} else {
1231
+				$model_ready_query_params[ $key ] = $value;
1232
+			}
1233
+		}
1234
+		return $model_ready_query_params;
1235
+	}
1236
+
1237
+
1238
+	/**
1239
+	 * Explodes the string on commas, and only returns items with $prefix followed by a period.
1240
+	 * If no prefix is specified, returns items with no period.
1241
+	 *
1242
+	 * @param string|array $string_to_explode eg "jibba,jabba, blah, blah, blah" or array('jibba', 'jabba' )
1243
+	 * @param string       $prefix            "Event" or "foobar"
1244
+	 * @return array $string_to_exploded exploded on COMMAS, and if a prefix was specified
1245
+	 *                                        we only return strings starting with that and a period; if no prefix was
1246
+	 *                                        specified we return all items containing NO periods
1247
+	 */
1248
+	public function explodeAndGetItemsPrefixedWith($string_to_explode, $prefix)
1249
+	{
1250
+		if (is_string($string_to_explode)) {
1251
+			$exploded_contents = explode(',', $string_to_explode);
1252
+		} elseif (is_array($string_to_explode)) {
1253
+			$exploded_contents = $string_to_explode;
1254
+		} else {
1255
+			$exploded_contents = array();
1256
+		}
1257
+		// if the string was empty, we want an empty array
1258
+		$exploded_contents = array_filter($exploded_contents);
1259
+		$contents_with_prefix = array();
1260
+		foreach ($exploded_contents as $item) {
1261
+			$item = trim($item);
1262
+			// if no prefix was provided, so we look for items with no "." in them
1263
+			if (! $prefix) {
1264
+				// does this item have a period?
1265
+				if (strpos($item, '.') === false) {
1266
+					// if not, then its what we're looking for
1267
+					$contents_with_prefix[] = $item;
1268
+				}
1269
+			} elseif (strpos($item, $prefix . '.') === 0) {
1270
+				// this item has the prefix and a period, grab it
1271
+				$contents_with_prefix[] = substr(
1272
+					$item,
1273
+					strpos($item, $prefix . '.') + strlen($prefix . '.')
1274
+				);
1275
+			} elseif ($item === $prefix) {
1276
+				// this item is JUST the prefix
1277
+				// so let's grab everything after, which is a blank string
1278
+				$contents_with_prefix[] = '';
1279
+			}
1280
+		}
1281
+		return $contents_with_prefix;
1282
+	}
1283
+
1284
+
1285
+	/**
1286
+	 * @deprecated since 4.8.36.rc.001 You should instead use Read::explode_and_get_items_prefixed_with.
1287
+	 * Deprecated because its return values were really quite confusing- sometimes it returned
1288
+	 * an empty array (when the include string was blank or '*') or sometimes it returned
1289
+	 * array('*') (when you provided a model and a model of that kind was found).
1290
+	 * Parses the $include_string so we fetch all the field names relating to THIS model
1291
+	 * (ie have NO period in them), or for the provided model (ie start with the model
1292
+	 * name and then a period).
1293
+	 * @param string $include_string @see Read:handle_request_get_all
1294
+	 * @param string $model_name
1295
+	 * @return array of fields for this model. If $model_name is provided, then
1296
+	 *                               the fields for that model, with the model's name removed from each.
1297
+	 *                               If $include_string was blank or '*' returns an empty array
1298
+	 */
1299
+	public function extractIncludesForThisModel($include_string, $model_name = null)
1300
+	{
1301
+		if (is_array($include_string)) {
1302
+			$include_string = implode(',', $include_string);
1303
+		}
1304
+		if ($include_string === '*' || $include_string === '') {
1305
+			return array();
1306
+		}
1307
+		$includes = explode(',', $include_string);
1308
+		$extracted_fields_to_include = array();
1309
+		if ($model_name) {
1310
+			foreach ($includes as $field_to_include) {
1311
+				$field_to_include = trim($field_to_include);
1312
+				if (strpos($field_to_include, $model_name . '.') === 0) {
1313
+					// found the model name at the exact start
1314
+					$field_sans_model_name = str_replace($model_name . '.', '', $field_to_include);
1315
+					$extracted_fields_to_include[] = $field_sans_model_name;
1316
+				} elseif ($field_to_include == $model_name) {
1317
+					$extracted_fields_to_include[] = '*';
1318
+				}
1319
+			}
1320
+		} else {
1321
+			// look for ones with no period
1322
+			foreach ($includes as $field_to_include) {
1323
+				$field_to_include = trim($field_to_include);
1324
+				if (strpos($field_to_include, '.') === false
1325
+					&& ! $this->getModelVersionInfo()->isModelNameInThisVersion($field_to_include)
1326
+				) {
1327
+					$extracted_fields_to_include[] = $field_to_include;
1328
+				}
1329
+			}
1330
+		}
1331
+		return $extracted_fields_to_include;
1332
+	}
1333
+
1334
+
1335
+	/**
1336
+	 * Gets the single item using the model according to the request in the context given, otherwise
1337
+	 * returns that it's inaccessible to the current user
1338
+	 *
1339
+	 * @param EEM_Base        $model
1340
+	 * @param WP_REST_Request $request
1341
+	 * @param null            $context
1342
+	 * @return array|WP_Error
1343
+	 */
1344
+	public function getOneOrReportPermissionError(EEM_Base $model, WP_REST_Request $request, $context = null)
1345
+	{
1346
+		$query_params = array(array($model->primary_key_name() => $request->get_param('id')), 'limit' => 1);
1347
+		if ($model instanceof \EEM_Soft_Delete_Base) {
1348
+			$query_params = $model->alter_query_params_so_deleted_and_undeleted_items_included($query_params);
1349
+		}
1350
+		$restricted_query_params = $query_params;
1351
+		$restricted_query_params['caps'] = $context;
1352
+		$this->setDebugInfo('model query params', $restricted_query_params);
1353
+		$model_rows = $model->get_all_wpdb_results($restricted_query_params);
1354
+		if (! empty($model_rows)) {
1355
+			return $this->createEntityFromWpdbResult(
1356
+				$model,
1357
+				array_shift($model_rows),
1358
+				$request
1359
+			);
1360
+		} else {
1361
+			// ok let's test to see if we WOULD have found it, had we not had restrictions from missing capabilities
1362
+			$lowercase_model_name = strtolower($model->get_this_model_name());
1363
+			$model_rows_found_sans_restrictions = $model->get_all_wpdb_results($query_params);
1364
+			if (! empty($model_rows_found_sans_restrictions)) {
1365
+				// you got shafted- it existed but we didn't want to tell you!
1366
+				return new WP_Error(
1367
+					'rest_user_cannot_' . $context,
1368
+					sprintf(
1369
+						__('Sorry, you cannot %1$s this %2$s. Missing permissions are: %3$s', 'event_espresso'),
1370
+						$context,
1371
+						strtolower($model->get_this_model_name()),
1372
+						Capabilities::getMissingPermissionsString(
1373
+							$model,
1374
+							$context
1375
+						)
1376
+					),
1377
+					array('status' => 403)
1378
+				);
1379
+			} else {
1380
+				// it's not you. It just doesn't exist
1381
+				return new WP_Error(
1382
+					sprintf('rest_%s_invalid_id', $lowercase_model_name),
1383
+					sprintf(__('Invalid %s ID.', 'event_espresso'), $lowercase_model_name),
1384
+					array('status' => 404)
1385
+				);
1386
+			}
1387
+		}
1388
+	}
1389 1389
 }
Please login to merge, or discard this patch.
validation/EE_Model_Matching_Query_Validation_Strategy.strategy.php 1 patch
Indentation   +71 added lines, -71 removed lines patch added patch discarded remove patch
@@ -13,86 +13,86 @@
 block discarded – undo
13 13
 class EE_Model_Matching_Query_Validation_Strategy extends EE_Validation_Strategy_Base
14 14
 {
15 15
 
16
-    /**
17
-     *
18
-     * @var EEM_Base
19
-     */
20
-    protected $_model;
21
-    protected $_query_params;
22
-    protected $_input_field_name;
16
+	/**
17
+	 *
18
+	 * @var EEM_Base
19
+	 */
20
+	protected $_model;
21
+	protected $_query_params;
22
+	protected $_input_field_name;
23 23
 
24 24
 
25 25
 
26
-    /**
27
-     * @param string $validation_error_message
28
-     * @param string $model_name  name of an EEM_Base model
29
-     * @param array  $query_params     @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
30
-     * @param string $input_field_name the input will be treated as this field's value
31
-     * @throws \EE_Error
32
-     */
33
-    public function __construct($validation_error_message = null, $model_name = '', $query_params = array(), $input_field_name = '')
34
-    {
35
-        if (! EE_Registry::instance()->is_model_name($model_name)) {
36
-            throw new EE_Error(sprintf(__('You must provide a valid model object ', 'event_espresso'), $model_name));
37
-        }
38
-        $this->_model = EE_Registry::instance()->load_model($model_name);
39
-        $this->_query_params = $query_params;
40
-        if (empty($input_field_name)) {
41
-            $input_field_name = $this->_model->primary_key_name();
42
-        }
43
-        $this->_input_field_name = $input_field_name;
44
-        parent::__construct($validation_error_message);
45
-    }
26
+	/**
27
+	 * @param string $validation_error_message
28
+	 * @param string $model_name  name of an EEM_Base model
29
+	 * @param array  $query_params     @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
30
+	 * @param string $input_field_name the input will be treated as this field's value
31
+	 * @throws \EE_Error
32
+	 */
33
+	public function __construct($validation_error_message = null, $model_name = '', $query_params = array(), $input_field_name = '')
34
+	{
35
+		if (! EE_Registry::instance()->is_model_name($model_name)) {
36
+			throw new EE_Error(sprintf(__('You must provide a valid model object ', 'event_espresso'), $model_name));
37
+		}
38
+		$this->_model = EE_Registry::instance()->load_model($model_name);
39
+		$this->_query_params = $query_params;
40
+		if (empty($input_field_name)) {
41
+			$input_field_name = $this->_model->primary_key_name();
42
+		}
43
+		$this->_input_field_name = $input_field_name;
44
+		parent::__construct($validation_error_message);
45
+	}
46 46
 
47 47
 
48 48
 
49
-    /**
50
-     * @param $normalized_value
51
-     * @return bool|void
52
-     * @throws \EE_Error
53
-     * @throws \EE_Validation_Error
54
-     */
55
-    public function validate($normalized_value)
56
-    {
57
-        if (empty($normalized_value)) {
58
-            return true;
59
-        }
60
-        $combined_query_params = $this->get_query_params();
61
-        $combined_query_params[0][ $this->treat_input_as_field() ] = $normalized_value;
62
-        if (! $this->get_model()->exists($combined_query_params)) {
63
-            throw new EE_Validation_Error($this->get_validation_error_message(), 'no_matching_model_object');
64
-        }
65
-    }
49
+	/**
50
+	 * @param $normalized_value
51
+	 * @return bool|void
52
+	 * @throws \EE_Error
53
+	 * @throws \EE_Validation_Error
54
+	 */
55
+	public function validate($normalized_value)
56
+	{
57
+		if (empty($normalized_value)) {
58
+			return true;
59
+		}
60
+		$combined_query_params = $this->get_query_params();
61
+		$combined_query_params[0][ $this->treat_input_as_field() ] = $normalized_value;
62
+		if (! $this->get_model()->exists($combined_query_params)) {
63
+			throw new EE_Validation_Error($this->get_validation_error_message(), 'no_matching_model_object');
64
+		}
65
+	}
66 66
 
67
-    /**
68
-     * Gets the model used for querying
69
-     * @return EEM_Base
70
-     */
71
-    public function get_model()
72
-    {
73
-        return $this->_model;
74
-    }
67
+	/**
68
+	 * Gets the model used for querying
69
+	 * @return EEM_Base
70
+	 */
71
+	public function get_model()
72
+	{
73
+		return $this->_model;
74
+	}
75 75
 
76
-    /**
77
-     * Returns query params used for model query
78
-     * @return array
79
-     */
80
-    public function get_query_params()
81
-    {
82
-        return (array) $this->_query_params;
83
-    }
76
+	/**
77
+	 * Returns query params used for model query
78
+	 * @return array
79
+	 */
80
+	public function get_query_params()
81
+	{
82
+		return (array) $this->_query_params;
83
+	}
84 84
 
85
-    /**
86
-     * Gets the name of the field that will be used for lookup.
87
-     * eg it could be "EVT_name", meaning that if there is a model object in
88
-     * the database that has that event name, and matching the other query parameters
89
-     * on this strategy, the input will pass validation server-side
90
-     * @return string
91
-     */
92
-    public function treat_input_as_field()
93
-    {
94
-        return $this->_input_field_name;
95
-    }
85
+	/**
86
+	 * Gets the name of the field that will be used for lookup.
87
+	 * eg it could be "EVT_name", meaning that if there is a model object in
88
+	 * the database that has that event name, and matching the other query parameters
89
+	 * on this strategy, the input will pass validation server-side
90
+	 * @return string
91
+	 */
92
+	public function treat_input_as_field()
93
+	{
94
+		return $this->_input_field_name;
95
+	}
96 96
 }
97 97
 
98 98
 // End of file EE_FUll_HTML_Validation_Strategy.strategy.php
Please login to merge, or discard this patch.