Completed
Branch FET/simplify-rest-api-passing-... (d968c1)
by
unknown
88:32 queued 78:52
created

determineNestedConditionQueryParameters()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 53

Duplication

Lines 17
Ratio 32.08 %

Importance

Changes 0
Metric Value
cc 5
nc 4
nop 6
dl 17
loc 53
rs 8.7143
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace EventEspresso\core\libraries\rest_api;
4
5
use DomainException;
6
use EE_Capabilities;
7
use EE_Datetime_Field;
8
use EE_Error;
9
use EE_Infinite_Integer_Field;
10
use EE_Maybe_Serialized_Simple_HTML_Field;
11
use EE_Model_Field_Base;
12
use EE_Serialized_Text_Field;
13
use EEM_Base;
14
15
/**
16
 * Class Model_Data_Translator
17
 * Class for translating data between the EE4 models and JSON.
18
 * Some of this class needs to interpret an incoming array of query params from
19
 * the REST API and prepare it for use by the models. Some of this code seems duplicated
20
 * from the models but it's anticipated to diverge (because providing parameters
21
 * in the REST API is sometimes more difficult than in PHP directly. Eg, providing an array like
22
 * array( 'where' => array( 'EVT_ID' => array( '<', 100 ) ) ) in PHP is easy, but in a query string it needs to look
23
 * like
24
 * "where[EVT_ID][]=<&where[EVT_ID][]=100" is less intuitive, so we may want
25
 * to allow REST API query parameters to diverge from the format accepted by models)
26
 *
27
 * @package               Event Espresso
28
 * @subpackage
29
 * @author                Mike Nelson
30
 * @since                 4.8.36
31
 */
32
class ModelDataTranslator
33
{
34
35
    /**
36
     * We used to use -1 for infinity in the rest api, but that's ambiguous for
37
     * fields that COULD contain -1; so we use null
38
     */
39
    const EE_INF_IN_REST = null;
40
41
42
    /**
43
     * Prepares a possible array of input values from JSON for use by the models
44
     *
45
     * @param EE_Model_Field_Base $field_obj
46
     * @param mixed               $original_value_maybe_array
47
     * @param string              $requested_version
48
     * @param string              $timezone_string treat values as being in this timezone
49
     * @return mixed
50
     * @throws RestException
51
     */
52
    public static function prepareFieldValuesFromJson(
53
        $field_obj,
54
        $original_value_maybe_array,
55
        $requested_version,
56
        $timezone_string = 'UTC'
57
    ) {
58
        if (is_array($original_value_maybe_array)
59
            && ! $field_obj instanceof EE_Serialized_Text_Field
60
        ) {
61
            $new_value_maybe_array = array();
62
            foreach ($original_value_maybe_array as $array_key => $array_item) {
63
                $new_value_maybe_array[ $array_key ] = ModelDataTranslator::prepareFieldValueFromJson(
64
                    $field_obj,
65
                    $array_item,
66
                    $requested_version,
67
                    $timezone_string
68
                );
69
            }
70
        } else {
71
            $new_value_maybe_array = ModelDataTranslator::prepareFieldValueFromJson(
72
                $field_obj,
73
                $original_value_maybe_array,
74
                $requested_version,
75
                $timezone_string
76
            );
77
        }
78
        return $new_value_maybe_array;
79
    }
80
81
82
    /**
83
     * Prepares an array of field values FOR use in JSON/REST API
84
     *
85
     * @param EE_Model_Field_Base $field_obj
86
     * @param mixed               $original_value_maybe_array
87
     * @param string              $request_version (eg 4.8.36)
88
     * @return array
89
     */
90
    public static function prepareFieldValuesForJson($field_obj, $original_value_maybe_array, $request_version)
91
    {
92
        if (is_array($original_value_maybe_array)) {
93
            $new_value = array();
94
            foreach ($original_value_maybe_array as $key => $value) {
95
                $new_value[ $key ] = ModelDataTranslator::prepareFieldValuesForJson(
96
                    $field_obj,
97
                    $value,
98
                    $request_version
99
                );
100
            }
101
        } else {
102
            $new_value = ModelDataTranslator::prepareFieldValueForJson(
103
                $field_obj,
104
                $original_value_maybe_array,
105
                $request_version
106
            );
107
        }
108
        return $new_value;
109
    }
110
111
112
    /**
113
     * Prepares incoming data from the json or $_REQUEST parameters for the models'
114
     * "$query_params".
115
     *
116
     * @param EE_Model_Field_Base $field_obj
117
     * @param mixed               $original_value
118
     * @param string              $requested_version
119
     * @param string              $timezone_string treat values as being in this timezone
120
     * @return mixed
121
     * @throws RestException
122
     * @throws DomainException
123
     * @throws EE_Error
124
     */
125
    public static function prepareFieldValueFromJson(
126
        $field_obj,
127
        $original_value,
128
        $requested_version,
129
        $timezone_string = 'UTC' // UTC
130
    ) {
131
        // check if they accidentally submitted an error value. If so throw an exception
132
        if (is_array($original_value)
133
            && isset($original_value['error_code'], $original_value['error_message'])) {
134
            throw new RestException(
135
                'rest_submitted_error_value',
136
                sprintf(
137
                    esc_html__(
138
                        'You tried to submit a JSON error object as a value for %1$s. That\'s not allowed.',
139
                        'event_espresso'
140
                    ),
141
                    $field_obj->get_name()
142
                ),
143
                array(
144
                    'status' => 400,
145
                )
146
            );
147
        }
148
        // double-check for serialized PHP. We never accept serialized PHP. No way Jose.
149
        ModelDataTranslator::throwExceptionIfContainsSerializedData($original_value);
150
        $timezone_string = $timezone_string !== '' ? $timezone_string : get_option('timezone_string', '');
151
        $new_value = null;
0 ignored issues
show
Unused Code introduced by
$new_value is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
152
        // walk through the submitted data and double-check for serialized PHP. We never accept serialized PHP. No
153
        // way Jose.
154
        ModelDataTranslator::throwExceptionIfContainsSerializedData($original_value);
155
        if ($field_obj instanceof EE_Infinite_Integer_Field
156
            && in_array($original_value, array(null, ''), true)
157
        ) {
158
            $new_value = EE_INF;
159
        } elseif ($field_obj instanceof EE_Datetime_Field) {
160
            $new_value = rest_parse_date(
161
                self::getTimestampWithTimezoneOffset($original_value, $field_obj, $timezone_string)
162
            );
163
            if ($new_value === false) {
164
                throw new RestException(
165
                    'invalid_format_for_timestamp',
166
                    sprintf(
167
                        esc_html__(
168
                            '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.',
169
                            'event_espresso'
170
                        ),
171
                        'RFC3339',
172
                        'ISO8601',
173
                        $original_value
174
                    ),
175
                    array(
176
                        'status' => 400,
177
                    )
178
                );
179
            }
180
        } else {
181
            $new_value = $original_value;
182
        }
183
        return $new_value;
184
    }
185
186
187
    /**
188
     * This checks if the incoming timestamp has timezone information already on it and if it doesn't then adds timezone
189
     * information via details obtained from the host site.
190
     *
191
     * @param string            $original_timestamp
192
     * @param EE_Datetime_Field $datetime_field
193
     * @param                   $timezone_string
194
     * @return string
195
     * @throws DomainException
196
     */
197
    private static function getTimestampWithTimezoneOffset(
198
        $original_timestamp,
199
        EE_Datetime_Field $datetime_field,
200
        $timezone_string
201
    ) {
202
        // already have timezone information?
203
        if (preg_match('/Z|(\+|\-)(\d{2}:\d{2})/', $original_timestamp)) {
204
            // yes, we're ignoring the timezone.
205
            return $original_timestamp;
206
        }
207
        // need to append timezone
208
        list($offset_sign, $offset_secs) = self::parseTimezoneOffset(
209
            $datetime_field->get_timezone_offset(
210
                new \DateTimeZone($timezone_string),
211
                $original_timestamp
212
            )
213
        );
214
        $offset_string =
215
            str_pad(
216
                floor($offset_secs / HOUR_IN_SECONDS),
217
                2,
218
                '0',
219
                STR_PAD_LEFT
220
            )
221
            . ':'
222
            . str_pad(
223
                ($offset_secs % HOUR_IN_SECONDS) / MINUTE_IN_SECONDS,
224
                2,
225
                '0',
226
                STR_PAD_LEFT
227
            );
228
        return $original_timestamp . $offset_sign . $offset_string;
229
    }
230
231
232
    /**
233
     * Throws an exception if $data is a serialized PHP string (or somehow an actually PHP object, although I don't
234
     * think that can happen). If $data is an array, recurses into its keys and values
235
     *
236
     * @param mixed $data
237
     * @throws RestException
238
     * @return void
239
     */
240
    public static function throwExceptionIfContainsSerializedData($data)
241
    {
242
        if (is_array($data)) {
243
            foreach ($data as $key => $value) {
244
                ModelDataTranslator::throwExceptionIfContainsSerializedData($key);
245
                ModelDataTranslator::throwExceptionIfContainsSerializedData($value);
246
            }
247
        } else {
248
            if (is_serialized($data) || is_object($data)) {
249
                throw new RestException(
250
                    'serialized_data_submission_prohibited',
251
                    esc_html__(
252
                    // @codingStandardsIgnoreStart
253
                        'You tried to submit a string of serialized text. Serialized PHP is prohibited over the EE4 REST API.',
254
                        // @codingStandardsIgnoreEnd
255
                        'event_espresso'
256
                    )
257
                );
258
            }
259
        }
260
    }
261
262
263
    /**
264
     * determines what's going on with them timezone strings
265
     *
266
     * @param int $timezone_offset
267
     * @return array
268
     */
269
    private static function parseTimezoneOffset($timezone_offset)
270
    {
271
        $first_char = substr((string) $timezone_offset, 0, 1);
272
        if ($first_char === '+' || $first_char === '-') {
273
            $offset_sign = $first_char;
274
            $offset_secs = substr((string) $timezone_offset, 1);
275
        } else {
276
            $offset_sign = '+';
277
            $offset_secs = $timezone_offset;
278
        }
279
        return array($offset_sign, $offset_secs);
280
    }
281
282
283
    /**
284
     * Prepares a field's value for display in the API
285
     *
286
     * @param EE_Model_Field_Base $field_obj
287
     * @param mixed               $original_value
288
     * @param string              $requested_version
289
     * @return mixed
290
     */
291
    public static function prepareFieldValueForJson($field_obj, $original_value, $requested_version)
292
    {
293
        if ($original_value === EE_INF) {
294
            $new_value = ModelDataTranslator::EE_INF_IN_REST;
295
        } elseif ($field_obj instanceof EE_Datetime_Field) {
296
            if (is_string($original_value)) {
297
                // did they submit a string of a unix timestamp?
298
                if (is_numeric($original_value)) {
299
                    $datetime_obj = new \DateTime();
300
                    $datetime_obj->setTimestamp((int) $original_value);
301
                } else {
302
                    // first, check if its a MySQL timestamp in GMT
303
                    $datetime_obj = \DateTime::createFromFormat('Y-m-d H:i:s', $original_value);
304
                }
305
                if (! $datetime_obj instanceof \DateTime) {
306
                    // so it's not a unix timestamp or a MySQL timestamp. Maybe its in the field's date/time format?
307
                    $datetime_obj = $field_obj->prepare_for_set($original_value);
308
                }
309
                $original_value = $datetime_obj;
310
            }
311
            if ($original_value instanceof \DateTime) {
312
                $new_value = $original_value->format('Y-m-d H:i:s');
313
            } elseif (is_int($original_value) || is_float($original_value)) {
314
                $new_value = date('Y-m-d H:i:s', $original_value);
315
            } elseif ($original_value === null || $original_value === '') {
316
                $new_value = null;
317
            } else {
318
                // so it's not a datetime object, unix timestamp (as string or int),
319
                // MySQL timestamp, or even a string in the field object's format. So no idea what it is
320
                throw new \EE_Error(
321
                    sprintf(
322
                        esc_html__(
323
                        // @codingStandardsIgnoreStart
324
                            '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".',
325
                            // @codingStandardsIgnoreEnd
326
                            'event_espresso'
327
                        ),
328
                        $original_value,
329
                        $field_obj->get_name(),
330
                        $field_obj->get_model_name(),
331
                        $field_obj->get_time_format() . ' ' . $field_obj->get_time_format()
332
                    )
333
                );
334
            }
335
            if ($new_value !== null) {
336
                $new_value = mysql_to_rfc3339($new_value);
337
            }
338
        } else {
339
            $new_value = $original_value;
340
        }
341
        // are we about to send an object? just don't. We have no good way to represent it in JSON.
342
        // can't just check using is_object() because that missed PHP incomplete objects
343
        if (! ModelDataTranslator::isRepresentableInJson($new_value)) {
344
            $new_value = array(
345
                'error_code'    => 'php_object_not_return',
346
                'error_message' => esc_html__(
347
                    'The value of this field in the database is a PHP object, which can\'t be represented in JSON.',
348
                    'event_espresso'
349
                ),
350
            );
351
        }
352
        return apply_filters(
353
            'FHEE__EventEspresso\core\libraries\rest_api\Model_Data_Translator__prepare_field_for_rest_api',
354
            $new_value,
355
            $field_obj,
356
            $original_value,
357
            $requested_version
358
        );
359
    }
360
361
362
    /**
363
     * Prepares condition-query-parameters (like what's in where and having) from
364
     * the format expected in the API to use in the models
365
     *
366
     * @param array $inputted_query_params_of_this_type
367
     * @param EEM_Base $model
368
     * @param string $requested_version
369
     * @param boolean $writing whether this data will be written to the DB, or if we're just building a query.
370
     *                          If we're writing to the DB, we don't expect any operators, or any logic query
371
     *                          parameters, and we also won't accept serialized data unless the current user has
372
     *                          unfiltered_html.
373
     * @return array
374
     * @throws DomainException
375
     * @throws EE_Error
376
     * @throws RestException
377
     * @throws \EventEspresso\core\exceptions\InvalidDataTypeException
378
     * @throws \EventEspresso\core\exceptions\InvalidInterfaceException
379
     * @throws \InvalidArgumentException
380
     */
381
    public static function prepareConditionsQueryParamsForModels(
382
        $inputted_query_params_of_this_type,
383
        EEM_Base $model,
384
        $requested_version,
385
        $writing = false
386
    ) {
387
        $query_param_for_models = array();
388
        foreach ($inputted_query_params_of_this_type as $query_param_key => $query_param_value) {
389
            list(
390
                $field,
391
                $query_param_key,
392
                $query_param_sans_stars,
393
                $timezone,
394
                $is_gmt_datetime_field
395
                ) = self::determineFieldAndTimezone(
396
                    $model,
397
                    $query_param_key
398
                );
399
            if ($field instanceof EE_Model_Field_Base) {
400
                $translated_value = self::determineConditionsQueryParameterValue(
401
                    $model,
402
                    $field,
403
                    $requested_version,
404
                    $timezone,
405
                    $query_param_key,
406
                    $query_param_value,
407
                    $writing
408
                );
409
                if ((isset($query_param_for_models[ $query_param_key ]) && $is_gmt_datetime_field)
410
                    || $translated_value === null
411
                ) {
412
                    // they have already provided a non-gmt field, ignore the gmt one. That's what WP core
413
                    // currently does (they might change it though). See https://core.trac.wordpress.org/ticket/39954
414
                    // OR we couldn't create a translated value from their input
415
                    continue;
416
                }
417
                $query_param_for_models[ $query_param_key ] = $translated_value;
418
            } else {
419
                $nested_query_params = self::determineNestedConditionQueryParameters(
420
                    $model,
421
                    $query_param_key,
422
                    $query_param_sans_stars,
423
                    $query_param_value,
424
                    $requested_version,
425
                    $writing
426
                );
427
                if ($nested_query_params) {
428
                    $query_param_for_models[$query_param_key] = $nested_query_params;
429
                }
430
            }
431
        }
432
        return $query_param_for_models;
433
    }
434
435
    /**
436
     * If the query param isn't for a field, it must be a nested query parameter which requires different logic.
437
     * @since $VID:$
438
     * @param EEM_Base $model
439
     * @param $query_param_key
440
     * @param $query_param_sans_stars
441
     * @param $query_param_value
442
     * @param $requested_version
443
     * @param $writing
444
     * @return array
445
     * @throws DomainException
446
     * @throws EE_Error
447
     * @throws RestException
448
     */
449
    private static function determineNestedConditionQueryParameters(
450
        EEM_Base $model,
451
        $query_param_key,
452
        $query_param_sans_stars,
453
        $query_param_value,
454
        $requested_version,
455
        $writing
456
    ) {
457
458
        // so this param doesn't correspond to a field eh?
459
        if ($writing) {
460
            // always tell API clients about invalid parameters when they're creating data. Otherwise,
461
            // they are probably going to create invalid data
462
            throw new RestException(
463
                'invalid_field',
464
                sprintf(
465
                    /* translators: 1: variable name */
466
                    esc_html__('You have provided an invalid parameter: "%1$s"', 'event_espresso'),
467
                    $query_param_key
468
                )
469
            );
470
        } else {
471
            // so it's not for a field, is it a logic query param key?
472
            if (in_array(
473
                $query_param_sans_stars,
474
                $model->logic_query_param_keys()
475
            )) {
476
                return ModelDataTranslator::prepareConditionsQueryParamsForModels(
477
                    $query_param_value,
478
                    $model,
479
                    $requested_version
480
                );
481 View Code Duplication
            } elseif (defined('EE_REST_API_DEBUG_MODE') && EE_REST_API_DEBUG_MODE) {
482
                // only tell API clients they got it wrong if we're in debug mode
483
                // otherwise try our best ot fulfill their request by ignoring this invalid data
484
                throw new RestException(
485
                    'invalid_parameter',
486
                    sprintf(
487
                        /* translators: 1: variable name */
488
                        esc_html__(
489
                            'You provided an invalid parameter, with key "%1$s"',
490
                            'event_espresso'
491
                        ),
492
                        $query_param_key
493
                    ),
494
                    array(
495
                        'status' => 400,
496
                    )
497
                );
498
            }
499
        }
500
        return null;
501
    }
502
503
    /**
504
     * Determines what field, query param name, and query param name without stars, and timezone to use.
505
     * @since $VID:$
506
     * @param EEM_Base $model
507
     * @param $query_param_sans_stars
508
     * @return array {
509
     * @type EE_Model_Field_Base $field
510
     * @type string $query_param_key
511
     * @type string $query_param_sans_stars
512
     * @type string $timezone
513
     * @type boolean $is_gmt_datetime_field
514
     * }
515
     * @throws EE_Error
516
     * @throws \EventEspresso\core\exceptions\InvalidDataTypeException
517
     * @throws \EventEspresso\core\exceptions\InvalidInterfaceException
518
     * @throws \InvalidArgumentException
519
     */
520
    private static function determineFieldAndTimezone(EEM_Base $model, $query_param_key)
521
    {
522
        $query_param_sans_stars = ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey(
523
            $query_param_key
524
        );
525
        $is_gmt_datetime_field = false;
526
        $field = ModelDataTranslator::deduceFieldFromQueryParam(
527
            $query_param_sans_stars,
528
            $model
529
        );
530
        // double-check is it a *_gmt field?
531
        if (! $field instanceof EE_Model_Field_Base
532
            && ModelDataTranslator::isGmtDateFieldName($query_param_sans_stars)
533
        ) {
534
            // yep, take off '_gmt', and find the field
535
            $query_param_key = ModelDataTranslator::removeGmtFromFieldName($query_param_sans_stars);
536
            $field = ModelDataTranslator::deduceFieldFromQueryParam(
537
                $query_param_key,
538
                $model
539
            );
540
            $timezone = 'UTC';
541
            $is_gmt_datetime_field = true;
542
        } elseif ($field instanceof EE_Datetime_Field) {
543
            // so it's not a GMT field. Set the timezone on the model to the default
544
            $timezone = \EEH_DTT_Helper::get_valid_timezone_string();
545
        } else {
546
            // just keep using what's already set for the timezone
547
            $timezone = $model->get_timezone();
548
        }
549
        return array(
550
            $field,
551
            $query_param_key,
552
            $query_param_sans_stars,
553
            $timezone,
554
            $is_gmt_datetime_field
555
        );
556
    }
557
558
    /**
559
     * Given a ton of input, determines the value to use for the models.
560
     * @since $VID:$
561
     * @param EEM_Base $model
562
     * @param EE_Model_Field_Base $field
563
     * @param $requested_version
564
     * @param $timezone
565
     * @param $query_param_key
566
     * @param $query_param_value
567
     * @param bool $writing
568
     * @return array|null
569
     * @throws DomainException
570
     * @throws EE_Error
571
     * @throws RestException
572
     */
573
    private static function determineConditionsQueryParameterValue(
574
        EEM_Base $model,
575
        EE_Model_Field_Base $field,
576
        $requested_version,
577
        $timezone,
578
        $query_param_key,
579
        $query_param_value,
580
        $writing = false
581
    ) {
582
        if (! $writing && is_array($query_param_value)) {
583
            if (! \EEH_Array::is_array_numerically_and_sequentially_indexed($query_param_value)) {
584
                if (is_array($query_param_value)
585
                    && count($query_param_value) === 1
586
                    && array_key_exists(
587
                        key($query_param_value),
588
                        $model->valid_operators()
589
                    )
590
                ) {
591
                    $sub_array_value =  reset($query_param_value);
592
                    $sub_array_key = key($query_param_value);
593
                    // they're doing something like "&where[EVT_ID][IN]=1,2,3" or "&where[EVT_ID][>]=5"
594
                    if (array_key_exists(
595
                        $sub_array_key,
596
                        array_merge(
597
                            $model->valid_in_style_operators(),
598
                            $model->valid_between_style_operators()
599
                        )
600
                    )) {
601
                        // the value should be JSON or CSV
602
                        $values = json_decode($sub_array_value);
603
                        if (! is_array($values)) {
604
                            $values = array_filter(
605
                                array_map(
606
                                    'trim',
607
                                    explode(
608
                                        ',',
609
                                        $sub_array_value
610
                                    )
611
                                )
612
                            );
613
                        }
614
                        $query_param_value = array(
615
                            $sub_array_key,
616
                            $values
617
                        );
618
                    } elseif (array_key_exists(
619
                        $sub_array_key,
620
                        $model->valid_null_style_operators()
621
                    )) {
622
                        $query_param_value = array($sub_array_key);
623
                    } else {
624
                        $query_param_value = array($sub_array_key, $sub_array_value);
625
                    }
626 View Code Duplication
                } elseif (defined('EE_REST_API_DEBUG_MODE') && EE_REST_API_DEBUG_MODE) {
627
                    throw new RestException(
628
                        'numerically_indexed_array_of_values_only',
629
                        sprintf(
630
                            /* translators: 1: variable name*/
631
                            esc_html__(
632
                                'The array provided for the parameter "%1$s" should be numerically indexed.',
633
                                'event_espresso'
634
                            ),
635
                            $query_param_key
636
                        ),
637
                        array(
638
                            'status' => 400,
639
                        )
640
                    );
641
                }
642
            }
643
            $valid_operators = $model->valid_operators();
644
            // did they specify an operator?
645
            if (isset($query_param_value[0])
646
                && isset($valid_operators[ $query_param_value[0] ])
647
            ) {
648
                $sub_array_key = $query_param_value[0];
649
                $translated_value = array($sub_array_key);
650
                if (array_key_exists($sub_array_key, $model->valid_in_style_operators())
651
                    && isset($query_param_value[1])
652
                    && ! isset($query_param_value[2])
653
                ) {
654
                    $translated_value[] = ModelDataTranslator::prepareFieldValuesFromJson(
655
                        $field,
656
                        $query_param_value[1],
657
                        $requested_version,
658
                        $timezone
659
                    );
660
                } elseif (array_key_exists($sub_array_key, $model->valid_between_style_operators())
661
                    && isset($query_param_value[1])
662
                    && is_array($query_param_value[1])
663
                    && isset($query_param_key[1][0], $query_param_value[1][1])
664
                    && ! isset($query_param_value[1][2])
665
                    && ! isset($query_param_value[2])
666
                ) {
667
                    $translated_value[] = array(
668
                        ModelDataTranslator::prepareFieldValuesFromJson(
669
                            $field,
670
                            $query_param_value[1][0],
671
                            $requested_version,
672
                            $timezone
673
                        ),
674
                        ModelDataTranslator::prepareFieldValuesFromJson(
675
                            $field,
676
                            $query_param_value[1][1],
677
                            $requested_version,
678
                            $timezone
679
                        )
680
                    );
681
                } elseif (array_key_exists($sub_array_key, $model->valid_like_style_operators())
682
                    && isset($query_param_value[1])
683
                    && ! isset($query_param_value[2])
684
                ) {
685
                    // we want to leave this value mostly-as-is (eg don't force it to be a float
686
                    // or a boolean or an enum value. Leave it as-is with wildcards etc)
687
                    // but do verify it at least doesn't have any serialized data
688
                    ModelDataTranslator::throwExceptionIfContainsSerializedData($query_param_value[1]);
689
                    $translated_value[] = $query_param_value[1];
690
                } elseif (array_key_exists($sub_array_key, $model->valid_null_style_operators())
691
                    && ! isset($query_param_value[1])) {
692
                    // no arguments should have been provided, so don't look for any
693
                } elseif (isset($query_param_value[1])
694
                    && ! isset($query_param_value[2])
695
                    && ! array_key_exists(
696
                        $sub_array_key,
697
                        array_merge(
698
                            $model->valid_in_style_operators(),
699
                            $model->valid_null_style_operators(),
700
                            $model->valid_like_style_operators(),
701
                            $model->valid_between_style_operators()
702
                        )
703
                    )
704
                ) {
705
                    // it's a valid operator, but none of the exceptions. Treat it normally.
706
                    $translated_value[] = ModelDataTranslator::prepareFieldValuesFromJson(
707
                        $field,
708
                        $query_param_value[1],
709
                        $requested_version,
710
                        $timezone
711
                    );
712 View Code Duplication
                } else {
713
                    // so they provided a valid operator, but wrong number of arguments
714
                    if (defined('EE_REST_API_DEBUG_MODE') && EE_REST_API_DEBUG_MODE) {
715
                        throw new RestException(
716
                            'wrong_number_of_arguments',
717
                            sprintf(
718
                                esc_html__(
719
                                    'The operator you provided, "%1$s" had the wrong number of arguments',
720
                                    'event_espresso'
721
                                ),
722
                                $sub_array_key
723
                            ),
724
                            array(
725
                                'status' => 400,
726
                            )
727
                        );
728
                    }
729
                    $translated_value = null;
730
                }
731 View Code Duplication
            } else {
732
                // so they didn't provide a valid operator
733
                if (defined('EE_REST_API_DEBUG_MODE') && EE_REST_API_DEBUG_MODE) {
734
                    throw new RestException(
735
                        'invalid_operator',
736
                        sprintf(
737
                            esc_html__(
738
                                'You provided an invalid parameter, with key "%1$s" and value "%2$s"',
739
                                'event_espresso'
740
                            ),
741
                            $query_param_key,
742
                            $query_param_value
743
                        ),
744
                        array(
745
                            'status' => 400,
746
                        )
747
                    );
748
                }
749
                // if we aren't in debug mode, then just try our best to fulfill the user's request
750
                $translated_value = null;
751
            }
752
        } else {
753
            $translated_value = ModelDataTranslator::prepareFieldValueFromJson(
754
                $field,
755
                $query_param_value,
756
                $requested_version,
757
                $timezone
758
            );
759
        }
760
        return $translated_value;
761
    }
762
763
764
    /**
765
     * Mostly checks if the last 4 characters are "_gmt", indicating its a
766
     * gmt date field name
767
     *
768
     * @param string $field_name
769
     * @return boolean
770
     */
771
    public static function isGmtDateFieldName($field_name)
772
    {
773
        return substr(
774
            ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey($field_name),
775
            -4,
776
            4
777
        ) === '_gmt';
778
    }
779
780
781
    /**
782
     * Removes the last "_gmt" part of a field name (and if there is no "_gmt" at the end, leave it alone)
783
     *
784
     * @param string $field_name
785
     * @return string
786
     */
787
    public static function removeGmtFromFieldName($field_name)
788
    {
789
        if (! ModelDataTranslator::isGmtDateFieldName($field_name)) {
790
            return $field_name;
791
        }
792
        $query_param_sans_stars = ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey(
793
            $field_name
794
        );
795
        $query_param_sans_gmt_and_sans_stars = substr(
796
            $query_param_sans_stars,
797
            0,
798
            strrpos(
799
                $field_name,
800
                '_gmt'
801
            )
802
        );
803
        return str_replace($query_param_sans_stars, $query_param_sans_gmt_and_sans_stars, $field_name);
804
    }
805
806
807
    /**
808
     * Takes a field name from the REST API and prepares it for the model querying
809
     *
810
     * @param string $field_name
811
     * @return string
812
     */
813
    public static function prepareFieldNameFromJson($field_name)
814
    {
815
        if (ModelDataTranslator::isGmtDateFieldName($field_name)) {
816
            return ModelDataTranslator::removeGmtFromFieldName($field_name);
817
        }
818
        return $field_name;
819
    }
820
821
822
    /**
823
     * Takes array of field names from REST API and prepares for models
824
     *
825
     * @param array $field_names
826
     * @return array of field names (possibly include model prefixes)
827
     */
828
    public static function prepareFieldNamesFromJson(array $field_names)
829
    {
830
        $new_array = array();
831
        foreach ($field_names as $key => $field_name) {
832
            $new_array[ $key ] = ModelDataTranslator::prepareFieldNameFromJson($field_name);
833
        }
834
        return $new_array;
835
    }
836
837
838
    /**
839
     * Takes array where array keys are field names (possibly with model path prefixes)
840
     * from the REST API and prepares them for model querying
841
     *
842
     * @param array $field_names_as_keys
843
     * @return array
844
     */
845
    public static function prepareFieldNamesInArrayKeysFromJson(array $field_names_as_keys)
846
    {
847
        $new_array = array();
848
        foreach ($field_names_as_keys as $field_name => $value) {
849
            $new_array[ ModelDataTranslator::prepareFieldNameFromJson($field_name) ] = $value;
850
        }
851
        return $new_array;
852
    }
853
854
855
    /**
856
     * Prepares an array of model query params for use in the REST API
857
     *
858
     * @param array    $model_query_params
859
     * @param EEM_Base $model
860
     * @param string   $requested_version  eg "4.8.36". If null is provided, defaults to the latest release of the EE4
861
     *                                     REST API
862
     * @return array which can be passed into the EE4 REST API when querying a model resource
863
     * @throws EE_Error
864
     */
865
    public static function prepareQueryParamsForRestApi(
866
        array $model_query_params,
867
        EEM_Base $model,
868
        $requested_version = null
869
    ) {
870
        if ($requested_version === null) {
871
            $requested_version = \EED_Core_Rest_Api::latest_rest_api_version();
872
        }
873
        $rest_query_params = $model_query_params;
874 View Code Duplication
        if (isset($model_query_params[0])) {
875
            $rest_query_params['where'] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
876
                $model_query_params[0],
877
                $model,
878
                $requested_version
879
            );
880
            unset($rest_query_params[0]);
881
        }
882 View Code Duplication
        if (isset($model_query_params['having'])) {
883
            $rest_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
884
                $model_query_params['having'],
885
                $model,
886
                $requested_version
887
            );
888
        }
889
        return apply_filters(
890
            'FHEE__EventEspresso\core\libraries\rest_api\Model_Data_Translator__prepare_query_params_for_rest_api',
891
            $rest_query_params,
892
            $model_query_params,
893
            $model,
894
            $requested_version
895
        );
896
    }
897
898
899
    /**
900
     * Prepares all the sub-conditions query parameters (eg having or where conditions) for use in the rest api
901
     *
902
     * @param array    $inputted_query_params_of_this_type  eg like the "where" or "having" conditions query params
903
     *                                                      passed into EEM_Base::get_all()
904
     * @param EEM_Base $model
905
     * @param string   $requested_version                   eg "4.8.36"
906
     * @return array ready for use in the rest api query params
907
     * @throws EE_Error
908
     * @throws ObjectDetectedException if somehow a PHP object were in the query params' values,
909
     *                                                      (which would be really unusual)
910
     */
911
    public static function prepareConditionsQueryParamsForRestApi(
912
        $inputted_query_params_of_this_type,
913
        EEM_Base $model,
914
        $requested_version
915
    ) {
916
        $query_param_for_models = array();
917
        foreach ($inputted_query_params_of_this_type as $query_param_key => $query_param_value) {
918
            $field = ModelDataTranslator::deduceFieldFromQueryParam(
919
                ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey($query_param_key),
920
                $model
921
            );
922
            if ($field instanceof EE_Model_Field_Base) {
923
                // did they specify an operator?
924
                if (is_array($query_param_value)) {
925
                    $op = $query_param_value[0];
926
                    $translated_value = array($op);
927
                    if (isset($query_param_value[1])) {
928
                        $value = $query_param_value[1];
929
                        $translated_value[1] = ModelDataTranslator::prepareFieldValuesForJson(
930
                            $field,
931
                            $value,
932
                            $requested_version
933
                        );
934
                    }
935
                } else {
936
                    $translated_value = ModelDataTranslator::prepareFieldValueForJson(
937
                        $field,
938
                        $query_param_value,
939
                        $requested_version
940
                    );
941
                }
942
                $query_param_for_models[ $query_param_key ] = $translated_value;
943
            } else {
944
                // so it's not for a field, assume it's a logic query param key
945
                $query_param_for_models[ $query_param_key ] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
946
                    $query_param_value,
947
                    $model,
948
                    $requested_version
949
                );
950
            }
951
        }
952
        return $query_param_for_models;
953
    }
954
955
956
    /**
957
     * @param $condition_query_param_key
958
     * @return string
959
     */
960 View Code Duplication
    public static function removeStarsAndAnythingAfterFromConditionQueryParamKey($condition_query_param_key)
961
    {
962
        $pos_of_star = strpos($condition_query_param_key, '*');
963
        if ($pos_of_star === false) {
964
            return $condition_query_param_key;
965
        } else {
966
            $condition_query_param_sans_star = substr($condition_query_param_key, 0, $pos_of_star);
967
            return $condition_query_param_sans_star;
968
        }
969
    }
970
971
972
    /**
973
     * Takes the input parameter and finds the model field that it indicates.
974
     *
975
     * @param string   $query_param_name like Registration.Transaction.TXN_ID, Event.Datetime.start_time, or REG_ID
976
     * @param EEM_Base $model
977
     * @return EE_Model_Field_Base
978
     * @throws EE_Error
979
     */
980
    public static function deduceFieldFromQueryParam($query_param_name, EEM_Base $model)
981
    {
982
        // ok, now proceed with deducing which part is the model's name, and which is the field's name
983
        // which will help us find the database table and column
984
        $query_param_parts = explode('.', $query_param_name);
985
        if (empty($query_param_parts)) {
986
            throw new EE_Error(
987
                sprintf(
988
                    __(
989
                        '_extract_column_name is empty when trying to extract column and table name from %s',
990
                        'event_espresso'
991
                    ),
992
                    $query_param_name
993
                )
994
            );
995
        }
996
        $number_of_parts = count($query_param_parts);
997
        $last_query_param_part = $query_param_parts[ count($query_param_parts) - 1 ];
998
        if ($number_of_parts === 1) {
999
            $field_name = $last_query_param_part;
1000
        } else {// $number_of_parts >= 2
1001
            // the last part is the column name, and there are only 2parts. therefore...
1002
            $field_name = $last_query_param_part;
1003
            $model = \EE_Registry::instance()->load_model($query_param_parts[ $number_of_parts - 2 ]);
1004
        }
1005
        try {
1006
            return $model->field_settings_for($field_name, false);
0 ignored issues
show
Bug introduced by
It seems like $model is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
1007
        } catch (EE_Error $e) {
1008
            return null;
1009
        }
1010
    }
1011
1012
1013
    /**
1014
     * Returns true if $data can be easily represented in JSON.
1015
     * Basically, objects and resources can't be represented in JSON easily.
1016
     *
1017
     * @param mixed $data
1018
     * @return bool
1019
     */
1020
    protected static function isRepresentableInJson($data)
1021
    {
1022
        return is_scalar($data)
1023
               || is_array($data)
1024
               || is_null($data);
1025
    }
1026
}
1027