Completed
Branch BUG/ticket-embed-expired-event... (80a031)
by
unknown
21:16 queued 09:22
created

Read::includeRequestedModels()   C

Complexity

Conditions 10
Paths 22

Size

Total Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
nc 22
nop 5
dl 0
loc 64
rs 6.9187
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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\controllers\model;
4
5
use DateTimeZone;
6
use EE_Model_Field_Base;
7
use EEH_DTT_Helper;
8
use EEM_Soft_Delete_Base;
9
use EventEspresso\core\exceptions\InvalidDataTypeException;
10
use EventEspresso\core\exceptions\InvalidInterfaceException;
11
use EventEspresso\core\exceptions\RestPasswordIncorrectException;
12
use EventEspresso\core\exceptions\RestPasswordRequiredException;
13
use EventEspresso\core\libraries\rest_api\ObjectDetectedException;
14
use EventEspresso\core\services\loaders\LoaderFactory;
15
use Exception;
16
use InvalidArgumentException;
17
use ReflectionException;
18
use stdClass;
19
use WP_Error;
20
use WP_REST_Request;
21
use EventEspresso\core\libraries\rest_api\Capabilities;
22
use EventEspresso\core\libraries\rest_api\CalculatedModelFields;
23
use EventEspresso\core\libraries\rest_api\RestException;
24
use EventEspresso\core\libraries\rest_api\ModelDataTranslator;
25
use EventEspresso\core\entities\models\JsonModelSchema;
26
use EE_Belongs_To_Relation;
27
use EE_Datetime_Field;
28
use EE_Error;
29
use EE_Registry;
30
use EED_Core_Rest_Api;
31
use EEH_Inflector;
32
use EEM_Base;
33
use EEM_CPT_Base;
34
use WP_REST_Response;
35
36
/**
37
 * Read controller for models
38
 * Handles requests relating to GET-ting model information
39
 *
40
 * @package               Event Espresso
41
 * @subpackage
42
 * @author                Mike Nelson
43
 */
44
class Read extends Base
45
{
46
47
48
    /**
49
     * @var CalculatedModelFields
50
     */
51
    protected $fields_calculator;
52
53
54
    /**
55
     * Read constructor.
56
     * @param CalculatedModelFields $fields_calculator
57
     */
58
    public function __construct(CalculatedModelFields $fields_calculator)
59
    {
60
        parent::__construct();
61
        $this->fields_calculator = $fields_calculator;
62
    }
63
64
65
    /**
66
     * Handles requests to get all (or a filtered subset) of entities for a particular model
67
     *
68
     * @param WP_REST_Request $request
69
     * @param string $version
70
     * @param string $model_name
71
     * @return WP_REST_Response|WP_Error
72
     * @throws InvalidArgumentException
73
     * @throws InvalidDataTypeException
74
     * @throws InvalidInterfaceException
75
     */
76 View Code Duplication
    public static function handleRequestGetAll(WP_REST_Request $request, $version, $model_name)
77
    {
78
        $controller = LoaderFactory::getLoader()->getNew('EventEspresso\core\libraries\rest_api\controllers\model\Read');
79
        try {
80
            $controller->setRequestedVersion($version);
81
            if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
82
                return $controller->sendResponse(
83
                    new WP_Error(
84
                        'endpoint_parsing_error',
85
                        sprintf(
86
                            __(
87
                                'There is no model for endpoint %s. Please contact event espresso support',
88
                                'event_espresso'
89
                            ),
90
                            $model_name
91
                        )
92
                    )
93
                );
94
            }
95
            return $controller->sendResponse(
96
                $controller->getEntitiesFromModel(
97
                    $controller->getModelVersionInfo()->loadModel($model_name),
98
                    $request
99
                )
100
            );
101
        } catch (Exception $e) {
102
            return $controller->sendResponse($e);
103
        }
104
    }
105
106
107
    /**
108
     * Prepares and returns schema for any OPTIONS request.
109
     *
110
     * @param string $version The API endpoint version being used.
111
     * @param string $model_name Something like `Event` or `Registration`
112
     * @return array
113
     * @throws InvalidArgumentException
114
     * @throws InvalidDataTypeException
115
     * @throws InvalidInterfaceException
116
     */
117
    public static function handleSchemaRequest($version, $model_name)
118
    {
119
        $controller = LoaderFactory::getLoader()->getNew('EventEspresso\core\libraries\rest_api\controllers\model\Read');
120
        try {
121
            $controller->setRequestedVersion($version);
122
            if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
123
                return array();
124
            }
125
            // get the model for this version
126
            $model = $controller->getModelVersionInfo()->loadModel($model_name);
127
            $model_schema = new JsonModelSchema($model, LoaderFactory::getLoader()->getShared('EventEspresso\core\libraries\rest_api\CalculatedModelFields'));
128
            return $model_schema->getModelSchemaForRelations(
129
                $controller->getModelVersionInfo()->relationSettings($model),
130
                $controller->customizeSchemaForRestResponse(
131
                    $model,
132
                    $model_schema->getModelSchemaForFields(
133
                        $controller->getModelVersionInfo()->fieldsOnModelInThisVersion($model),
134
                        $model_schema->getInitialSchemaStructure()
135
                    )
136
                )
137
            );
138
        } catch (Exception $e) {
139
            return array();
140
        }
141
    }
142
143
144
    /**
145
     * This loops through each field in the given schema for the model and does the following:
146
     * - add any extra fields that are REST API specific and related to existing fields.
147
     * - transform default values into the correct format for a REST API response.
148
     *
149
     * @param EEM_Base $model
150
     * @param array    $schema
151
     * @return array  The final schema.
152
     */
153
    protected function customizeSchemaForRestResponse(EEM_Base $model, array $schema)
154
    {
155
        foreach ($this->getModelVersionInfo()->fieldsOnModelInThisVersion($model) as $field_name => $field) {
156
            $schema = $this->translateDefaultsForRestResponse(
157
                $field_name,
158
                $field,
159
                $this->maybeAddExtraFieldsToSchema($field_name, $field, $schema)
160
            );
161
        }
162
        return $schema;
163
    }
164
165
166
    /**
167
     * This is used to ensure that the 'default' value set in the schema response is formatted correctly for the REST
168
     * response.
169
     *
170
     * @param                      $field_name
171
     * @param EE_Model_Field_Base  $field
172
     * @param array                $schema
173
     * @return array
174
     * @throws ObjectDetectedException if a default value has a PHP object, which should never do (and if we
175
     * did, let's know about it ASAP, so let the exception bubble up)
176
     */
177
    protected function translateDefaultsForRestResponse($field_name, EE_Model_Field_Base $field, array $schema)
178
    {
179
        if (isset($schema['properties'][ $field_name ]['default'])) {
180
            if (is_array($schema['properties'][ $field_name ]['default'])) {
181
                foreach ($schema['properties'][ $field_name ]['default'] as $default_key => $default_value) {
182 View Code Duplication
                    if ($default_key === 'raw') {
183
                        $schema['properties'][ $field_name ]['default'][ $default_key ] =
184
                            ModelDataTranslator::prepareFieldValueForJson(
185
                                $field,
186
                                $default_value,
187
                                $this->getModelVersionInfo()->requestedVersion()
188
                            );
189
                    }
190
                }
191 View Code Duplication
            } else {
192
                $schema['properties'][ $field_name ]['default'] = ModelDataTranslator::prepareFieldValueForJson(
193
                    $field,
194
                    $schema['properties'][ $field_name ]['default'],
195
                    $this->getModelVersionInfo()->requestedVersion()
196
                );
197
            }
198
        }
199
        return $schema;
200
    }
201
202
203
    /**
204
     * Adds additional fields to the schema
205
     * The REST API returns a GMT value field for each datetime field in the resource.  Thus the description about this
206
     * needs to be added to the schema.
207
     *
208
     * @param                      $field_name
209
     * @param EE_Model_Field_Base  $field
210
     * @param array                $schema
211
     * @return array
212
     */
213
    protected function maybeAddExtraFieldsToSchema($field_name, EE_Model_Field_Base $field, array $schema)
214
    {
215
        if ($field instanceof EE_Datetime_Field) {
216
            $schema['properties'][ $field_name . '_gmt' ] = $field->getSchema();
217
            // modify the description
218
            $schema['properties'][ $field_name . '_gmt' ]['description'] = sprintf(
219
                esc_html__('%s - the value for this field is in GMT.', 'event_espresso'),
220
                wp_specialchars_decode($field->get_nicename(), ENT_QUOTES)
221
            );
222
        }
223
        return $schema;
224
    }
225
226
227
    /**
228
     * Used to figure out the route from the request when a `WP_REST_Request` object is not available
229
     *
230
     * @return string
231
     */
232
    protected function getRouteFromRequest()
233
    {
234
        if (isset($GLOBALS['wp'])
235
            && $GLOBALS['wp'] instanceof \WP
236
            && isset($GLOBALS['wp']->query_vars['rest_route'])
237
        ) {
238
            return $GLOBALS['wp']->query_vars['rest_route'];
239
        } else {
240
            return isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '/';
241
        }
242
    }
243
244
245
    /**
246
     * Gets a single entity related to the model indicated in the path and its id
247
     *
248
     * @param WP_REST_Request $request
249
     * @param string $version
250
     * @param string $model_name
251
     * @return WP_REST_Response|WP_Error
252
     * @throws InvalidDataTypeException
253
     * @throws InvalidInterfaceException
254
     * @throws InvalidArgumentException
255
     */
256 View Code Duplication
    public static function handleRequestGetOne(WP_REST_Request $request, $version, $model_name)
257
    {
258
        $controller = LoaderFactory::getLoader()->getNew('EventEspresso\core\libraries\rest_api\controllers\model\Read');
259
        try {
260
            $controller->setRequestedVersion($version);
261
            if (! $controller->getModelVersionInfo()->isModelNameInThisVersion($model_name)) {
262
                return $controller->sendResponse(
263
                    new WP_Error(
264
                        'endpoint_parsing_error',
265
                        sprintf(
266
                            __(
267
                                'There is no model for endpoint %s. Please contact event espresso support',
268
                                'event_espresso'
269
                            ),
270
                            $model_name
271
                        )
272
                    )
273
                );
274
            }
275
            return $controller->sendResponse(
276
                $controller->getEntityFromModel(
277
                    $controller->getModelVersionInfo()->loadModel($model_name),
278
                    $request
279
                )
280
            );
281
        } catch (Exception $e) {
282
            return $controller->sendResponse($e);
283
        }
284
    }
285
286
287
    /**
288
     * Gets all the related entities (or if its a belongs-to relation just the one)
289
     * to the item with the given id
290
     *
291
     * @param WP_REST_Request $request
292
     * @param string $version
293
     * @param string $model_name
294
     * @param string $related_model_name
295
     * @return WP_REST_Response|WP_Error
296
     * @throws InvalidDataTypeException
297
     * @throws InvalidInterfaceException
298
     * @throws InvalidArgumentException
299
     */
300
    public static function handleRequestGetRelated(
301
        WP_REST_Request $request,
302
        $version,
303
        $model_name,
304
        $related_model_name
305
    ) {
306
        $controller = LoaderFactory::getLoader()->getNew('EventEspresso\core\libraries\rest_api\controllers\model\Read');
307
        try {
308
            $controller->setRequestedVersion($version);
309
            $main_model = $controller->validateModel($model_name);
310
            $controller->validateModel($related_model_name);
311
            return $controller->sendResponse(
312
                $controller->getEntitiesFromRelation(
313
                    $request->get_param('id'),
314
                    $main_model->related_settings_for($related_model_name),
315
                    $request
316
                )
317
            );
318
        } catch (Exception $e) {
319
            return $controller->sendResponse($e);
320
        }
321
    }
322
323
324
    /**
325
     * Gets a collection for the given model and filters
326
     *
327
     * @param EEM_Base $model
328
     * @param WP_REST_Request $request
329
     * @return array
330
     * @throws EE_Error
331
     * @throws InvalidArgumentException
332
     * @throws InvalidDataTypeException
333
     * @throws InvalidInterfaceException
334
     * @throws ReflectionException
335
     * @throws RestException
336
     */
337
    public function getEntitiesFromModel($model, $request)
338
    {
339
        $query_params = $this->createModelQueryParams($model, $request->get_params());
340
        if (! Capabilities::currentUserHasPartialAccessTo($model, $query_params['caps'])) {
341
            $model_name_plural = EEH_Inflector::pluralize_and_lower($model->get_this_model_name());
342
            throw new RestException(
343
                sprintf('rest_%s_cannot_list', $model_name_plural),
344
                sprintf(
345
                    __('Sorry, you are not allowed to list %1$s. Missing permissions: %2$s', 'event_espresso'),
346
                    $model_name_plural,
347
                    Capabilities::getMissingPermissionsString($model, $query_params['caps'])
348
                ),
349
                array('status' => 403)
350
            );
351
        }
352
        if (! $request->get_header('no_rest_headers')) {
353
            $this->setHeadersFromQueryParams($model, $query_params);
354
        }
355
        /** @type array $results */
356
        $results = $model->get_all_wpdb_results($query_params);
357
        $nice_results = array();
358
        foreach ($results as $result) {
359
            $nice_results[] =  $this->createEntityFromWpdbResult(
360
                $model,
361
                $result,
362
                $request
363
            );
364
        }
365
        return $nice_results;
366
    }
367
368
369
    /**
370
     * Gets the collection for given relation object
371
     * The same as Read::get_entities_from_model(), except if the relation
372
     * is a HABTM relation, in which case it merges any non-foreign-key fields from
373
     * the join-model-object into the results
374
     *
375
     * @param array $primary_model_query_params query params for finding the item from which
376
     *                                                            relations will be based
377
     * @param \EE_Model_Relation_Base $relation
378
     * @param WP_REST_Request $request
379
     * @return array
380
     * @throws EE_Error
381
     * @throws InvalidArgumentException
382
     * @throws InvalidDataTypeException
383
     * @throws InvalidInterfaceException
384
     * @throws ReflectionException
385
     * @throws RestException
386
     * @throws \EventEspresso\core\exceptions\ModelConfigurationException
387
     */
388
    protected function getEntitiesFromRelationUsingModelQueryParams($primary_model_query_params, $relation, $request)
389
    {
390
        $context = $this->validateContext($request->get_param('caps'));
391
        $model = $relation->get_this_model();
392
        $related_model = $relation->get_other_model();
393
        if (! isset($primary_model_query_params[0])) {
394
            $primary_model_query_params[0] = array();
395
        }
396
        // check if they can access the 1st model object
397
        $primary_model_query_params = array(
398
            0       => $primary_model_query_params[0],
399
            'limit' => 1,
400
        );
401
        if ($model instanceof EEM_Soft_Delete_Base) {
402
            $primary_model_query_params = $model->alter_query_params_so_deleted_and_undeleted_items_included(
403
                $primary_model_query_params
404
            );
405
        }
406
        $restricted_query_params = $primary_model_query_params;
407
        $restricted_query_params['caps'] = $context;
408
        $restricted_query_params['limit'] = 1;
409
        $this->setDebugInfo('main model query params', $restricted_query_params);
410
        $this->setDebugInfo('missing caps', Capabilities::getMissingPermissionsString($related_model, $context));
411
        $primary_model_rows = $model->get_all_wpdb_results($restricted_query_params);
412
        $primary_model_row = null;
413
        if (is_array($primary_model_rows)) {
414
            $primary_model_row = reset($primary_model_rows);
415
        }
416
        if (! (
417
            Capabilities::currentUserHasPartialAccessTo($related_model, $context)
418
            && $primary_model_row
419
        )
420
        ) {
421
            if ($relation instanceof EE_Belongs_To_Relation) {
422
                $related_model_name_maybe_plural = strtolower($related_model->get_this_model_name());
423
            } else {
424
                $related_model_name_maybe_plural = EEH_Inflector::pluralize_and_lower(
425
                    $related_model->get_this_model_name()
426
                );
427
            }
428
            throw new RestException(
429
                sprintf('rest_%s_cannot_list', $related_model_name_maybe_plural),
430
                sprintf(
431
                    __(
432
                        'Sorry, you are not allowed to list %1$s related to %2$s. Missing permissions: %3$s',
433
                        'event_espresso'
434
                    ),
435
                    $related_model_name_maybe_plural,
436
                    $relation->get_this_model()->get_this_model_name(),
437
                    implode(
438
                        ',',
439
                        array_keys(
440
                            Capabilities::getMissingPermissions($related_model, $context)
441
                        )
442
                    )
443
                ),
444
                array('status' => 403)
445
            );
446
        }
447
448
        $this->checkPassword(
449
            $model,
450
            $primary_model_row,
451
            $restricted_query_params,
452
            $request
453
        );
454
        $query_params = $this->createModelQueryParams($relation->get_other_model(), $request->get_params());
0 ignored issues
show
Bug introduced by
It seems like $relation->get_other_model() can be null; however, createModelQueryParams() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
455
        foreach ($primary_model_query_params[0] as $where_condition_key => $where_condition_value) {
456
            $query_params[0][ $relation->get_this_model()->get_this_model_name()
457
                              . '.'
458
                              . $where_condition_key ] = $where_condition_value;
459
        }
460
        $query_params['default_where_conditions'] = 'none';
461
        $query_params['caps'] = $context;
462
        if (! $request->get_header('no_rest_headers')) {
463
            $this->setHeadersFromQueryParams($relation->get_other_model(), $query_params);
0 ignored issues
show
Bug introduced by
It seems like $relation->get_other_model() can be null; however, setHeadersFromQueryParams() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
464
        }
465
        /** @type array $results */
466
        $results = $relation->get_other_model()->get_all_wpdb_results($query_params);
467
        $nice_results = array();
468
        foreach ($results as $result) {
469
            $nice_result = $this->createEntityFromWpdbResult(
470
                $relation->get_other_model(),
0 ignored issues
show
Bug introduced by
It seems like $relation->get_other_model() can be null; however, createEntityFromWpdbResult() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
471
                $result,
472
                $request
473
            );
474
            if ($relation instanceof \EE_HABTM_Relation) {
475
                // put the unusual stuff (properties from the HABTM relation) first, and make sure
476
                // if there are conflicts we prefer the properties from the main model
477
                $join_model_result = $this->createEntityFromWpdbResult(
478
                    $relation->get_join_model(),
0 ignored issues
show
Bug introduced by
It seems like $relation->get_join_model() can be null; however, createEntityFromWpdbResult() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
479
                    $result,
480
                    $request
481
                );
482
                $joined_result = array_merge($nice_result, $join_model_result);
483
                // but keep the meta stuff from the main model
484
                if (isset($nice_result['meta'])) {
485
                    $joined_result['meta'] = $nice_result['meta'];
486
                }
487
                $nice_result = $joined_result;
488
            }
489
            $nice_results[] = $nice_result;
490
        }
491
        if ($relation instanceof EE_Belongs_To_Relation) {
492
            return array_shift($nice_results);
493
        } else {
494
            return $nice_results;
495
        }
496
    }
497
498
499
    /**
500
     * Gets the collection for given relation object
501
     * The same as Read::get_entities_from_model(), except if the relation
502
     * is a HABTM relation, in which case it merges any non-foreign-key fields from
503
     * the join-model-object into the results
504
     *
505
     * @param string                  $id the ID of the thing we are fetching related stuff from
506
     * @param \EE_Model_Relation_Base $relation
507
     * @param WP_REST_Request         $request
508
     * @return array
509
     * @throws EE_Error
510
     */
511
    public function getEntitiesFromRelation($id, $relation, $request)
512
    {
513 View Code Duplication
        if (! $relation->get_this_model()->has_primary_key_field()) {
514
            throw new EE_Error(
515
                sprintf(
516
                    __(
517
                    // @codingStandardsIgnoreStart
518
                        'Read::get_entities_from_relation should only be called from a model with a primary key, it was called from %1$s',
519
                        // @codingStandardsIgnoreEnd
520
                        'event_espresso'
521
                    ),
522
                    $relation->get_this_model()->get_this_model_name()
523
                )
524
            );
525
        }
526
        // can we edit that main item?
527
        // if not, show nothing but an error
528
        // otherwise, please proceed
529
        return $this->getEntitiesFromRelationUsingModelQueryParams(
530
            array(
531
                array(
532
                    $relation->get_this_model()->primary_key_name() => $id,
533
                ),
534
            ),
535
            $relation,
536
            $request
537
        );
538
    }
539
540
541
    /**
542
     * Sets the headers that are based on the model and query params,
543
     * like the total records. This should only be called on the original request
544
     * from the client, not on subsequent internal
545
     *
546
     * @param EEM_Base $model
547
     * @param array    $query_params
548
     * @return void
549
     */
550
    protected function setHeadersFromQueryParams($model, $query_params)
551
    {
552
        $this->setDebugInfo('model query params', $query_params);
553
        $this->setDebugInfo(
554
            'missing caps',
555
            Capabilities::getMissingPermissionsString($model, $query_params['caps'])
556
        );
557
        // normally the limit to a 2-part array, where the 2nd item is the limit
558
        if (! isset($query_params['limit'])) {
559
            $query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
560
        }
561
        if (is_array($query_params['limit'])) {
562
            $limit_parts = $query_params['limit'];
563
        } else {
564
            $limit_parts = explode(',', $query_params['limit']);
565
            if (count($limit_parts) == 1) {
566
                $limit_parts = array(0, $limit_parts[0]);
567
            }
568
        }
569
        // remove the group by and having parts of the query, as those will
570
        // make the sql query return an array of values, instead of just a single value
571
        unset($query_params['group_by'], $query_params['having'], $query_params['limit']);
572
        $count = $model->count($query_params, null, true);
573
        $pages = $count / $limit_parts[1];
574
        $this->setResponseHeader('Total', $count, false);
575
        $this->setResponseHeader('PageSize', $limit_parts[1], false);
576
        $this->setResponseHeader('TotalPages', ceil($pages), false);
577
    }
578
579
580
    /**
581
     * Changes database results into REST API entities
582
     *
583
     * @param EEM_Base $model
584
     * @param array $db_row like results from $wpdb->get_results()
585
     * @param WP_REST_Request $rest_request
586
     * @param string $deprecated no longer used
587
     * @return array ready for being converted into json for sending to client
588
     * @throws EE_Error
589
     * @throws RestException
590
     * @throws InvalidDataTypeException
591
     * @throws InvalidInterfaceException
592
     * @throws InvalidArgumentException
593
     * @throws ReflectionException
594
     */
595
    public function createEntityFromWpdbResult($model, $db_row, $rest_request, $deprecated = null)
596
    {
597
        if (! $rest_request instanceof WP_REST_Request) {
0 ignored issues
show
Bug introduced by
The class WP_REST_Request does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
598
            // ok so this was called in the old style, where the 3rd arg was
599
            // $include, and the 4th arg was $context
600
            // now setup the request just to avoid fatal errors, although we won't be able
601
            // to truly make use of it because it's kinda devoid of info
602
            $rest_request = new WP_REST_Request();
603
            $rest_request->set_param('include', $rest_request);
604
            $rest_request->set_param('caps', $deprecated);
605
        }
606
        if ($rest_request->get_param('caps') == null) {
607
            $rest_request->set_param('caps', EEM_Base::caps_read);
608
        }
609
        $current_user_full_access_to_entity = $model->currentUserCan(
610
            EEM_Base::caps_read_admin,
611
            $model->deduce_fields_n_values_from_cols_n_values($db_row)
612
        );
613
        $entity_array = $this->createBareEntityFromWpdbResults($model, $db_row);
614
        $entity_array = $this->addExtraFields($model, $db_row, $entity_array);
615
        $entity_array['_links'] = $this->getEntityLinks($model, $db_row, $entity_array);
616
        // when it's a regular read request for a model with a password and the password wasn't provided
617
        // remove the password protected fields
618
        $has_protected_fields = false;
619
        try {
620
            $this->checkPassword(
621
                $model,
622
                $db_row,
623
                $model->alter_query_params_to_restrict_by_ID(
624
                    $model->get_index_primary_key_string(
625
                        $model->deduce_fields_n_values_from_cols_n_values($db_row)
626
                    )
627
                ),
628
                $rest_request
629
            );
630
        } catch (RestPasswordRequiredException $e) {
631
            if ($model->hasPassword()) {
632
                // just remove protected fields
633
                $has_protected_fields = true;
634
                $entity_array = Capabilities::filterOutPasswordProtectedFields(
635
                    $entity_array,
636
                    $model,
637
                    $this->getModelVersionInfo()
638
                );
639
            } else {
640
                // that's a problem. None of this should be accessible if no password was provided
641
                throw $e;
642
            }
643
        }
644
645
        $entity_array['_calculated_fields'] = $this->getEntityCalculations($model, $db_row, $rest_request, $has_protected_fields);
646
        $entity_array = apply_filters(
647
            'FHEE__Read__create_entity_from_wpdb_results__entity_before_including_requested_models',
648
            $entity_array,
649
            $model,
650
            $rest_request->get_param('caps'),
651
            $rest_request,
652
            $this
653
        );
654
        // add an empty protected property for now. If it's still around after we remove everything the request didn't
655
        // want, we'll populate it then. k?
656
        $entity_array['_protected'] = array();
657
        // remove any properties the request didn't want. This way _protected won't bother mentioning them
658
        $entity_array = $this->includeOnlyRequestedProperties($model, $rest_request, $entity_array);
659
        $entity_array = $this->includeRequestedModels($model, $rest_request, $entity_array, $db_row, $has_protected_fields);
660
        // if they still wanted the _protected property, add it.
661
        if (isset($entity_array['_protected'])) {
662
            $entity_array = $this->addProtectedProperty($model, $entity_array, $has_protected_fields);
663
        }
664
        $entity_array = apply_filters(
665
            'FHEE__Read__create_entity_from_wpdb_results__entity_before_inaccessible_field_removal',
666
            $entity_array,
667
            $model,
668
            $rest_request->get_param('caps'),
669
            $rest_request,
670
            $this
671
        );
672
        if (! $current_user_full_access_to_entity) {
673
            $result_without_inaccessible_fields = Capabilities::filterOutInaccessibleEntityFields(
674
                $entity_array,
675
                $model,
676
                $rest_request->get_param('caps'),
677
                $this->getModelVersionInfo()
678
            );
679
        } else {
680
            $result_without_inaccessible_fields = $entity_array;
681
        }
682
        $this->setDebugInfo(
683
            'inaccessible fields',
684
            array_keys(array_diff_key((array) $entity_array, (array) $result_without_inaccessible_fields))
685
        );
686
        return apply_filters(
687
            'FHEE__Read__create_entity_from_wpdb_results__entity_return',
688
            $result_without_inaccessible_fields,
689
            $model,
690
            $rest_request->get_param('caps')
691
        );
692
    }
693
694
    /**
695
     * Returns an array describing which fields can be protected, and which actually were removed this request
696
     * @since 4.9.74.p
697
     * @param $model
698
     * @param $results_so_far
699
     * @param $protected
700
     * @return array results
701
     */
702
    protected function addProtectedProperty(EEM_Base $model, $results_so_far, $protected)
703
    {
704
        if (! $model->hasPassword() || ! $protected) {
705
            return $results_so_far;
706
        }
707
        $password_field = $model->getPasswordField();
708
        $all_protected = array_merge(
709
            array($password_field->get_name()),
710
            $password_field->protectedFields()
711
        );
712
        $fields_included = array_keys($results_so_far);
713
        $fields_included = array_intersect(
714
            $all_protected,
715
            $fields_included
716
        );
717
        foreach ($fields_included as $field_name) {
718
            $results_so_far['_protected'][] = $field_name ;
719
        }
720
        return $results_so_far;
721
    }
722
723
    /**
724
     * Creates a REST entity array (JSON object we're going to return in the response, but
725
     * for now still a PHP array, but soon enough we'll call json_encode on it, don't worry),
726
     * from $wpdb->get_row( $sql, ARRAY_A)
727
     *
728
     * @param EEM_Base $model
729
     * @param array    $db_row
730
     * @return array entity mostly ready for converting to JSON and sending in the response
731
     */
732
    protected function createBareEntityFromWpdbResults(EEM_Base $model, $db_row)
733
    {
734
        $result = $model->deduce_fields_n_values_from_cols_n_values($db_row);
735
        $result = array_intersect_key(
736
            $result,
737
            $this->getModelVersionInfo()->fieldsOnModelInThisVersion($model)
738
        );
739
        // if this is a CPT, we need to set the global $post to it,
740
        // otherwise shortcodes etc won't work properly while rendering it
741
        if ($model instanceof \EEM_CPT_Base) {
742
            $do_chevy_shuffle = true;
743
        } else {
744
            $do_chevy_shuffle = false;
745
        }
746
        if ($do_chevy_shuffle) {
747
            global $post;
748
            $old_post = $post;
749
            $post = get_post($result[ $model->primary_key_name() ]);
750
            if (! $post instanceof \WP_Post) {
751
                // well that's weird, because $result is what we JUST fetched from the database
752
                throw new RestException(
753
                    'error_fetching_post_from_database_results',
754
                    esc_html__(
755
                        'An item was retrieved from the database but it\'s not a WP_Post like it should be.',
756
                        'event_espresso'
757
                    )
758
                );
759
            }
760
            $model_object_classname = 'EE_' . $model->get_this_model_name();
761
            $post->{$model_object_classname} = \EE_Registry::instance()->load_class(
762
                $model_object_classname,
763
                $result,
764
                false,
765
                false
766
            );
767
        }
768
        foreach ($result as $field_name => $field_value) {
769
            $field_obj = $model->field_settings_for($field_name);
770
            if ($this->isSubclassOfOne($field_obj, $this->getModelVersionInfo()->fieldsIgnored())) {
771
                unset($result[ $field_name ]);
772
            } elseif ($this->isSubclassOfOne(
773
                $field_obj,
774
                $this->getModelVersionInfo()->fieldsThatHaveRenderedFormat()
775
            )
776
            ) {
777
                $result[ $field_name ] = array(
778
                    'raw'      => $this->prepareFieldObjValueForJson($field_obj, $field_value),
779
                    'rendered' => $this->prepareFieldObjValueForJson($field_obj, $field_value, 'pretty'),
780
                );
781
            } elseif ($this->isSubclassOfOne(
782
                $field_obj,
783
                $this->getModelVersionInfo()->fieldsThatHavePrettyFormat()
784
            )
785
            ) {
786
                $result[ $field_name ] = array(
787
                    'raw'    => $this->prepareFieldObjValueForJson($field_obj, $field_value),
788
                    'pretty' => $this->prepareFieldObjValueForJson($field_obj, $field_value, 'pretty'),
789
                );
790
            } elseif ($field_obj instanceof \EE_Datetime_Field) {
791
                $field_value = $field_obj->prepare_for_set_from_db($field_value);
792
                // if the value is null, but we're not supposed to permit null, then set to the field's default
793
                if (is_null($field_value)) {
794
                    $field_value = $field_obj->getDefaultDateTimeObj();
795
                }
796
                if (is_null($field_value)) {
797
                    $gmt_date = $local_date = ModelDataTranslator::prepareFieldValuesForJson(
798
                        $field_obj,
799
                        $field_value,
800
                        $this->getModelVersionInfo()->requestedVersion()
801
                    );
802
                } else {
803
                    $timezone = $field_value->getTimezone();
804
                    EEH_DTT_Helper::setTimezone($field_value, new DateTimeZone('UTC'));
805
                    $gmt_date = ModelDataTranslator::prepareFieldValuesForJson(
806
                        $field_obj,
807
                        $field_value,
808
                        $this->getModelVersionInfo()->requestedVersion()
809
                    );
810
                    EEH_DTT_Helper::setTimezone($field_value, $timezone);
811
                    $local_date = ModelDataTranslator::prepareFieldValuesForJson(
812
                        $field_obj,
813
                        $field_value,
814
                        $this->getModelVersionInfo()->requestedVersion()
815
                    );
816
                }
817
                $result[ $field_name . '_gmt' ] = $gmt_date;
818
                $result[ $field_name ] = $local_date;
819
            } else {
820
                $result[ $field_name ] = $this->prepareFieldObjValueForJson($field_obj, $field_value);
821
            }
822
        }
823
        if ($do_chevy_shuffle) {
824
            $post = $old_post;
825
        }
826
        return $result;
827
    }
828
829
830
    /**
831
     * Takes a value all the way from the DB representation, to the model object's representation, to the
832
     * user-facing PHP representation, to the REST API representation. (Assumes you've already taken from the DB
833
     * representation using $field_obj->prepare_for_set_from_db())
834
     *
835
     * @param EE_Model_Field_Base $field_obj
836
     * @param mixed               $value  as it's stored on a model object
837
     * @param string              $format valid values are 'normal' (default), 'pretty', 'datetime_obj'
838
     * @return mixed
839
     * @throws ObjectDetectedException if $value contains a PHP object
840
     */
841
    protected function prepareFieldObjValueForJson(EE_Model_Field_Base $field_obj, $value, $format = 'normal')
842
    {
843
        $value = $field_obj->prepare_for_set_from_db($value);
844
        switch ($format) {
845
            case 'pretty':
846
                $value = $field_obj->prepare_for_pretty_echoing($value);
847
                break;
848
            case 'normal':
849
            default:
850
                $value = $field_obj->prepare_for_get($value);
851
                break;
852
        }
853
        return ModelDataTranslator::prepareFieldValuesForJson(
854
            $field_obj,
855
            $value,
856
            $this->getModelVersionInfo()->requestedVersion()
857
        );
858
    }
859
860
861
    /**
862
     * Adds a few extra fields to the entity response
863
     *
864
     * @param EEM_Base $model
865
     * @param array    $db_row
866
     * @param array    $entity_array
867
     * @return array modified entity
868
     */
869
    protected function addExtraFields(EEM_Base $model, $db_row, $entity_array)
870
    {
871
        if ($model instanceof EEM_CPT_Base) {
872
            $entity_array['link'] = get_permalink($db_row[ $model->get_primary_key_field()->get_qualified_column() ]);
873
        }
874
        return $entity_array;
875
    }
876
877
878
    /**
879
     * Gets links we want to add to the response
880
     *
881
     * @global \WP_REST_Server $wp_rest_server
882
     * @param EEM_Base         $model
883
     * @param array            $db_row
884
     * @param array            $entity_array
885
     * @return array the _links item in the entity
886
     */
887
    protected function getEntityLinks($model, $db_row, $entity_array)
888
    {
889
        // add basic links
890
        $links = array();
891
        if ($model->has_primary_key_field()) {
892
            $links['self'] = array(
893
                array(
894
                    'href' => $this->getVersionedLinkTo(
895
                        EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
896
                        . '/'
897
                        . $entity_array[ $model->primary_key_name() ]
898
                    ),
899
                ),
900
            );
901
        }
902
        $links['collection'] = array(
903
            array(
904
                'href' => $this->getVersionedLinkTo(
905
                    EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
906
                ),
907
            ),
908
        );
909
        // add links to related models
910
        if ($model->has_primary_key_field()) {
911
            foreach ($this->getModelVersionInfo()->relationSettings($model) as $relation_name => $relation_obj) {
912
                $related_model_part = Read::getRelatedEntityName($relation_name, $relation_obj);
913
                $links[ EED_Core_Rest_Api::ee_api_link_namespace . $related_model_part ] = array(
914
                    array(
915
                        'href'   => $this->getVersionedLinkTo(
916
                            EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
917
                            . '/'
918
                            . $entity_array[ $model->primary_key_name() ]
919
                            . '/'
920
                            . $related_model_part
921
                        ),
922
                        'single' => $relation_obj instanceof EE_Belongs_To_Relation ? true : false,
923
                    ),
924
                );
925
            }
926
        }
927
        return $links;
928
    }
929
930
931
    /**
932
     * Adds the included models indicated in the request to the entity provided
933
     *
934
     * @param EEM_Base $model
935
     * @param WP_REST_Request $rest_request
936
     * @param array $entity_array
937
     * @param array $db_row
938
     * @param boolean $included_items_protected if the original item is password protected, don't include any related models.
939
     * @return array the modified entity
940
     * @throws RestException
941
     */
942
    protected function includeRequestedModels(
943
        EEM_Base $model,
944
        WP_REST_Request $rest_request,
945
        $entity_array,
946
        $db_row = array(),
947
        $included_items_protected = false
948
    ) {
949
        // if $db_row not included, hope the entity array has what we need
950
        if (! $db_row) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $db_row of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
951
            $db_row = $entity_array;
952
        }
953
        $relation_settings = $this->getModelVersionInfo()->relationSettings($model);
954
        foreach ($relation_settings as $relation_name => $relation_obj) {
955
            $related_fields_to_include = $this->explodeAndGetItemsPrefixedWith(
956
                $rest_request->get_param('include'),
957
                $relation_name
958
            );
959
            $related_fields_to_calculate = $this->explodeAndGetItemsPrefixedWith(
960
                $rest_request->get_param('calculate'),
961
                $relation_name
962
            );
963
            // did they specify they wanted to include a related model, or
964
            // specific fields from a related model?
965
            // or did they specify to calculate a field from a related model?
966
            if ($related_fields_to_include || $related_fields_to_calculate) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $related_fields_to_include of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $related_fields_to_calculate of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
967
                // if so, we should include at least some part of the related model
968
                $pretend_related_request = new WP_REST_Request();
969
                $pretend_related_request->set_query_params(
970
                    array(
971
                        'caps'      => $rest_request->get_param('caps'),
972
                        'include'   => $related_fields_to_include,
973
                        'calculate' => $related_fields_to_calculate,
974
                        'password' => $rest_request->get_param('password')
975
                    )
976
                );
977
                $pretend_related_request->add_header('no_rest_headers', true);
978
                $primary_model_query_params = $model->alter_query_params_to_restrict_by_ID(
979
                    $model->get_index_primary_key_string(
980
                        $model->deduce_fields_n_values_from_cols_n_values($db_row)
981
                    )
982
                );
983
                if (! $included_items_protected) {
984
                    try {
985
                        $related_results = $this->getEntitiesFromRelationUsingModelQueryParams(
986
                            $primary_model_query_params,
987
                            $relation_obj,
988
                            $pretend_related_request
989
                        );
990
                    } catch (RestException $e) {
991
                        $related_results = null;
992
                    }
993
                } else {
994
                    // they're protected, hide them.
995
                    $related_results = null;
996
                    $entity_array['_protected'][] = Read::getRelatedEntityName($relation_name, $relation_obj);
997
                }
998
                if ($related_results instanceof WP_Error || $related_results === null) {
0 ignored issues
show
Bug introduced by
The class WP_Error does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
999
                    $related_results = $relation_obj instanceof EE_Belongs_To_Relation ? null : array();
1000
                }
1001
                $entity_array[ Read::getRelatedEntityName($relation_name, $relation_obj) ] = $related_results;
1002
            }
1003
        }
1004
        return $entity_array;
1005
    }
1006
1007
    /**
1008
     * If the user has requested only specific properties (including meta properties like _links or _protected)
1009
     * remove everything else.
1010
     * @since 4.9.74.p
1011
     * @param EEM_Base $model
1012
     * @param WP_REST_Request $rest_request
1013
     * @param $entity_array
1014
     * @return array
1015
     * @throws EE_Error
1016
     */
1017
    protected function includeOnlyRequestedProperties(
1018
        EEM_Base $model,
1019
        WP_REST_Request $rest_request,
1020
        $entity_array
1021
    ) {
1022
1023
        $includes_for_this_model = $this->explodeAndGetItemsPrefixedWith($rest_request->get_param('include'), '');
1024
        $includes_for_this_model = $this->removeModelNamesFromArray($includes_for_this_model);
1025
        // if they passed in * or didn't specify any includes, return everything
1026
        if (! in_array('*', $includes_for_this_model)
1027
            && ! empty($includes_for_this_model)
1028
        ) {
1029
            if ($model->has_primary_key_field()) {
1030
                // always include the primary key. ya just gotta know that at least
1031
                $includes_for_this_model[] = $model->primary_key_name();
1032
            }
1033
            if ($this->explodeAndGetItemsPrefixedWith($rest_request->get_param('calculate'), '')) {
1034
                $includes_for_this_model[] = '_calculated_fields';
1035
            }
1036
            $entity_array = array_intersect_key($entity_array, array_flip($includes_for_this_model));
1037
        }
1038
        return $entity_array;
1039
    }
1040
1041
1042
    /**
1043
     * Returns a new array with all the names of models removed. Eg
1044
     * array( 'Event', 'Datetime.*', 'foobar' ) would become array( 'Datetime.*', 'foobar' )
1045
     *
1046
     * @param array $arr
1047
     * @return array
1048
     */
1049
    private function removeModelNamesFromArray($arr)
1050
    {
1051
        return array_diff($arr, array_keys(EE_Registry::instance()->non_abstract_db_models));
1052
    }
1053
1054
1055
    /**
1056
     * Gets the calculated fields for the response
1057
     *
1058
     * @param EEM_Base        $model
1059
     * @param array           $wpdb_row
1060
     * @param WP_REST_Request $rest_request
1061
     * @param boolean $row_is_protected whether this row is password protected or not
1062
     * @return \stdClass the _calculations item in the entity
1063
     * @throws ObjectDetectedException if a default value has a PHP object, which should never do (and if we
1064
     * did, let's know about it ASAP, so let the exception bubble up)
1065
     */
1066
    protected function getEntityCalculations($model, $wpdb_row, $rest_request, $row_is_protected = false)
1067
    {
1068
        $calculated_fields = $this->explodeAndGetItemsPrefixedWith(
1069
            $rest_request->get_param('calculate'),
1070
            ''
1071
        );
1072
        // note: setting calculate=* doesn't do anything
1073
        $calculated_fields_to_return = new \stdClass();
1074
        $protected_fields = array();
1075
        foreach ($calculated_fields as $field_to_calculate) {
1076
            try {
1077
                // it's password protected, so they shouldn't be able to read this. Remove the value
1078
                $schema = $this->fields_calculator->getJsonSchemaForModel($model);
1079
                if ($row_is_protected
1080
                    && isset($schema['properties'][ $field_to_calculate ]['protected'])
1081
                    && $schema['properties'][ $field_to_calculate ]['protected']) {
1082
                    $calculated_value = null;
1083
                    $protected_fields[] = $field_to_calculate;
1084
                    if ($schema['properties'][ $field_to_calculate ]['type']) {
1085
                        switch ($schema['properties'][ $field_to_calculate ]['type']) {
1086
                            case 'boolean':
1087
                                $calculated_value = false;
1088
                                break;
1089
                            case 'integer':
1090
                                $calculated_value = 0;
1091
                                break;
1092
                            case 'string':
1093
                                $calculated_value = '';
1094
                                break;
1095
                            case 'array':
1096
                                $calculated_value = array();
1097
                                break;
1098
                            case 'object':
1099
                                $calculated_value = new stdClass();
1100
                                break;
1101
                        }
1102
                    }
1103
                } else {
1104
                    $calculated_value = ModelDataTranslator::prepareFieldValueForJson(
1105
                        null,
1106
                        $this->fields_calculator->retrieveCalculatedFieldValue(
1107
                            $model,
1108
                            $field_to_calculate,
1109
                            $wpdb_row,
1110
                            $rest_request,
1111
                            $this
1112
                        ),
1113
                        $this->getModelVersionInfo()->requestedVersion()
1114
                    );
1115
                }
1116
                $calculated_fields_to_return->{$field_to_calculate} = $calculated_value;
1117
            } catch (RestException $e) {
1118
                // if we don't have permission to read it, just leave it out. but let devs know about the problem
1119
                $this->setResponseHeader(
1120
                    'Notices-Field-Calculation-Errors['
1121
                    . $e->getStringCode()
1122
                    . ']['
1123
                    . $model->get_this_model_name()
1124
                    . ']['
1125
                    . $field_to_calculate
1126
                    . ']',
1127
                    $e->getMessage(),
1128
                    true
1129
                );
1130
            }
1131
        }
1132
        $calculated_fields_to_return->_protected = $protected_fields;
1133
        return $calculated_fields_to_return;
1134
    }
1135
1136
1137
    /**
1138
     * Gets the full URL to the resource, taking the requested version into account
1139
     *
1140
     * @param string $link_part_after_version_and_slash eg "events/10/datetimes"
1141
     * @return string url eg "http://mysite.com/wp-json/ee/v4.6/events/10/datetimes"
1142
     */
1143
    public function getVersionedLinkTo($link_part_after_version_and_slash)
1144
    {
1145
        return rest_url(
1146
            EED_Core_Rest_Api::get_versioned_route_to(
1147
                $link_part_after_version_and_slash,
1148
                $this->getModelVersionInfo()->requestedVersion()
1149
            )
1150
        );
1151
    }
1152
1153
1154
    /**
1155
     * Gets the correct lowercase name for the relation in the API according
1156
     * to the relation's type
1157
     *
1158
     * @param string                  $relation_name
1159
     * @param \EE_Model_Relation_Base $relation_obj
1160
     * @return string
1161
     */
1162
    public static function getRelatedEntityName($relation_name, $relation_obj)
1163
    {
1164
        if ($relation_obj instanceof EE_Belongs_To_Relation) {
1165
            return strtolower($relation_name);
1166
        } else {
1167
            return EEH_Inflector::pluralize_and_lower($relation_name);
1168
        }
1169
    }
1170
1171
1172
    /**
1173
     * Gets the one model object with the specified id for the specified model
1174
     *
1175
     * @param EEM_Base        $model
1176
     * @param WP_REST_Request $request
1177
     * @return array
1178
     */
1179
    public function getEntityFromModel($model, $request)
1180
    {
1181
        $context = $this->validateContext($request->get_param('caps'));
1182
        return $this->getOneOrReportPermissionError($model, $request, $context);
1183
    }
1184
1185
1186
    /**
1187
     * If a context is provided which isn't valid, maybe it was added in a future
1188
     * version so just treat it as a default read
1189
     *
1190
     * @param string $context
1191
     * @return string array key of EEM_Base::cap_contexts_to_cap_action_map()
1192
     */
1193
    public function validateContext($context)
1194
    {
1195
        if (! $context) {
1196
            $context = EEM_Base::caps_read;
1197
        }
1198
        $valid_contexts = EEM_Base::valid_cap_contexts();
1199
        if (in_array($context, $valid_contexts)) {
1200
            return $context;
1201
        } else {
1202
            return EEM_Base::caps_read;
1203
        }
1204
    }
1205
1206
1207
    /**
1208
     * Verifies the passed in value is an allowable default where conditions value.
1209
     *
1210
     * @param $default_query_params
1211
     * @return string
1212
     */
1213
    public function validateDefaultQueryParams($default_query_params)
1214
    {
1215
        $valid_default_where_conditions_for_api_calls = array(
1216
            EEM_Base::default_where_conditions_all,
1217
            EEM_Base::default_where_conditions_minimum_all,
1218
            EEM_Base::default_where_conditions_minimum_others,
1219
        );
1220
        if (! $default_query_params) {
1221
            $default_query_params = EEM_Base::default_where_conditions_all;
1222
        }
1223
        if (in_array(
1224
            $default_query_params,
1225
            $valid_default_where_conditions_for_api_calls,
1226
            true
1227
        )) {
1228
            return $default_query_params;
1229
        } else {
1230
            return EEM_Base::default_where_conditions_all;
1231
        }
1232
    }
1233
1234
1235
    /**
1236
     * 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.
1237
     * Note: right now the query parameter keys for fields (and related fields)
1238
     * can be left as-is, but it's quite possible this will change someday.
1239
     * Also, this method's contents might be candidate for moving to Model_Data_Translator
1240
     *
1241
     * @param EEM_Base $model
1242
     * @param array    $query_parameters  from $_GET parameter @see Read:handle_request_get_all
0 ignored issues
show
Bug introduced by
There is no parameter named $query_parameters. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1243
     * @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)
1244
     *                                    or FALSE to indicate that absolutely no results should be returned
1245
     * @throws EE_Error
1246
     * @throws RestException
1247
     */
1248
    public function createModelQueryParams($model, $query_params)
1249
    {
1250
        $model_query_params = array();
1251 View Code Duplication
        if (isset($query_params['where'])) {
1252
            $model_query_params[0] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1253
                $query_params['where'],
1254
                $model,
1255
                $this->getModelVersionInfo()->requestedVersion()
1256
            );
1257
        }
1258
        if (isset($query_params['order_by'])) {
1259
            $order_by = $query_params['order_by'];
1260
        } elseif (isset($query_params['orderby'])) {
1261
            $order_by = $query_params['orderby'];
1262
        } else {
1263
            $order_by = null;
1264
        }
1265
        if ($order_by !== null) {
1266
            if (is_array($order_by)) {
1267
                $order_by = ModelDataTranslator::prepareFieldNamesInArrayKeysFromJson($order_by);
1268
            } else {
1269
                // it's a single item
1270
                $order_by = ModelDataTranslator::prepareFieldNameFromJson($order_by);
1271
            }
1272
            $model_query_params['order_by'] = $order_by;
1273
        }
1274
        if (isset($query_params['group_by'])) {
1275
            $group_by = $query_params['group_by'];
1276
        } elseif (isset($query_params['groupby'])) {
1277
            $group_by = $query_params['groupby'];
1278
        } else {
1279
            $group_by = array_keys($model->get_combined_primary_key_fields());
1280
        }
1281
        // make sure they're all real names
1282
        if (is_array($group_by)) {
1283
            $group_by = ModelDataTranslator::prepareFieldNamesFromJson($group_by);
1284
        }
1285
        if ($group_by !== null) {
1286
            $model_query_params['group_by'] = $group_by;
1287
        }
1288 View Code Duplication
        if (isset($query_params['having'])) {
1289
            $model_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1290
                $query_params['having'],
1291
                $model,
1292
                $this->getModelVersionInfo()->requestedVersion()
1293
            );
1294
        }
1295
        if (isset($query_params['order'])) {
1296
            $model_query_params['order'] = $query_params['order'];
1297
        }
1298
        if (isset($query_params['mine'])) {
1299
            $model_query_params = $model->alter_query_params_to_only_include_mine($model_query_params);
1300
        }
1301
        if (isset($query_params['limit'])) {
1302
            // limit should be either a string like '23' or '23,43', or an array with two items in it
1303
            if (! is_array($query_params['limit'])) {
1304
                $limit_array = explode(',', (string) $query_params['limit']);
1305
            } else {
1306
                $limit_array = $query_params['limit'];
1307
            }
1308
            $sanitized_limit = array();
1309
            foreach ($limit_array as $key => $limit_part) {
1310
                if ($this->debug_mode && (! is_numeric($limit_part) || count($sanitized_limit) > 2)) {
1311
                    throw new EE_Error(
1312
                        sprintf(
1313
                            __(
1314
                            // @codingStandardsIgnoreStart
1315
                                '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.',
1316
                                // @codingStandardsIgnoreEnd
1317
                                'event_espresso'
1318
                            ),
1319
                            wp_json_encode($query_params['limit'])
1320
                        )
1321
                    );
1322
                }
1323
                $sanitized_limit[] = (int) $limit_part;
1324
            }
1325
            $model_query_params['limit'] = implode(',', $sanitized_limit);
1326
        } else {
1327
            $model_query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
1328
        }
1329
        if (isset($query_params['caps'])) {
1330
            $model_query_params['caps'] = $this->validateContext($query_params['caps']);
1331
        } else {
1332
            $model_query_params['caps'] = EEM_Base::caps_read;
1333
        }
1334
        if (isset($query_params['default_where_conditions'])) {
1335
            $model_query_params['default_where_conditions'] = $this->validateDefaultQueryParams(
1336
                $query_params['default_where_conditions']
1337
            );
1338
        }
1339
        // if this is a model protected by a password on another model, exclude the password protected
1340
        // entities by default. But if they passed in a password, try to show them all. If the password is wrong,
1341
        // though, they'll get an error (see Read::createEntityFromWpdbResult() which calls Read::checkPassword)
1342
        if (! $model->hasPassword()
1343
            && $model->restrictedByRelatedModelPassword()
1344
            && $model_query_params['caps'] === EEM_Base::caps_read) {
1345
            if (empty($query_params['password'])) {
1346
                $model_query_params['exclude_protected'] = true;
1347
            }
1348
        }
1349
1350
        return apply_filters('FHEE__Read__create_model_query_params', $model_query_params, $query_params, $model);
1351
    }
1352
1353
1354
    /**
1355
     * Changes the REST-style query params for use in the models
1356
     *
1357
     * @deprecated
1358
     * @param EEM_Base $model
1359
     * @param array    $query_params sub-array from @see EEM_Base::get_all()
1360
     * @return array
1361
     */
1362 View Code Duplication
    public function prepareRestQueryParamsKeyForModels($model, $query_params)
1363
    {
1364
        $model_ready_query_params = array();
1365
        foreach ($query_params as $key => $value) {
1366
            if (is_array($value)) {
1367
                $model_ready_query_params[ $key ] = $this->prepareRestQueryParamsKeyForModels($model, $value);
0 ignored issues
show
Deprecated Code introduced by
The method EventEspresso\core\libra...eryParamsKeyForModels() has been deprecated.

This method has been deprecated.

Loading history...
1368
            } else {
1369
                $model_ready_query_params[ $key ] = $value;
1370
            }
1371
        }
1372
        return $model_ready_query_params;
1373
    }
1374
1375
1376
    /**
1377
     * @deprecated instead use ModelDataTranslator::prepareFieldValuesFromJson()
1378
     * @param $model
1379
     * @param $query_params
1380
     * @return array
1381
     */
1382 View Code Duplication
    public function prepareRestQueryParamsValuesForModels($model, $query_params)
1383
    {
1384
        $model_ready_query_params = array();
1385
        foreach ($query_params as $key => $value) {
1386
            if (is_array($value)) {
1387
                $model_ready_query_params[ $key ] = $this->prepareRestQueryParamsValuesForModels($model, $value);
0 ignored issues
show
Deprecated Code introduced by
The method EventEspresso\core\libra...ParamsValuesForModels() has been deprecated with message: instead use ModelDataTranslator::prepareFieldValuesFromJson()

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1388
            } else {
1389
                $model_ready_query_params[ $key ] = $value;
1390
            }
1391
        }
1392
        return $model_ready_query_params;
1393
    }
1394
1395
1396
    /**
1397
     * Explodes the string on commas, and only returns items with $prefix followed by a period.
1398
     * If no prefix is specified, returns items with no period.
1399
     *
1400
     * @param string|array $string_to_explode eg "jibba,jabba, blah, blah, blah" or array('jibba', 'jabba' )
1401
     * @param string       $prefix            "Event" or "foobar"
1402
     * @return array $string_to_exploded exploded on COMMAS, and if a prefix was specified
1403
     *                                        we only return strings starting with that and a period; if no prefix was
1404
     *                                        specified we return all items containing NO periods
1405
     */
1406
    public function explodeAndGetItemsPrefixedWith($string_to_explode, $prefix)
1407
    {
1408
        if (is_string($string_to_explode)) {
1409
            $exploded_contents = explode(',', $string_to_explode);
1410
        } elseif (is_array($string_to_explode)) {
1411
            $exploded_contents = $string_to_explode;
1412
        } else {
1413
            $exploded_contents = array();
1414
        }
1415
        // if the string was empty, we want an empty array
1416
        $exploded_contents = array_filter($exploded_contents);
1417
        $contents_with_prefix = array();
1418
        foreach ($exploded_contents as $item) {
1419
            $item = trim($item);
1420
            // if no prefix was provided, so we look for items with no "." in them
1421
            if (! $prefix) {
1422
                // does this item have a period?
1423
                if (strpos($item, '.') === false) {
1424
                    // if not, then its what we're looking for
1425
                    $contents_with_prefix[] = $item;
1426
                }
1427
            } elseif (strpos($item, $prefix . '.') === 0) {
1428
                // this item has the prefix and a period, grab it
1429
                $contents_with_prefix[] = substr(
1430
                    $item,
1431
                    strpos($item, $prefix . '.') + strlen($prefix . '.')
1432
                );
1433
            } elseif ($item === $prefix) {
1434
                // this item is JUST the prefix
1435
                // so let's grab everything after, which is a blank string
1436
                $contents_with_prefix[] = '';
1437
            }
1438
        }
1439
        return $contents_with_prefix;
1440
    }
1441
1442
1443
    /**
1444
     * @deprecated since 4.8.36.rc.001 You should instead use Read::explode_and_get_items_prefixed_with.
1445
     * Deprecated because its return values were really quite confusing- sometimes it returned
1446
     * an empty array (when the include string was blank or '*') or sometimes it returned
1447
     * array('*') (when you provided a model and a model of that kind was found).
1448
     * Parses the $include_string so we fetch all the field names relating to THIS model
1449
     * (ie have NO period in them), or for the provided model (ie start with the model
1450
     * name and then a period).
1451
     * @param string $include_string @see Read:handle_request_get_all
1452
     * @param string $model_name
1453
     * @return array of fields for this model. If $model_name is provided, then
1454
     *                               the fields for that model, with the model's name removed from each.
1455
     *                               If $include_string was blank or '*' returns an empty array
1456
     */
1457
    public function extractIncludesForThisModel($include_string, $model_name = null)
1458
    {
1459
        if (is_array($include_string)) {
1460
            $include_string = implode(',', $include_string);
1461
        }
1462
        if ($include_string === '*' || $include_string === '') {
1463
            return array();
1464
        }
1465
        $includes = explode(',', $include_string);
1466
        $extracted_fields_to_include = array();
1467
        if ($model_name) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $model_name of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1468
            foreach ($includes as $field_to_include) {
1469
                $field_to_include = trim($field_to_include);
1470
                if (strpos($field_to_include, $model_name . '.') === 0) {
1471
                    // found the model name at the exact start
1472
                    $field_sans_model_name = str_replace($model_name . '.', '', $field_to_include);
1473
                    $extracted_fields_to_include[] = $field_sans_model_name;
1474
                } elseif ($field_to_include == $model_name) {
1475
                    $extracted_fields_to_include[] = '*';
1476
                }
1477
            }
1478
        } else {
1479
            // look for ones with no period
1480
            foreach ($includes as $field_to_include) {
1481
                $field_to_include = trim($field_to_include);
1482
                if (strpos($field_to_include, '.') === false
1483
                    && ! $this->getModelVersionInfo()->isModelNameInThisVersion($field_to_include)
1484
                ) {
1485
                    $extracted_fields_to_include[] = $field_to_include;
1486
                }
1487
            }
1488
        }
1489
        return $extracted_fields_to_include;
1490
    }
1491
1492
1493
    /**
1494
     * Gets the single item using the model according to the request in the context given, otherwise
1495
     * returns that it's inaccessible to the current user
1496
     *
1497
     * @param EEM_Base $model
1498
     * @param WP_REST_Request $request
1499
     * @param null $context
1500
     * @return array
1501
     * @throws EE_Error
1502
     */
1503
    public function getOneOrReportPermissionError(EEM_Base $model, WP_REST_Request $request, $context = null)
1504
    {
1505
        $query_params = array(array($model->primary_key_name() => $request->get_param('id')), 'limit' => 1);
1506
        if ($model instanceof EEM_Soft_Delete_Base) {
1507
            $query_params = $model->alter_query_params_so_deleted_and_undeleted_items_included($query_params);
1508
        }
1509
        $restricted_query_params = $query_params;
1510
        $restricted_query_params['caps'] = $context;
1511
        $this->setDebugInfo('model query params', $restricted_query_params);
1512
        $model_rows = $model->get_all_wpdb_results($restricted_query_params);
1513
        if (! empty($model_rows)) {
1514
            return $this->createEntityFromWpdbResult(
1515
                $model,
1516
                reset($model_rows),
1517
                $request
1518
            );
1519
        } else {
1520
            // ok let's test to see if we WOULD have found it, had we not had restrictions from missing capabilities
1521
            $lowercase_model_name = strtolower($model->get_this_model_name());
1522
            if ($model->exists($query_params)) {
1523
                // you got shafted- it existed but we didn't want to tell you!
1524
                throw new RestException(
1525
                    'rest_user_cannot_' . $context,
1526
                    sprintf(
1527
                        __('Sorry, you cannot %1$s this %2$s. Missing permissions are: %3$s', 'event_espresso'),
1528
                        $context,
1529
                        $lowercase_model_name,
1530
                        Capabilities::getMissingPermissionsString(
1531
                            $model,
1532
                            $context
1533
                        )
1534
                    ),
1535
                    array('status' => 403)
1536
                );
1537
            } else {
1538
                // it's not you. It just doesn't exist
1539
                throw new RestException(
1540
                    sprintf('rest_%s_invalid_id', $lowercase_model_name),
1541
                    sprintf(__('Invalid %s ID.', 'event_espresso'), $lowercase_model_name),
1542
                    array('status' => 404)
1543
                );
1544
            }
1545
        }
1546
    }
1547
1548
    /**
1549
     * Checks that if this content requires a password to be read, that it's been provided and is correct.
1550
     * @since 4.9.74.p
1551
     * @param EEM_Base $model
1552
     * @param $model_row
1553
     * @param $query_params Adds 'default_where_conditions' => 'minimum' to ensure we don't confuse trashed with
1554
     *                      password protected.
1555
     * @param WP_REST_Request $request
1556
     * @throws EE_Error
1557
     * @throws InvalidArgumentException
1558
     * @throws InvalidDataTypeException
1559
     * @throws InvalidInterfaceException
1560
     * @throws RestPasswordRequiredException
1561
     * @throws RestPasswordIncorrectException
1562
     * @throws \EventEspresso\core\exceptions\ModelConfigurationException
1563
     * @throws ReflectionException
1564
     */
1565
    protected function checkPassword(EEM_Base $model, $model_row, $query_params, WP_REST_Request $request)
1566
    {
1567
        $query_params['default_where_conditions'] = 'minimum';
1568
        // stuff is only "protected" for front-end requests. Elsewhere, you either get full permission to access the object
1569
        // or you don't.
1570
        $request_caps = $request->get_param('caps');
1571
        if (isset($request_caps) && $request_caps !== EEM_Base::caps_read) {
1572
            return;
1573
        }
1574
        // if this entity requires a password, they better give it and it better be right!
1575
        if ($model->hasPassword()
1576
            && $model_row[ $model->getPasswordField()->get_qualified_column() ] !== '') {
1577
            if (empty($request['password'])) {
1578
                throw new RestPasswordRequiredException();
1579
            } elseif (!hash_equals(
1580
                $model_row[ $model->getPasswordField()->get_qualified_column() ],
1581
                $request['password']
1582
            )) {
1583
                throw new RestPasswordIncorrectException();
1584
            }
1585
        } // wait! maybe this content is password protected
1586
        elseif ($model->restrictedByRelatedModelPassword()
1587
            && $request->get_param('caps') === EEM_Base::caps_read) {
1588
            $password_supplied = $request->get_param('password');
1589
            if (empty($password_supplied)) {
1590
                $query_params['exclude_protected'] = true;
1591
                if (!$model->exists($query_params)) {
1592
                    throw new RestPasswordRequiredException();
1593
                }
1594
            } else {
1595
                $query_params[0][ $model->modelChainAndPassword() ] = $password_supplied;
1596
                if (!$model->exists($query_params)) {
1597
                    throw new RestPasswordIncorrectException();
1598
                }
1599
            }
1600
        }
1601
    }
1602
}
1603