Completed
Branch FET/rest-relation-endpoints (02db8d)
by
unknown
27:05 queued 18:22
created

Read::handleSchemaRequest()   A

Complexity

Conditions 3
Paths 7

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 7
nop 2
dl 0
loc 25
rs 9.52
c 0
b 0
f 0
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
                array(
624
                    0 => array(
625
                        $model->primary_key_name() => $db_row[ $model->get_primary_key_field()->get_qualified_column() ]
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
                    $related_results = $this->getEntitiesFromRelationUsingModelQueryParams(
985
                        $primary_model_query_params,
986
                        $relation_obj,
987
                        $pretend_related_request
988
                    );
989
                } else {
990
                    // they're protected, hide them.
991
                    $related_results = $relation_obj instanceof EE_Belongs_To_Relation ? null : array();
992
                    $entity_array['_protected'][] = Read::getRelatedEntityName($relation_name, $relation_obj);
993
                }
994
                if ($related_results instanceof WP_Error) {
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...
995
                    $related_results = null;
996
                }
997
                $entity_array[ Read::getRelatedEntityName($relation_name, $relation_obj) ] = $related_results;
998
            }
999
        }
1000
        return $entity_array;
1001
    }
1002
1003
    /**
1004
     * If the user has requested only specific properties (including meta properties like _links or _protected)
1005
     * remove everything else.
1006
     * @since 4.9.74.p
1007
     * @param EEM_Base $model
1008
     * @param WP_REST_Request $rest_request
1009
     * @param $entity_array
1010
     * @return array
1011
     * @throws EE_Error
1012
     */
1013
    protected function includeOnlyRequestedProperties(
1014
        EEM_Base $model,
1015
        WP_REST_Request $rest_request,
1016
        $entity_array
1017
    ) {
1018
1019
        $includes_for_this_model = $this->explodeAndGetItemsPrefixedWith($rest_request->get_param('include'), '');
1020
        $includes_for_this_model = $this->removeModelNamesFromArray($includes_for_this_model);
1021
        // if they passed in * or didn't specify any includes, return everything
1022
        if (! in_array('*', $includes_for_this_model)
1023
            && ! empty($includes_for_this_model)
1024
        ) {
1025
            if ($model->has_primary_key_field()) {
1026
                // always include the primary key. ya just gotta know that at least
1027
                $includes_for_this_model[] = $model->primary_key_name();
1028
            }
1029
            if ($this->explodeAndGetItemsPrefixedWith($rest_request->get_param('calculate'), '')) {
1030
                $includes_for_this_model[] = '_calculated_fields';
1031
            }
1032
            $entity_array = array_intersect_key($entity_array, array_flip($includes_for_this_model));
1033
        }
1034
        return $entity_array;
1035
    }
1036
1037
1038
    /**
1039
     * Returns a new array with all the names of models removed. Eg
1040
     * array( 'Event', 'Datetime.*', 'foobar' ) would become array( 'Datetime.*', 'foobar' )
1041
     *
1042
     * @param array $arr
1043
     * @return array
1044
     */
1045
    private function removeModelNamesFromArray($arr)
1046
    {
1047
        return array_diff($arr, array_keys(EE_Registry::instance()->non_abstract_db_models));
1048
    }
1049
1050
1051
    /**
1052
     * Gets the calculated fields for the response
1053
     *
1054
     * @param EEM_Base        $model
1055
     * @param array           $wpdb_row
1056
     * @param WP_REST_Request $rest_request
1057
     * @param boolean $row_is_protected whether this row is password protected or not
1058
     * @return \stdClass the _calculations item in the entity
1059
     * @throws ObjectDetectedException if a default value has a PHP object, which should never do (and if we
1060
     * did, let's know about it ASAP, so let the exception bubble up)
1061
     */
1062
    protected function getEntityCalculations($model, $wpdb_row, $rest_request, $row_is_protected = false)
1063
    {
1064
        $calculated_fields = $this->explodeAndGetItemsPrefixedWith(
1065
            $rest_request->get_param('calculate'),
1066
            ''
1067
        );
1068
        // note: setting calculate=* doesn't do anything
1069
        $calculated_fields_to_return = new \stdClass();
1070
        $protected_fields = array();
1071
        foreach ($calculated_fields as $field_to_calculate) {
1072
            try {
1073
                // it's password protected, so they shouldn't be able to read this. Remove the value
1074
                $schema = $this->fields_calculator->getJsonSchemaForModel($model);
1075
                if ($row_is_protected
1076
                    && isset($schema['properties'][ $field_to_calculate ]['protected'])
1077
                    && $schema['properties'][ $field_to_calculate ]['protected']) {
1078
                    $calculated_value = null;
1079
                    $protected_fields[] = $field_to_calculate;
1080
                    if ($schema['properties'][ $field_to_calculate ]['type']) {
1081
                        switch ($schema['properties'][ $field_to_calculate ]['type']) {
1082
                            case 'boolean':
1083
                                $calculated_value = false;
1084
                                break;
1085
                            case 'integer':
1086
                                $calculated_value = 0;
1087
                                break;
1088
                            case 'string':
1089
                                $calculated_value = '';
1090
                                break;
1091
                            case 'array':
1092
                                $calculated_value = array();
1093
                                break;
1094
                            case 'object':
1095
                                $calculated_value = new stdClass();
1096
                                break;
1097
                        }
1098
                    }
1099
                } else {
1100
                    $calculated_value = ModelDataTranslator::prepareFieldValueForJson(
1101
                        null,
1102
                        $this->fields_calculator->retrieveCalculatedFieldValue(
1103
                            $model,
1104
                            $field_to_calculate,
1105
                            $wpdb_row,
1106
                            $rest_request,
1107
                            $this
1108
                        ),
1109
                        $this->getModelVersionInfo()->requestedVersion()
1110
                    );
1111
                }
1112
                $calculated_fields_to_return->{$field_to_calculate} = $calculated_value;
1113
            } catch (RestException $e) {
1114
                // if we don't have permission to read it, just leave it out. but let devs know about the problem
1115
                $this->setResponseHeader(
1116
                    'Notices-Field-Calculation-Errors['
1117
                    . $e->getStringCode()
1118
                    . ']['
1119
                    . $model->get_this_model_name()
1120
                    . ']['
1121
                    . $field_to_calculate
1122
                    . ']',
1123
                    $e->getMessage(),
1124
                    true
1125
                );
1126
            }
1127
        }
1128
        $calculated_fields_to_return->_protected = $protected_fields;
1129
        return $calculated_fields_to_return;
1130
    }
1131
1132
1133
    /**
1134
     * Gets the full URL to the resource, taking the requested version into account
1135
     *
1136
     * @param string $link_part_after_version_and_slash eg "events/10/datetimes"
1137
     * @return string url eg "http://mysite.com/wp-json/ee/v4.6/events/10/datetimes"
1138
     */
1139
    public function getVersionedLinkTo($link_part_after_version_and_slash)
1140
    {
1141
        return rest_url(
1142
            EED_Core_Rest_Api::get_versioned_route_to(
1143
                $link_part_after_version_and_slash,
1144
                $this->getModelVersionInfo()->requestedVersion()
1145
            )
1146
        );
1147
    }
1148
1149
1150
    /**
1151
     * Gets the correct lowercase name for the relation in the API according
1152
     * to the relation's type
1153
     *
1154
     * @param string                  $relation_name
1155
     * @param \EE_Model_Relation_Base $relation_obj
1156
     * @return string
1157
     */
1158
    public static function getRelatedEntityName($relation_name, $relation_obj)
1159
    {
1160
        if ($relation_obj instanceof EE_Belongs_To_Relation) {
1161
            return strtolower($relation_name);
1162
        } else {
1163
            return EEH_Inflector::pluralize_and_lower($relation_name);
1164
        }
1165
    }
1166
1167
1168
    /**
1169
     * Gets the one model object with the specified id for the specified model
1170
     *
1171
     * @param EEM_Base        $model
1172
     * @param WP_REST_Request $request
1173
     * @return array
1174
     */
1175
    public function getEntityFromModel($model, $request)
1176
    {
1177
        $context = $this->validateContext($request->get_param('caps'));
1178
        return $this->getOneOrReportPermissionError($model, $request, $context);
1179
    }
1180
1181
1182
    /**
1183
     * If a context is provided which isn't valid, maybe it was added in a future
1184
     * version so just treat it as a default read
1185
     *
1186
     * @param string $context
1187
     * @return string array key of EEM_Base::cap_contexts_to_cap_action_map()
1188
     */
1189
    public function validateContext($context)
1190
    {
1191
        if (! $context) {
1192
            $context = EEM_Base::caps_read;
1193
        }
1194
        $valid_contexts = EEM_Base::valid_cap_contexts();
1195
        if (in_array($context, $valid_contexts)) {
1196
            return $context;
1197
        } else {
1198
            return EEM_Base::caps_read;
1199
        }
1200
    }
1201
1202
1203
    /**
1204
     * Verifies the passed in value is an allowable default where conditions value.
1205
     *
1206
     * @param $default_query_params
1207
     * @return string
1208
     */
1209
    public function validateDefaultQueryParams($default_query_params)
1210
    {
1211
        $valid_default_where_conditions_for_api_calls = array(
1212
            EEM_Base::default_where_conditions_all,
1213
            EEM_Base::default_where_conditions_minimum_all,
1214
            EEM_Base::default_where_conditions_minimum_others,
1215
        );
1216
        if (! $default_query_params) {
1217
            $default_query_params = EEM_Base::default_where_conditions_all;
1218
        }
1219
        if (in_array(
1220
            $default_query_params,
1221
            $valid_default_where_conditions_for_api_calls,
1222
            true
1223
        )) {
1224
            return $default_query_params;
1225
        } else {
1226
            return EEM_Base::default_where_conditions_all;
1227
        }
1228
    }
1229
1230
1231
    /**
1232
     * 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.
1233
     * Note: right now the query parameter keys for fields (and related fields)
1234
     * can be left as-is, but it's quite possible this will change someday.
1235
     * Also, this method's contents might be candidate for moving to Model_Data_Translator
1236
     *
1237
     * @param EEM_Base $model
1238
     * @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...
1239
     * @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)
1240
     *                                    or FALSE to indicate that absolutely no results should be returned
1241
     * @throws EE_Error
1242
     * @throws RestException
1243
     */
1244
    public function createModelQueryParams($model, $query_params)
1245
    {
1246
        $model_query_params = array();
1247 View Code Duplication
        if (isset($query_params['where'])) {
1248
            $model_query_params[0] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1249
                $query_params['where'],
1250
                $model,
1251
                $this->getModelVersionInfo()->requestedVersion()
1252
            );
1253
        }
1254
        if (isset($query_params['order_by'])) {
1255
            $order_by = $query_params['order_by'];
1256
        } elseif (isset($query_params['orderby'])) {
1257
            $order_by = $query_params['orderby'];
1258
        } else {
1259
            $order_by = null;
1260
        }
1261
        if ($order_by !== null) {
1262
            if (is_array($order_by)) {
1263
                $order_by = ModelDataTranslator::prepareFieldNamesInArrayKeysFromJson($order_by);
1264
            } else {
1265
                // it's a single item
1266
                $order_by = ModelDataTranslator::prepareFieldNameFromJson($order_by);
1267
            }
1268
            $model_query_params['order_by'] = $order_by;
1269
        }
1270
        if (isset($query_params['group_by'])) {
1271
            $group_by = $query_params['group_by'];
1272
        } elseif (isset($query_params['groupby'])) {
1273
            $group_by = $query_params['groupby'];
1274
        } else {
1275
            $group_by = array_keys($model->get_combined_primary_key_fields());
1276
        }
1277
        // make sure they're all real names
1278
        if (is_array($group_by)) {
1279
            $group_by = ModelDataTranslator::prepareFieldNamesFromJson($group_by);
1280
        }
1281
        if ($group_by !== null) {
1282
            $model_query_params['group_by'] = $group_by;
1283
        }
1284 View Code Duplication
        if (isset($query_params['having'])) {
1285
            $model_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1286
                $query_params['having'],
1287
                $model,
1288
                $this->getModelVersionInfo()->requestedVersion()
1289
            );
1290
        }
1291
        if (isset($query_params['order'])) {
1292
            $model_query_params['order'] = $query_params['order'];
1293
        }
1294
        if (isset($query_params['mine'])) {
1295
            $model_query_params = $model->alter_query_params_to_only_include_mine($model_query_params);
1296
        }
1297
        if (isset($query_params['limit'])) {
1298
            // limit should be either a string like '23' or '23,43', or an array with two items in it
1299
            if (! is_array($query_params['limit'])) {
1300
                $limit_array = explode(',', (string) $query_params['limit']);
1301
            } else {
1302
                $limit_array = $query_params['limit'];
1303
            }
1304
            $sanitized_limit = array();
1305
            foreach ($limit_array as $key => $limit_part) {
1306
                if ($this->debug_mode && (! is_numeric($limit_part) || count($sanitized_limit) > 2)) {
1307
                    throw new EE_Error(
1308
                        sprintf(
1309
                            __(
1310
                            // @codingStandardsIgnoreStart
1311
                                '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.',
1312
                                // @codingStandardsIgnoreEnd
1313
                                'event_espresso'
1314
                            ),
1315
                            wp_json_encode($query_params['limit'])
1316
                        )
1317
                    );
1318
                }
1319
                $sanitized_limit[] = (int) $limit_part;
1320
            }
1321
            $model_query_params['limit'] = implode(',', $sanitized_limit);
1322
        } else {
1323
            $model_query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
1324
        }
1325
        if (isset($query_params['caps'])) {
1326
            $model_query_params['caps'] = $this->validateContext($query_params['caps']);
1327
        } else {
1328
            $model_query_params['caps'] = EEM_Base::caps_read;
1329
        }
1330
        if (isset($query_params['default_where_conditions'])) {
1331
            $model_query_params['default_where_conditions'] = $this->validateDefaultQueryParams(
1332
                $query_params['default_where_conditions']
1333
            );
1334
        }
1335
        // if this is a model protected by a password on another model, exclude the password protected
1336
        // entities by default. But if they passed in a password, try to show them all. If the password is wrong,
1337
        // though, they'll get an error (see Read::createEntityFromWpdbResult() which calls Read::checkPassword)
1338
        if (! $model->hasPassword()
1339
            && $model->restrictedByRelatedModelPassword()
1340
            && $model_query_params['caps'] === EEM_Base::caps_read) {
1341
            if (empty($query_params['password'])) {
1342
                $model_query_params['exclude_protected'] = true;
1343
            }
1344
        }
1345
1346
        return apply_filters('FHEE__Read__create_model_query_params', $model_query_params, $query_params, $model);
1347
    }
1348
1349
1350
    /**
1351
     * Changes the REST-style query params for use in the models
1352
     *
1353
     * @deprecated
1354
     * @param EEM_Base $model
1355
     * @param array    $query_params sub-array from @see EEM_Base::get_all()
1356
     * @return array
1357
     */
1358 View Code Duplication
    public function prepareRestQueryParamsKeyForModels($model, $query_params)
1359
    {
1360
        $model_ready_query_params = array();
1361
        foreach ($query_params as $key => $value) {
1362
            if (is_array($value)) {
1363
                $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...
1364
            } else {
1365
                $model_ready_query_params[ $key ] = $value;
1366
            }
1367
        }
1368
        return $model_ready_query_params;
1369
    }
1370
1371
1372
    /**
1373
     * @deprecated instead use ModelDataTranslator::prepareFieldValuesFromJson()
1374
     * @param $model
1375
     * @param $query_params
1376
     * @return array
1377
     */
1378 View Code Duplication
    public function prepareRestQueryParamsValuesForModels($model, $query_params)
1379
    {
1380
        $model_ready_query_params = array();
1381
        foreach ($query_params as $key => $value) {
1382
            if (is_array($value)) {
1383
                $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...
1384
            } else {
1385
                $model_ready_query_params[ $key ] = $value;
1386
            }
1387
        }
1388
        return $model_ready_query_params;
1389
    }
1390
1391
1392
    /**
1393
     * Explodes the string on commas, and only returns items with $prefix followed by a period.
1394
     * If no prefix is specified, returns items with no period.
1395
     *
1396
     * @param string|array $string_to_explode eg "jibba,jabba, blah, blah, blah" or array('jibba', 'jabba' )
1397
     * @param string       $prefix            "Event" or "foobar"
1398
     * @return array $string_to_exploded exploded on COMMAS, and if a prefix was specified
1399
     *                                        we only return strings starting with that and a period; if no prefix was
1400
     *                                        specified we return all items containing NO periods
1401
     */
1402
    public function explodeAndGetItemsPrefixedWith($string_to_explode, $prefix)
1403
    {
1404
        if (is_string($string_to_explode)) {
1405
            $exploded_contents = explode(',', $string_to_explode);
1406
        } elseif (is_array($string_to_explode)) {
1407
            $exploded_contents = $string_to_explode;
1408
        } else {
1409
            $exploded_contents = array();
1410
        }
1411
        // if the string was empty, we want an empty array
1412
        $exploded_contents = array_filter($exploded_contents);
1413
        $contents_with_prefix = array();
1414
        foreach ($exploded_contents as $item) {
1415
            $item = trim($item);
1416
            // if no prefix was provided, so we look for items with no "." in them
1417
            if (! $prefix) {
1418
                // does this item have a period?
1419
                if (strpos($item, '.') === false) {
1420
                    // if not, then its what we're looking for
1421
                    $contents_with_prefix[] = $item;
1422
                }
1423
            } elseif (strpos($item, $prefix . '.') === 0) {
1424
                // this item has the prefix and a period, grab it
1425
                $contents_with_prefix[] = substr(
1426
                    $item,
1427
                    strpos($item, $prefix . '.') + strlen($prefix . '.')
1428
                );
1429
            } elseif ($item === $prefix) {
1430
                // this item is JUST the prefix
1431
                // so let's grab everything after, which is a blank string
1432
                $contents_with_prefix[] = '';
1433
            }
1434
        }
1435
        return $contents_with_prefix;
1436
    }
1437
1438
1439
    /**
1440
     * @deprecated since 4.8.36.rc.001 You should instead use Read::explode_and_get_items_prefixed_with.
1441
     * Deprecated because its return values were really quite confusing- sometimes it returned
1442
     * an empty array (when the include string was blank or '*') or sometimes it returned
1443
     * array('*') (when you provided a model and a model of that kind was found).
1444
     * Parses the $include_string so we fetch all the field names relating to THIS model
1445
     * (ie have NO period in them), or for the provided model (ie start with the model
1446
     * name and then a period).
1447
     * @param string $include_string @see Read:handle_request_get_all
1448
     * @param string $model_name
1449
     * @return array of fields for this model. If $model_name is provided, then
1450
     *                               the fields for that model, with the model's name removed from each.
1451
     *                               If $include_string was blank or '*' returns an empty array
1452
     */
1453
    public function extractIncludesForThisModel($include_string, $model_name = null)
1454
    {
1455
        if (is_array($include_string)) {
1456
            $include_string = implode(',', $include_string);
1457
        }
1458
        if ($include_string === '*' || $include_string === '') {
1459
            return array();
1460
        }
1461
        $includes = explode(',', $include_string);
1462
        $extracted_fields_to_include = array();
1463
        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...
1464
            foreach ($includes as $field_to_include) {
1465
                $field_to_include = trim($field_to_include);
1466
                if (strpos($field_to_include, $model_name . '.') === 0) {
1467
                    // found the model name at the exact start
1468
                    $field_sans_model_name = str_replace($model_name . '.', '', $field_to_include);
1469
                    $extracted_fields_to_include[] = $field_sans_model_name;
1470
                } elseif ($field_to_include == $model_name) {
1471
                    $extracted_fields_to_include[] = '*';
1472
                }
1473
            }
1474
        } else {
1475
            // look for ones with no period
1476
            foreach ($includes as $field_to_include) {
1477
                $field_to_include = trim($field_to_include);
1478
                if (strpos($field_to_include, '.') === false
1479
                    && ! $this->getModelVersionInfo()->isModelNameInThisVersion($field_to_include)
1480
                ) {
1481
                    $extracted_fields_to_include[] = $field_to_include;
1482
                }
1483
            }
1484
        }
1485
        return $extracted_fields_to_include;
1486
    }
1487
1488
1489
    /**
1490
     * Gets the single item using the model according to the request in the context given, otherwise
1491
     * returns that it's inaccessible to the current user
1492
     *
1493
     * @param EEM_Base $model
1494
     * @param WP_REST_Request $request
1495
     * @param null $context
1496
     * @return array
1497
     * @throws EE_Error
1498
     */
1499
    public function getOneOrReportPermissionError(EEM_Base $model, WP_REST_Request $request, $context = null)
1500
    {
1501
        $query_params = array(array($model->primary_key_name() => $request->get_param('id')), 'limit' => 1);
1502
        if ($model instanceof EEM_Soft_Delete_Base) {
1503
            $query_params = $model->alter_query_params_so_deleted_and_undeleted_items_included($query_params);
1504
        }
1505
        $restricted_query_params = $query_params;
1506
        $restricted_query_params['caps'] = $context;
1507
        $this->setDebugInfo('model query params', $restricted_query_params);
1508
        $model_rows = $model->get_all_wpdb_results($restricted_query_params);
1509
        if (! empty($model_rows)) {
1510
            return $this->createEntityFromWpdbResult(
1511
                $model,
1512
                reset($model_rows),
1513
                $request
1514
            );
1515
        } else {
1516
            // ok let's test to see if we WOULD have found it, had we not had restrictions from missing capabilities
1517
            $lowercase_model_name = strtolower($model->get_this_model_name());
1518
            if ($model->exists($query_params)) {
1519
                // you got shafted- it existed but we didn't want to tell you!
1520
                throw new RestException(
1521
                    'rest_user_cannot_' . $context,
1522
                    sprintf(
1523
                        __('Sorry, you cannot %1$s this %2$s. Missing permissions are: %3$s', 'event_espresso'),
1524
                        $context,
1525
                        $lowercase_model_name,
1526
                        Capabilities::getMissingPermissionsString(
1527
                            $model,
1528
                            $context
1529
                        )
1530
                    ),
1531
                    array('status' => 403)
1532
                );
1533
            } else {
1534
                // it's not you. It just doesn't exist
1535
                throw new RestException(
1536
                    sprintf('rest_%s_invalid_id', $lowercase_model_name),
1537
                    sprintf(__('Invalid %s ID.', 'event_espresso'), $lowercase_model_name),
1538
                    array('status' => 404)
1539
                );
1540
            }
1541
        }
1542
    }
1543
1544
    /**
1545
     * Checks that if this content requires a password to be read, that it's been provided and is correct.
1546
     * @since 4.9.74.p
1547
     * @param EEM_Base $model
1548
     * @param $model_row
1549
     * @param $query_params
1550
     * @param WP_REST_Request $request
1551
     * @throws EE_Error
1552
     * @throws InvalidArgumentException
1553
     * @throws InvalidDataTypeException
1554
     * @throws InvalidInterfaceException
1555
     * @throws RestPasswordRequiredException
1556
     * @throws RestPasswordIncorrectException
1557
     * @throws \EventEspresso\core\exceptions\ModelConfigurationException
1558
     * @throws ReflectionException
1559
     */
1560
    protected function checkPassword(EEM_Base $model, $model_row, $query_params, WP_REST_Request $request)
1561
    {
1562
        // stuff is only "protected" for front-end requests. Elsewhere, you either get full permission to access the object
1563
        // or you don't.
1564
        $request_caps = $request->get_param('caps');
1565
        if (isset($request_caps) && $request_caps !== EEM_Base::caps_read) {
1566
            return;
1567
        }
1568
        // if this entity requires a password, they better give it and it better be right!
1569
        if ($model->hasPassword()
1570
            && $model_row[ $model->getPasswordField()->get_qualified_column() ] !== '') {
1571
            if (empty($request['password'])) {
1572
                throw new RestPasswordRequiredException();
1573
            } elseif (!hash_equals(
1574
                $model_row[ $model->getPasswordField()->get_qualified_column() ],
1575
                $request['password']
1576
            )) {
1577
                throw new RestPasswordIncorrectException();
1578
            }
1579
        } // wait! maybe this content is password protected
1580
        elseif ($model->restrictedByRelatedModelPassword()
1581
            && $request->get_param('caps') === EEM_Base::caps_read) {
1582
            $password_supplied = $request->get_param('password');
1583
            if (empty($password_supplied)) {
1584
                $query_params['exclude_protected'] = true;
1585
                if (!$model->exists($query_params)) {
1586
                    throw new RestPasswordRequiredException();
1587
                }
1588
            } else {
1589
                $query_params[0][ $model->modelChainAndPassword() ] = $password_supplied;
1590
                if (!$model->exists($query_params)) {
1591
                    throw new RestPasswordIncorrectException();
1592
                }
1593
            }
1594
        }
1595
    }
1596
}
1597