Completed
Branch FET/editor-dates-tickets-refac... (c72528)
by
unknown
46:50 queued 36:04
created
core/libraries/rest_api/controllers/model/Read.php 1 patch
Indentation   +1555 added lines, -1555 removed lines patch added patch discarded remove patch
@@ -45,1559 +45,1559 @@
 block discarded – undo
45 45
 {
46 46
 
47 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
-    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
-                    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
-            } 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
-    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());
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);
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(),
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(),
479
-                    $result,
480
-                    $request
481
-                );
482
-                $joined_result = array_merge($join_model_result, $nice_result);
483
-                // $joined_result = array_merge($nice_result, $join_model_result);
484
-                // but keep the meta stuff from the main model
485
-                if (isset($nice_result['meta'])) {
486
-                    $joined_result['meta'] = $nice_result['meta'];
487
-                }
488
-                $nice_result = $joined_result;
489
-            }
490
-            $nice_results[] = $nice_result;
491
-        }
492
-        if ($relation instanceof EE_Belongs_To_Relation) {
493
-            return array_shift($nice_results);
494
-        } else {
495
-            return $nice_results;
496
-        }
497
-    }
498
-
499
-
500
-    /**
501
-     * Gets the collection for given relation object
502
-     * The same as Read::get_entities_from_model(), except if the relation
503
-     * is a HABTM relation, in which case it merges any non-foreign-key fields from
504
-     * the join-model-object into the results
505
-     *
506
-     * @param string                  $id the ID of the thing we are fetching related stuff from
507
-     * @param \EE_Model_Relation_Base $relation
508
-     * @param WP_REST_Request         $request
509
-     * @return array
510
-     * @throws EE_Error
511
-     */
512
-    public function getEntitiesFromRelation($id, $relation, $request)
513
-    {
514
-        if (! $relation->get_this_model()->has_primary_key_field()) {
515
-            throw new EE_Error(
516
-                sprintf(
517
-                    __(
518
-                    // @codingStandardsIgnoreStart
519
-                        'Read::get_entities_from_relation should only be called from a model with a primary key, it was called from %1$s',
520
-                        // @codingStandardsIgnoreEnd
521
-                        'event_espresso'
522
-                    ),
523
-                    $relation->get_this_model()->get_this_model_name()
524
-                )
525
-            );
526
-        }
527
-        // can we edit that main item?
528
-        // if not, show nothing but an error
529
-        // otherwise, please proceed
530
-        return $this->getEntitiesFromRelationUsingModelQueryParams(
531
-            array(
532
-                array(
533
-                    $relation->get_this_model()->primary_key_name() => $id,
534
-                ),
535
-            ),
536
-            $relation,
537
-            $request
538
-        );
539
-    }
540
-
541
-
542
-    /**
543
-     * Sets the headers that are based on the model and query params,
544
-     * like the total records. This should only be called on the original request
545
-     * from the client, not on subsequent internal
546
-     *
547
-     * @param EEM_Base $model
548
-     * @param array    $query_params
549
-     * @return void
550
-     */
551
-    protected function setHeadersFromQueryParams($model, $query_params)
552
-    {
553
-        $this->setDebugInfo('model query params', $query_params);
554
-        $this->setDebugInfo(
555
-            'missing caps',
556
-            Capabilities::getMissingPermissionsString($model, $query_params['caps'])
557
-        );
558
-        // normally the limit to a 2-part array, where the 2nd item is the limit
559
-        if (! isset($query_params['limit'])) {
560
-            $query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
561
-        }
562
-        if (is_array($query_params['limit'])) {
563
-            $limit_parts = $query_params['limit'];
564
-        } else {
565
-            $limit_parts = explode(',', $query_params['limit']);
566
-            if (count($limit_parts) == 1) {
567
-                $limit_parts = array(0, $limit_parts[0]);
568
-            }
569
-        }
570
-        // remove the group by and having parts of the query, as those will
571
-        // make the sql query return an array of values, instead of just a single value
572
-        unset($query_params['group_by'], $query_params['having'], $query_params['limit']);
573
-        $count = $model->count($query_params, null, true);
574
-        $pages = $count / $limit_parts[1];
575
-        $this->setResponseHeader('Total', $count, false);
576
-        $this->setResponseHeader('PageSize', $limit_parts[1], false);
577
-        $this->setResponseHeader('TotalPages', ceil($pages), false);
578
-    }
579
-
580
-
581
-    /**
582
-     * Changes database results into REST API entities
583
-     *
584
-     * @param EEM_Base $model
585
-     * @param array $db_row like results from $wpdb->get_results()
586
-     * @param WP_REST_Request $rest_request
587
-     * @param string $deprecated no longer used
588
-     * @return array ready for being converted into json for sending to client
589
-     * @throws EE_Error
590
-     * @throws RestException
591
-     * @throws InvalidDataTypeException
592
-     * @throws InvalidInterfaceException
593
-     * @throws InvalidArgumentException
594
-     * @throws ReflectionException
595
-     */
596
-    public function createEntityFromWpdbResult($model, $db_row, $rest_request, $deprecated = null)
597
-    {
598
-        if (! $rest_request instanceof WP_REST_Request) {
599
-            // ok so this was called in the old style, where the 3rd arg was
600
-            // $include, and the 4th arg was $context
601
-            // now setup the request just to avoid fatal errors, although we won't be able
602
-            // to truly make use of it because it's kinda devoid of info
603
-            $rest_request = new WP_REST_Request();
604
-            $rest_request->set_param('include', $rest_request);
605
-            $rest_request->set_param('caps', $deprecated);
606
-        }
607
-        if ($rest_request->get_param('caps') == null) {
608
-            $rest_request->set_param('caps', EEM_Base::caps_read);
609
-        }
610
-        $current_user_full_access_to_entity = $model->currentUserCan(
611
-            EEM_Base::caps_read_admin,
612
-            $model->deduce_fields_n_values_from_cols_n_values($db_row)
613
-        );
614
-        $entity_array = $this->createBareEntityFromWpdbResults($model, $db_row);
615
-        $entity_array = $this->addExtraFields($model, $db_row, $entity_array);
616
-        $entity_array['_links'] = $this->getEntityLinks($model, $db_row, $entity_array);
617
-        // when it's a regular read request for a model with a password and the password wasn't provided
618
-        // remove the password protected fields
619
-        $has_protected_fields = false;
620
-        try {
621
-            $this->checkPassword(
622
-                $model,
623
-                $db_row,
624
-                $model->alter_query_params_to_restrict_by_ID(
625
-                    $model->get_index_primary_key_string(
626
-                        $model->deduce_fields_n_values_from_cols_n_values($db_row)
627
-                    )
628
-                ),
629
-                $rest_request
630
-            );
631
-        } catch (RestPasswordRequiredException $e) {
632
-            if ($model->hasPassword()) {
633
-                // just remove protected fields
634
-                $has_protected_fields = true;
635
-                $entity_array = Capabilities::filterOutPasswordProtectedFields(
636
-                    $entity_array,
637
-                    $model,
638
-                    $this->getModelVersionInfo()
639
-                );
640
-            } else {
641
-                // that's a problem. None of this should be accessible if no password was provided
642
-                throw $e;
643
-            }
644
-        }
645
-
646
-        $entity_array['_calculated_fields'] = $this->getEntityCalculations($model, $db_row, $rest_request, $has_protected_fields);
647
-        $entity_array = apply_filters(
648
-            'FHEE__Read__create_entity_from_wpdb_results__entity_before_including_requested_models',
649
-            $entity_array,
650
-            $model,
651
-            $rest_request->get_param('caps'),
652
-            $rest_request,
653
-            $this
654
-        );
655
-        // add an empty protected property for now. If it's still around after we remove everything the request didn't
656
-        // want, we'll populate it then. k?
657
-        $entity_array['_protected'] = array();
658
-        // remove any properties the request didn't want. This way _protected won't bother mentioning them
659
-        $entity_array = $this->includeOnlyRequestedProperties($model, $rest_request, $entity_array);
660
-        $entity_array = $this->includeRequestedModels($model, $rest_request, $entity_array, $db_row, $has_protected_fields);
661
-        // if they still wanted the _protected property, add it.
662
-        if (isset($entity_array['_protected'])) {
663
-            $entity_array = $this->addProtectedProperty($model, $entity_array, $has_protected_fields);
664
-        }
665
-        $entity_array = apply_filters(
666
-            'FHEE__Read__create_entity_from_wpdb_results__entity_before_inaccessible_field_removal',
667
-            $entity_array,
668
-            $model,
669
-            $rest_request->get_param('caps'),
670
-            $rest_request,
671
-            $this
672
-        );
673
-        if (! $current_user_full_access_to_entity) {
674
-            $result_without_inaccessible_fields = Capabilities::filterOutInaccessibleEntityFields(
675
-                $entity_array,
676
-                $model,
677
-                $rest_request->get_param('caps'),
678
-                $this->getModelVersionInfo()
679
-            );
680
-        } else {
681
-            $result_without_inaccessible_fields = $entity_array;
682
-        }
683
-        $this->setDebugInfo(
684
-            'inaccessible fields',
685
-            array_keys(array_diff_key((array) $entity_array, (array) $result_without_inaccessible_fields))
686
-        );
687
-        return apply_filters(
688
-            'FHEE__Read__create_entity_from_wpdb_results__entity_return',
689
-            $result_without_inaccessible_fields,
690
-            $model,
691
-            $rest_request->get_param('caps')
692
-        );
693
-    }
694
-
695
-    /**
696
-     * Returns an array describing which fields can be protected, and which actually were removed this request
697
-     * @since 4.9.74.p
698
-     * @param $model
699
-     * @param $results_so_far
700
-     * @param $protected
701
-     * @return array results
702
-     */
703
-    protected function addProtectedProperty(EEM_Base $model, $results_so_far, $protected)
704
-    {
705
-        if (! $model->hasPassword() || ! $protected) {
706
-            return $results_so_far;
707
-        }
708
-        $password_field = $model->getPasswordField();
709
-        $all_protected = array_merge(
710
-            array($password_field->get_name()),
711
-            $password_field->protectedFields()
712
-        );
713
-        $fields_included = array_keys($results_so_far);
714
-        $fields_included = array_intersect(
715
-            $all_protected,
716
-            $fields_included
717
-        );
718
-        foreach ($fields_included as $field_name) {
719
-            $results_so_far['_protected'][] = $field_name ;
720
-        }
721
-        return $results_so_far;
722
-    }
723
-
724
-    /**
725
-     * Creates a REST entity array (JSON object we're going to return in the response, but
726
-     * for now still a PHP array, but soon enough we'll call json_encode on it, don't worry),
727
-     * from $wpdb->get_row( $sql, ARRAY_A)
728
-     *
729
-     * @param EEM_Base $model
730
-     * @param array    $db_row
731
-     * @return array entity mostly ready for converting to JSON and sending in the response
732
-     */
733
-    protected function createBareEntityFromWpdbResults(EEM_Base $model, $db_row)
734
-    {
735
-        $result = $model->deduce_fields_n_values_from_cols_n_values($db_row);
736
-        $result = array_intersect_key(
737
-            $result,
738
-            $this->getModelVersionInfo()->fieldsOnModelInThisVersion($model)
739
-        );
740
-        // if this is a CPT, we need to set the global $post to it,
741
-        // otherwise shortcodes etc won't work properly while rendering it
742
-        if ($model instanceof \EEM_CPT_Base) {
743
-            $do_chevy_shuffle = true;
744
-        } else {
745
-            $do_chevy_shuffle = false;
746
-        }
747
-        if ($do_chevy_shuffle) {
748
-            global $post;
749
-            $old_post = $post;
750
-            $post = get_post($result[ $model->primary_key_name() ]);
751
-            if (! $post instanceof \WP_Post) {
752
-                // well that's weird, because $result is what we JUST fetched from the database
753
-                throw new RestException(
754
-                    'error_fetching_post_from_database_results',
755
-                    esc_html__(
756
-                        'An item was retrieved from the database but it\'s not a WP_Post like it should be.',
757
-                        'event_espresso'
758
-                    )
759
-                );
760
-            }
761
-            $model_object_classname = 'EE_' . $model->get_this_model_name();
762
-            $post->{$model_object_classname} = \EE_Registry::instance()->load_class(
763
-                $model_object_classname,
764
-                $result,
765
-                false,
766
-                false
767
-            );
768
-        }
769
-        foreach ($result as $field_name => $field_value) {
770
-            $field_obj = $model->field_settings_for($field_name);
771
-            if ($this->isSubclassOfOne($field_obj, $this->getModelVersionInfo()->fieldsIgnored())) {
772
-                unset($result[ $field_name ]);
773
-            } elseif ($this->isSubclassOfOne(
774
-                $field_obj,
775
-                $this->getModelVersionInfo()->fieldsThatHaveRenderedFormat()
776
-            )
777
-            ) {
778
-                $result[ $field_name ] = array(
779
-                    'raw'      => $this->prepareFieldObjValueForJson($field_obj, $field_value),
780
-                    'rendered' => $this->prepareFieldObjValueForJson($field_obj, $field_value, 'pretty'),
781
-                );
782
-            } elseif ($this->isSubclassOfOne(
783
-                $field_obj,
784
-                $this->getModelVersionInfo()->fieldsThatHavePrettyFormat()
785
-            )
786
-            ) {
787
-                $result[ $field_name ] = array(
788
-                    'raw'    => $this->prepareFieldObjValueForJson($field_obj, $field_value),
789
-                    'pretty' => $this->prepareFieldObjValueForJson($field_obj, $field_value, 'pretty'),
790
-                );
791
-            } elseif ($field_obj instanceof \EE_Datetime_Field) {
792
-                $field_value = $field_obj->prepare_for_set_from_db($field_value);
793
-                // if the value is null, but we're not supposed to permit null, then set to the field's default
794
-                if (is_null($field_value)) {
795
-                    $field_value = $field_obj->getDefaultDateTimeObj();
796
-                }
797
-                if (is_null($field_value)) {
798
-                    $gmt_date = $local_date = ModelDataTranslator::prepareFieldValuesForJson(
799
-                        $field_obj,
800
-                        $field_value,
801
-                        $this->getModelVersionInfo()->requestedVersion()
802
-                    );
803
-                } else {
804
-                    $timezone = $field_value->getTimezone();
805
-                    EEH_DTT_Helper::setTimezone($field_value, new DateTimeZone('UTC'));
806
-                    $gmt_date = ModelDataTranslator::prepareFieldValuesForJson(
807
-                        $field_obj,
808
-                        $field_value,
809
-                        $this->getModelVersionInfo()->requestedVersion()
810
-                    );
811
-                    EEH_DTT_Helper::setTimezone($field_value, $timezone);
812
-                    $local_date = ModelDataTranslator::prepareFieldValuesForJson(
813
-                        $field_obj,
814
-                        $field_value,
815
-                        $this->getModelVersionInfo()->requestedVersion()
816
-                    );
817
-                }
818
-                $result[ $field_name . '_gmt' ] = $gmt_date;
819
-                $result[ $field_name ] = $local_date;
820
-            } else {
821
-                $result[ $field_name ] = $this->prepareFieldObjValueForJson($field_obj, $field_value);
822
-            }
823
-        }
824
-        if ($do_chevy_shuffle) {
825
-            $post = $old_post;
826
-        }
827
-        return $result;
828
-    }
829
-
830
-
831
-    /**
832
-     * Takes a value all the way from the DB representation, to the model object's representation, to the
833
-     * user-facing PHP representation, to the REST API representation. (Assumes you've already taken from the DB
834
-     * representation using $field_obj->prepare_for_set_from_db())
835
-     *
836
-     * @param EE_Model_Field_Base $field_obj
837
-     * @param mixed               $value  as it's stored on a model object
838
-     * @param string              $format valid values are 'normal' (default), 'pretty', 'datetime_obj'
839
-     * @return mixed
840
-     * @throws ObjectDetectedException if $value contains a PHP object
841
-     */
842
-    protected function prepareFieldObjValueForJson(EE_Model_Field_Base $field_obj, $value, $format = 'normal')
843
-    {
844
-        $value = $field_obj->prepare_for_set_from_db($value);
845
-        switch ($format) {
846
-            case 'pretty':
847
-                $value = $field_obj->prepare_for_pretty_echoing($value);
848
-                break;
849
-            case 'normal':
850
-            default:
851
-                $value = $field_obj->prepare_for_get($value);
852
-                break;
853
-        }
854
-        return ModelDataTranslator::prepareFieldValuesForJson(
855
-            $field_obj,
856
-            $value,
857
-            $this->getModelVersionInfo()->requestedVersion()
858
-        );
859
-    }
860
-
861
-
862
-    /**
863
-     * Adds a few extra fields to the entity response
864
-     *
865
-     * @param EEM_Base $model
866
-     * @param array    $db_row
867
-     * @param array    $entity_array
868
-     * @return array modified entity
869
-     */
870
-    protected function addExtraFields(EEM_Base $model, $db_row, $entity_array)
871
-    {
872
-        if ($model instanceof EEM_CPT_Base) {
873
-            $entity_array['link'] = get_permalink($db_row[ $model->get_primary_key_field()->get_qualified_column() ]);
874
-        }
875
-        return $entity_array;
876
-    }
877
-
878
-
879
-    /**
880
-     * Gets links we want to add to the response
881
-     *
882
-     * @global \WP_REST_Server $wp_rest_server
883
-     * @param EEM_Base         $model
884
-     * @param array            $db_row
885
-     * @param array            $entity_array
886
-     * @return array the _links item in the entity
887
-     */
888
-    protected function getEntityLinks($model, $db_row, $entity_array)
889
-    {
890
-        // add basic links
891
-        $links = array();
892
-        if ($model->has_primary_key_field()) {
893
-            $links['self'] = array(
894
-                array(
895
-                    'href' => $this->getVersionedLinkTo(
896
-                        EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
897
-                        . '/'
898
-                        . $entity_array[ $model->primary_key_name() ]
899
-                    ),
900
-                ),
901
-            );
902
-        }
903
-        $links['collection'] = array(
904
-            array(
905
-                'href' => $this->getVersionedLinkTo(
906
-                    EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
907
-                ),
908
-            ),
909
-        );
910
-        // add links to related models
911
-        if ($model->has_primary_key_field()) {
912
-            foreach ($this->getModelVersionInfo()->relationSettings($model) as $relation_name => $relation_obj) {
913
-                $related_model_part = Read::getRelatedEntityName($relation_name, $relation_obj);
914
-                $links[ EED_Core_Rest_Api::ee_api_link_namespace . $related_model_part ] = array(
915
-                    array(
916
-                        'href'   => $this->getVersionedLinkTo(
917
-                            EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
918
-                            . '/'
919
-                            . $entity_array[ $model->primary_key_name() ]
920
-                            . '/'
921
-                            . $related_model_part
922
-                        ),
923
-                        'single' => $relation_obj instanceof EE_Belongs_To_Relation ? true : false,
924
-                    ),
925
-                );
926
-            }
927
-        }
928
-        return $links;
929
-    }
930
-
931
-
932
-    /**
933
-     * Adds the included models indicated in the request to the entity provided
934
-     *
935
-     * @param EEM_Base $model
936
-     * @param WP_REST_Request $rest_request
937
-     * @param array $entity_array
938
-     * @param array $db_row
939
-     * @param boolean $included_items_protected if the original item is password protected, don't include any related models.
940
-     * @return array the modified entity
941
-     * @throws RestException
942
-     */
943
-    protected function includeRequestedModels(
944
-        EEM_Base $model,
945
-        WP_REST_Request $rest_request,
946
-        $entity_array,
947
-        $db_row = array(),
948
-        $included_items_protected = false
949
-    ) {
950
-        // if $db_row not included, hope the entity array has what we need
951
-        if (! $db_row) {
952
-            $db_row = $entity_array;
953
-        }
954
-        $relation_settings = $this->getModelVersionInfo()->relationSettings($model);
955
-        foreach ($relation_settings as $relation_name => $relation_obj) {
956
-            $related_fields_to_include = $this->explodeAndGetItemsPrefixedWith(
957
-                $rest_request->get_param('include'),
958
-                $relation_name
959
-            );
960
-            $related_fields_to_calculate = $this->explodeAndGetItemsPrefixedWith(
961
-                $rest_request->get_param('calculate'),
962
-                $relation_name
963
-            );
964
-            // did they specify they wanted to include a related model, or
965
-            // specific fields from a related model?
966
-            // or did they specify to calculate a field from a related model?
967
-            if ($related_fields_to_include || $related_fields_to_calculate) {
968
-                // if so, we should include at least some part of the related model
969
-                $pretend_related_request = new WP_REST_Request();
970
-                $pretend_related_request->set_query_params(
971
-                    array(
972
-                        'caps'      => $rest_request->get_param('caps'),
973
-                        'include'   => $related_fields_to_include,
974
-                        'calculate' => $related_fields_to_calculate,
975
-                        'password' => $rest_request->get_param('password')
976
-                    )
977
-                );
978
-                $pretend_related_request->add_header('no_rest_headers', true);
979
-                $primary_model_query_params = $model->alter_query_params_to_restrict_by_ID(
980
-                    $model->get_index_primary_key_string(
981
-                        $model->deduce_fields_n_values_from_cols_n_values($db_row)
982
-                    )
983
-                );
984
-                if (! $included_items_protected) {
985
-                    try {
986
-                        $related_results = $this->getEntitiesFromRelationUsingModelQueryParams(
987
-                            $primary_model_query_params,
988
-                            $relation_obj,
989
-                            $pretend_related_request
990
-                        );
991
-                    } catch (RestException $e) {
992
-                        $related_results = null;
993
-                    }
994
-                } else {
995
-                    // they're protected, hide them.
996
-                    $related_results = null;
997
-                    $entity_array['_protected'][] = Read::getRelatedEntityName($relation_name, $relation_obj);
998
-                }
999
-                if ($related_results instanceof WP_Error || $related_results === null) {
1000
-                    $related_results = $relation_obj instanceof EE_Belongs_To_Relation ? null : array();
1001
-                }
1002
-                $entity_array[ Read::getRelatedEntityName($relation_name, $relation_obj) ] = $related_results;
1003
-            }
1004
-        }
1005
-        return $entity_array;
1006
-    }
1007
-
1008
-    /**
1009
-     * If the user has requested only specific properties (including meta properties like _links or _protected)
1010
-     * remove everything else.
1011
-     * @since 4.9.74.p
1012
-     * @param EEM_Base $model
1013
-     * @param WP_REST_Request $rest_request
1014
-     * @param $entity_array
1015
-     * @return array
1016
-     * @throws EE_Error
1017
-     */
1018
-    protected function includeOnlyRequestedProperties(
1019
-        EEM_Base $model,
1020
-        WP_REST_Request $rest_request,
1021
-        $entity_array
1022
-    ) {
1023
-
1024
-        $includes_for_this_model = $this->explodeAndGetItemsPrefixedWith($rest_request->get_param('include'), '');
1025
-        $includes_for_this_model = $this->removeModelNamesFromArray($includes_for_this_model);
1026
-        // if they passed in * or didn't specify any includes, return everything
1027
-        if (! in_array('*', $includes_for_this_model)
1028
-            && ! empty($includes_for_this_model)
1029
-        ) {
1030
-            if ($model->has_primary_key_field()) {
1031
-                // always include the primary key. ya just gotta know that at least
1032
-                $includes_for_this_model[] = $model->primary_key_name();
1033
-            }
1034
-            if ($this->explodeAndGetItemsPrefixedWith($rest_request->get_param('calculate'), '')) {
1035
-                $includes_for_this_model[] = '_calculated_fields';
1036
-            }
1037
-            $entity_array = array_intersect_key($entity_array, array_flip($includes_for_this_model));
1038
-        }
1039
-        return $entity_array;
1040
-    }
1041
-
1042
-
1043
-    /**
1044
-     * Returns a new array with all the names of models removed. Eg
1045
-     * array( 'Event', 'Datetime.*', 'foobar' ) would become array( 'Datetime.*', 'foobar' )
1046
-     *
1047
-     * @param array $arr
1048
-     * @return array
1049
-     */
1050
-    private function removeModelNamesFromArray($arr)
1051
-    {
1052
-        return array_diff($arr, array_keys(EE_Registry::instance()->non_abstract_db_models));
1053
-    }
1054
-
1055
-
1056
-    /**
1057
-     * Gets the calculated fields for the response
1058
-     *
1059
-     * @param EEM_Base        $model
1060
-     * @param array           $wpdb_row
1061
-     * @param WP_REST_Request $rest_request
1062
-     * @param boolean $row_is_protected whether this row is password protected or not
1063
-     * @return \stdClass the _calculations item in the entity
1064
-     * @throws ObjectDetectedException if a default value has a PHP object, which should never do (and if we
1065
-     * did, let's know about it ASAP, so let the exception bubble up)
1066
-     */
1067
-    protected function getEntityCalculations($model, $wpdb_row, $rest_request, $row_is_protected = false)
1068
-    {
1069
-        $calculated_fields = $this->explodeAndGetItemsPrefixedWith(
1070
-            $rest_request->get_param('calculate'),
1071
-            ''
1072
-        );
1073
-        // note: setting calculate=* doesn't do anything
1074
-        $calculated_fields_to_return = new \stdClass();
1075
-        $protected_fields = array();
1076
-        foreach ($calculated_fields as $field_to_calculate) {
1077
-            try {
1078
-                // it's password protected, so they shouldn't be able to read this. Remove the value
1079
-                $schema = $this->fields_calculator->getJsonSchemaForModel($model);
1080
-                if ($row_is_protected
1081
-                    && isset($schema['properties'][ $field_to_calculate ]['protected'])
1082
-                    && $schema['properties'][ $field_to_calculate ]['protected']) {
1083
-                    $calculated_value = null;
1084
-                    $protected_fields[] = $field_to_calculate;
1085
-                    if ($schema['properties'][ $field_to_calculate ]['type']) {
1086
-                        switch ($schema['properties'][ $field_to_calculate ]['type']) {
1087
-                            case 'boolean':
1088
-                                $calculated_value = false;
1089
-                                break;
1090
-                            case 'integer':
1091
-                                $calculated_value = 0;
1092
-                                break;
1093
-                            case 'string':
1094
-                                $calculated_value = '';
1095
-                                break;
1096
-                            case 'array':
1097
-                                $calculated_value = array();
1098
-                                break;
1099
-                            case 'object':
1100
-                                $calculated_value = new stdClass();
1101
-                                break;
1102
-                        }
1103
-                    }
1104
-                } else {
1105
-                    $calculated_value = ModelDataTranslator::prepareFieldValueForJson(
1106
-                        null,
1107
-                        $this->fields_calculator->retrieveCalculatedFieldValue(
1108
-                            $model,
1109
-                            $field_to_calculate,
1110
-                            $wpdb_row,
1111
-                            $rest_request,
1112
-                            $this
1113
-                        ),
1114
-                        $this->getModelVersionInfo()->requestedVersion()
1115
-                    );
1116
-                }
1117
-                $calculated_fields_to_return->{$field_to_calculate} = $calculated_value;
1118
-            } catch (RestException $e) {
1119
-                // if we don't have permission to read it, just leave it out. but let devs know about the problem
1120
-                $this->setResponseHeader(
1121
-                    'Notices-Field-Calculation-Errors['
1122
-                    . $e->getStringCode()
1123
-                    . ']['
1124
-                    . $model->get_this_model_name()
1125
-                    . ']['
1126
-                    . $field_to_calculate
1127
-                    . ']',
1128
-                    $e->getMessage(),
1129
-                    true
1130
-                );
1131
-            }
1132
-        }
1133
-        $calculated_fields_to_return->_protected = $protected_fields;
1134
-        return $calculated_fields_to_return;
1135
-    }
1136
-
1137
-
1138
-    /**
1139
-     * Gets the full URL to the resource, taking the requested version into account
1140
-     *
1141
-     * @param string $link_part_after_version_and_slash eg "events/10/datetimes"
1142
-     * @return string url eg "http://mysite.com/wp-json/ee/v4.6/events/10/datetimes"
1143
-     */
1144
-    public function getVersionedLinkTo($link_part_after_version_and_slash)
1145
-    {
1146
-        return rest_url(
1147
-            EED_Core_Rest_Api::get_versioned_route_to(
1148
-                $link_part_after_version_and_slash,
1149
-                $this->getModelVersionInfo()->requestedVersion()
1150
-            )
1151
-        );
1152
-    }
1153
-
1154
-
1155
-    /**
1156
-     * Gets the correct lowercase name for the relation in the API according
1157
-     * to the relation's type
1158
-     *
1159
-     * @param string                  $relation_name
1160
-     * @param \EE_Model_Relation_Base $relation_obj
1161
-     * @return string
1162
-     */
1163
-    public static function getRelatedEntityName($relation_name, $relation_obj)
1164
-    {
1165
-        if ($relation_obj instanceof EE_Belongs_To_Relation) {
1166
-            return strtolower($relation_name);
1167
-        } else {
1168
-            return EEH_Inflector::pluralize_and_lower($relation_name);
1169
-        }
1170
-    }
1171
-
1172
-
1173
-    /**
1174
-     * Gets the one model object with the specified id for the specified model
1175
-     *
1176
-     * @param EEM_Base        $model
1177
-     * @param WP_REST_Request $request
1178
-     * @return array
1179
-     */
1180
-    public function getEntityFromModel($model, $request)
1181
-    {
1182
-        $context = $this->validateContext($request->get_param('caps'));
1183
-        return $this->getOneOrReportPermissionError($model, $request, $context);
1184
-    }
1185
-
1186
-
1187
-    /**
1188
-     * If a context is provided which isn't valid, maybe it was added in a future
1189
-     * version so just treat it as a default read
1190
-     *
1191
-     * @param string $context
1192
-     * @return string array key of EEM_Base::cap_contexts_to_cap_action_map()
1193
-     */
1194
-    public function validateContext($context)
1195
-    {
1196
-        if (! $context) {
1197
-            $context = EEM_Base::caps_read;
1198
-        }
1199
-        $valid_contexts = EEM_Base::valid_cap_contexts();
1200
-        if (in_array($context, $valid_contexts)) {
1201
-            return $context;
1202
-        } else {
1203
-            return EEM_Base::caps_read;
1204
-        }
1205
-    }
1206
-
1207
-
1208
-    /**
1209
-     * Verifies the passed in value is an allowable default where conditions value.
1210
-     *
1211
-     * @param $default_query_params
1212
-     * @return string
1213
-     */
1214
-    public function validateDefaultQueryParams($default_query_params)
1215
-    {
1216
-        $valid_default_where_conditions_for_api_calls = array(
1217
-            EEM_Base::default_where_conditions_all,
1218
-            EEM_Base::default_where_conditions_minimum_all,
1219
-            EEM_Base::default_where_conditions_minimum_others,
1220
-        );
1221
-        if (! $default_query_params) {
1222
-            $default_query_params = EEM_Base::default_where_conditions_all;
1223
-        }
1224
-        if (in_array(
1225
-            $default_query_params,
1226
-            $valid_default_where_conditions_for_api_calls,
1227
-            true
1228
-        )) {
1229
-            return $default_query_params;
1230
-        } else {
1231
-            return EEM_Base::default_where_conditions_all;
1232
-        }
1233
-    }
1234
-
1235
-
1236
-    /**
1237
-     * 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.
1238
-     * Note: right now the query parameter keys for fields (and related fields)
1239
-     * can be left as-is, but it's quite possible this will change someday.
1240
-     * Also, this method's contents might be candidate for moving to Model_Data_Translator
1241
-     *
1242
-     * @param EEM_Base $model
1243
-     * @param array    $query_parameters  from $_GET parameter @see Read:handle_request_get_all
1244
-     * @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)
1245
-     *                                    or FALSE to indicate that absolutely no results should be returned
1246
-     * @throws EE_Error
1247
-     * @throws RestException
1248
-     */
1249
-    public function createModelQueryParams($model, $query_params)
1250
-    {
1251
-        $model_query_params = array();
1252
-        if (isset($query_params['where'])) {
1253
-            $model_query_params[0] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1254
-                $query_params['where'],
1255
-                $model,
1256
-                $this->getModelVersionInfo()->requestedVersion()
1257
-            );
1258
-        }
1259
-        if (isset($query_params['order_by'])) {
1260
-            $order_by = $query_params['order_by'];
1261
-        } elseif (isset($query_params['orderby'])) {
1262
-            $order_by = $query_params['orderby'];
1263
-        } else {
1264
-            $order_by = null;
1265
-        }
1266
-        if ($order_by !== null) {
1267
-            if (is_array($order_by)) {
1268
-                $order_by = ModelDataTranslator::prepareFieldNamesInArrayKeysFromJson($order_by);
1269
-            } else {
1270
-                // it's a single item
1271
-                $order_by = ModelDataTranslator::prepareFieldNameFromJson($order_by);
1272
-            }
1273
-            $model_query_params['order_by'] = $order_by;
1274
-        }
1275
-        if (isset($query_params['group_by'])) {
1276
-            $group_by = $query_params['group_by'];
1277
-        } elseif (isset($query_params['groupby'])) {
1278
-            $group_by = $query_params['groupby'];
1279
-        } else {
1280
-            $group_by = array_keys($model->get_combined_primary_key_fields());
1281
-        }
1282
-        // make sure they're all real names
1283
-        if (is_array($group_by)) {
1284
-            $group_by = ModelDataTranslator::prepareFieldNamesFromJson($group_by);
1285
-        }
1286
-        if ($group_by !== null) {
1287
-            $model_query_params['group_by'] = $group_by;
1288
-        }
1289
-        if (isset($query_params['having'])) {
1290
-            $model_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1291
-                $query_params['having'],
1292
-                $model,
1293
-                $this->getModelVersionInfo()->requestedVersion()
1294
-            );
1295
-        }
1296
-        if (isset($query_params['order'])) {
1297
-            $model_query_params['order'] = $query_params['order'];
1298
-        }
1299
-        if (isset($query_params['mine'])) {
1300
-            $model_query_params = $model->alter_query_params_to_only_include_mine($model_query_params);
1301
-        }
1302
-        if (isset($query_params['limit'])) {
1303
-            // limit should be either a string like '23' or '23,43', or an array with two items in it
1304
-            if (! is_array($query_params['limit'])) {
1305
-                $limit_array = explode(',', (string) $query_params['limit']);
1306
-            } else {
1307
-                $limit_array = $query_params['limit'];
1308
-            }
1309
-            $sanitized_limit = array();
1310
-            foreach ($limit_array as $key => $limit_part) {
1311
-                if ($this->debug_mode && (! is_numeric($limit_part) || count($sanitized_limit) > 2)) {
1312
-                    throw new EE_Error(
1313
-                        sprintf(
1314
-                            __(
1315
-                            // @codingStandardsIgnoreStart
1316
-                                '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.',
1317
-                                // @codingStandardsIgnoreEnd
1318
-                                'event_espresso'
1319
-                            ),
1320
-                            wp_json_encode($query_params['limit'])
1321
-                        )
1322
-                    );
1323
-                }
1324
-                $sanitized_limit[] = (int) $limit_part;
1325
-            }
1326
-            $model_query_params['limit'] = implode(',', $sanitized_limit);
1327
-        } else {
1328
-            $model_query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
1329
-        }
1330
-        if (isset($query_params['caps'])) {
1331
-            $model_query_params['caps'] = $this->validateContext($query_params['caps']);
1332
-        } else {
1333
-            $model_query_params['caps'] = EEM_Base::caps_read;
1334
-        }
1335
-        if (isset($query_params['default_where_conditions'])) {
1336
-            $model_query_params['default_where_conditions'] = $this->validateDefaultQueryParams(
1337
-                $query_params['default_where_conditions']
1338
-            );
1339
-        }
1340
-        // if this is a model protected by a password on another model, exclude the password protected
1341
-        // entities by default. But if they passed in a password, try to show them all. If the password is wrong,
1342
-        // though, they'll get an error (see Read::createEntityFromWpdbResult() which calls Read::checkPassword)
1343
-        if (! $model->hasPassword()
1344
-            && $model->restrictedByRelatedModelPassword()
1345
-            && $model_query_params['caps'] === EEM_Base::caps_read) {
1346
-            if (empty($query_params['password'])) {
1347
-                $model_query_params['exclude_protected'] = true;
1348
-            }
1349
-        }
1350
-
1351
-        return apply_filters('FHEE__Read__create_model_query_params', $model_query_params, $query_params, $model);
1352
-    }
1353
-
1354
-
1355
-    /**
1356
-     * Changes the REST-style query params for use in the models
1357
-     *
1358
-     * @deprecated
1359
-     * @param EEM_Base $model
1360
-     * @param array    $query_params sub-array from @see EEM_Base::get_all()
1361
-     * @return array
1362
-     */
1363
-    public function prepareRestQueryParamsKeyForModels($model, $query_params)
1364
-    {
1365
-        $model_ready_query_params = array();
1366
-        foreach ($query_params as $key => $value) {
1367
-            if (is_array($value)) {
1368
-                $model_ready_query_params[ $key ] = $this->prepareRestQueryParamsKeyForModels($model, $value);
1369
-            } else {
1370
-                $model_ready_query_params[ $key ] = $value;
1371
-            }
1372
-        }
1373
-        return $model_ready_query_params;
1374
-    }
1375
-
1376
-
1377
-    /**
1378
-     * @deprecated instead use ModelDataTranslator::prepareFieldValuesFromJson()
1379
-     * @param $model
1380
-     * @param $query_params
1381
-     * @return array
1382
-     */
1383
-    public function prepareRestQueryParamsValuesForModels($model, $query_params)
1384
-    {
1385
-        $model_ready_query_params = array();
1386
-        foreach ($query_params as $key => $value) {
1387
-            if (is_array($value)) {
1388
-                $model_ready_query_params[ $key ] = $this->prepareRestQueryParamsValuesForModels($model, $value);
1389
-            } else {
1390
-                $model_ready_query_params[ $key ] = $value;
1391
-            }
1392
-        }
1393
-        return $model_ready_query_params;
1394
-    }
1395
-
1396
-
1397
-    /**
1398
-     * Explodes the string on commas, and only returns items with $prefix followed by a period.
1399
-     * If no prefix is specified, returns items with no period.
1400
-     *
1401
-     * @param string|array $string_to_explode eg "jibba,jabba, blah, blah, blah" or array('jibba', 'jabba' )
1402
-     * @param string       $prefix            "Event" or "foobar"
1403
-     * @return array $string_to_exploded exploded on COMMAS, and if a prefix was specified
1404
-     *                                        we only return strings starting with that and a period; if no prefix was
1405
-     *                                        specified we return all items containing NO periods
1406
-     */
1407
-    public function explodeAndGetItemsPrefixedWith($string_to_explode, $prefix)
1408
-    {
1409
-        if (is_string($string_to_explode)) {
1410
-            $exploded_contents = explode(',', $string_to_explode);
1411
-        } elseif (is_array($string_to_explode)) {
1412
-            $exploded_contents = $string_to_explode;
1413
-        } else {
1414
-            $exploded_contents = array();
1415
-        }
1416
-        // if the string was empty, we want an empty array
1417
-        $exploded_contents = array_filter($exploded_contents);
1418
-        $contents_with_prefix = array();
1419
-        foreach ($exploded_contents as $item) {
1420
-            $item = trim($item);
1421
-            // if no prefix was provided, so we look for items with no "." in them
1422
-            if (! $prefix) {
1423
-                // does this item have a period?
1424
-                if (strpos($item, '.') === false) {
1425
-                    // if not, then its what we're looking for
1426
-                    $contents_with_prefix[] = $item;
1427
-                }
1428
-            } elseif (strpos($item, $prefix . '.') === 0) {
1429
-                // this item has the prefix and a period, grab it
1430
-                $contents_with_prefix[] = substr(
1431
-                    $item,
1432
-                    strpos($item, $prefix . '.') + strlen($prefix . '.')
1433
-                );
1434
-            } elseif ($item === $prefix) {
1435
-                // this item is JUST the prefix
1436
-                // so let's grab everything after, which is a blank string
1437
-                $contents_with_prefix[] = '';
1438
-            }
1439
-        }
1440
-        return $contents_with_prefix;
1441
-    }
1442
-
1443
-
1444
-    /**
1445
-     * @deprecated since 4.8.36.rc.001 You should instead use Read::explode_and_get_items_prefixed_with.
1446
-     * Deprecated because its return values were really quite confusing- sometimes it returned
1447
-     * an empty array (when the include string was blank or '*') or sometimes it returned
1448
-     * array('*') (when you provided a model and a model of that kind was found).
1449
-     * Parses the $include_string so we fetch all the field names relating to THIS model
1450
-     * (ie have NO period in them), or for the provided model (ie start with the model
1451
-     * name and then a period).
1452
-     * @param string $include_string @see Read:handle_request_get_all
1453
-     * @param string $model_name
1454
-     * @return array of fields for this model. If $model_name is provided, then
1455
-     *                               the fields for that model, with the model's name removed from each.
1456
-     *                               If $include_string was blank or '*' returns an empty array
1457
-     */
1458
-    public function extractIncludesForThisModel($include_string, $model_name = null)
1459
-    {
1460
-        if (is_array($include_string)) {
1461
-            $include_string = implode(',', $include_string);
1462
-        }
1463
-        if ($include_string === '*' || $include_string === '') {
1464
-            return array();
1465
-        }
1466
-        $includes = explode(',', $include_string);
1467
-        $extracted_fields_to_include = array();
1468
-        if ($model_name) {
1469
-            foreach ($includes as $field_to_include) {
1470
-                $field_to_include = trim($field_to_include);
1471
-                if (strpos($field_to_include, $model_name . '.') === 0) {
1472
-                    // found the model name at the exact start
1473
-                    $field_sans_model_name = str_replace($model_name . '.', '', $field_to_include);
1474
-                    $extracted_fields_to_include[] = $field_sans_model_name;
1475
-                } elseif ($field_to_include == $model_name) {
1476
-                    $extracted_fields_to_include[] = '*';
1477
-                }
1478
-            }
1479
-        } else {
1480
-            // look for ones with no period
1481
-            foreach ($includes as $field_to_include) {
1482
-                $field_to_include = trim($field_to_include);
1483
-                if (strpos($field_to_include, '.') === false
1484
-                    && ! $this->getModelVersionInfo()->isModelNameInThisVersion($field_to_include)
1485
-                ) {
1486
-                    $extracted_fields_to_include[] = $field_to_include;
1487
-                }
1488
-            }
1489
-        }
1490
-        return $extracted_fields_to_include;
1491
-    }
1492
-
1493
-
1494
-    /**
1495
-     * Gets the single item using the model according to the request in the context given, otherwise
1496
-     * returns that it's inaccessible to the current user
1497
-     *
1498
-     * @param EEM_Base $model
1499
-     * @param WP_REST_Request $request
1500
-     * @param null $context
1501
-     * @return array
1502
-     * @throws EE_Error
1503
-     */
1504
-    public function getOneOrReportPermissionError(EEM_Base $model, WP_REST_Request $request, $context = null)
1505
-    {
1506
-        $query_params = array(array($model->primary_key_name() => $request->get_param('id')), 'limit' => 1);
1507
-        if ($model instanceof EEM_Soft_Delete_Base) {
1508
-            $query_params = $model->alter_query_params_so_deleted_and_undeleted_items_included($query_params);
1509
-        }
1510
-        $restricted_query_params = $query_params;
1511
-        $restricted_query_params['caps'] = $context;
1512
-        $this->setDebugInfo('model query params', $restricted_query_params);
1513
-        $model_rows = $model->get_all_wpdb_results($restricted_query_params);
1514
-        if (! empty($model_rows)) {
1515
-            return $this->createEntityFromWpdbResult(
1516
-                $model,
1517
-                reset($model_rows),
1518
-                $request
1519
-            );
1520
-        } else {
1521
-            // ok let's test to see if we WOULD have found it, had we not had restrictions from missing capabilities
1522
-            $lowercase_model_name = strtolower($model->get_this_model_name());
1523
-            if ($model->exists($query_params)) {
1524
-                // you got shafted- it existed but we didn't want to tell you!
1525
-                throw new RestException(
1526
-                    'rest_user_cannot_' . $context,
1527
-                    sprintf(
1528
-                        __('Sorry, you cannot %1$s this %2$s. Missing permissions are: %3$s', 'event_espresso'),
1529
-                        $context,
1530
-                        $lowercase_model_name,
1531
-                        Capabilities::getMissingPermissionsString(
1532
-                            $model,
1533
-                            $context
1534
-                        )
1535
-                    ),
1536
-                    array('status' => 403)
1537
-                );
1538
-            } else {
1539
-                // it's not you. It just doesn't exist
1540
-                throw new RestException(
1541
-                    sprintf('rest_%s_invalid_id', $lowercase_model_name),
1542
-                    sprintf(__('Invalid %s ID.', 'event_espresso'), $lowercase_model_name),
1543
-                    array('status' => 404)
1544
-                );
1545
-            }
1546
-        }
1547
-    }
1548
-
1549
-    /**
1550
-     * Checks that if this content requires a password to be read, that it's been provided and is correct.
1551
-     * @since 4.9.74.p
1552
-     * @param EEM_Base $model
1553
-     * @param $model_row
1554
-     * @param $query_params Adds 'default_where_conditions' => 'minimum' to ensure we don't confuse trashed with
1555
-     *                      password protected.
1556
-     * @param WP_REST_Request $request
1557
-     * @throws EE_Error
1558
-     * @throws InvalidArgumentException
1559
-     * @throws InvalidDataTypeException
1560
-     * @throws InvalidInterfaceException
1561
-     * @throws RestPasswordRequiredException
1562
-     * @throws RestPasswordIncorrectException
1563
-     * @throws \EventEspresso\core\exceptions\ModelConfigurationException
1564
-     * @throws ReflectionException
1565
-     */
1566
-    protected function checkPassword(EEM_Base $model, $model_row, $query_params, WP_REST_Request $request)
1567
-    {
1568
-        $query_params['default_where_conditions'] = 'minimum';
1569
-        // stuff is only "protected" for front-end requests. Elsewhere, you either get full permission to access the object
1570
-        // or you don't.
1571
-        $request_caps = $request->get_param('caps');
1572
-        if (isset($request_caps) && $request_caps !== EEM_Base::caps_read) {
1573
-            return;
1574
-        }
1575
-        // if this entity requires a password, they better give it and it better be right!
1576
-        if ($model->hasPassword()
1577
-            && $model_row[ $model->getPasswordField()->get_qualified_column() ] !== '') {
1578
-            if (empty($request['password'])) {
1579
-                throw new RestPasswordRequiredException();
1580
-            } elseif (!hash_equals(
1581
-                $model_row[ $model->getPasswordField()->get_qualified_column() ],
1582
-                $request['password']
1583
-            )) {
1584
-                throw new RestPasswordIncorrectException();
1585
-            }
1586
-        } // wait! maybe this content is password protected
1587
-        elseif ($model->restrictedByRelatedModelPassword()
1588
-            && $request->get_param('caps') === EEM_Base::caps_read) {
1589
-            $password_supplied = $request->get_param('password');
1590
-            if (empty($password_supplied)) {
1591
-                $query_params['exclude_protected'] = true;
1592
-                if (!$model->exists($query_params)) {
1593
-                    throw new RestPasswordRequiredException();
1594
-                }
1595
-            } else {
1596
-                $query_params[0][ $model->modelChainAndPassword() ] = $password_supplied;
1597
-                if (!$model->exists($query_params)) {
1598
-                    throw new RestPasswordIncorrectException();
1599
-                }
1600
-            }
1601
-        }
1602
-    }
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
+	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
+					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
+			} 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
+	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());
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);
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(),
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(),
479
+					$result,
480
+					$request
481
+				);
482
+				$joined_result = array_merge($join_model_result, $nice_result);
483
+				// $joined_result = array_merge($nice_result, $join_model_result);
484
+				// but keep the meta stuff from the main model
485
+				if (isset($nice_result['meta'])) {
486
+					$joined_result['meta'] = $nice_result['meta'];
487
+				}
488
+				$nice_result = $joined_result;
489
+			}
490
+			$nice_results[] = $nice_result;
491
+		}
492
+		if ($relation instanceof EE_Belongs_To_Relation) {
493
+			return array_shift($nice_results);
494
+		} else {
495
+			return $nice_results;
496
+		}
497
+	}
498
+
499
+
500
+	/**
501
+	 * Gets the collection for given relation object
502
+	 * The same as Read::get_entities_from_model(), except if the relation
503
+	 * is a HABTM relation, in which case it merges any non-foreign-key fields from
504
+	 * the join-model-object into the results
505
+	 *
506
+	 * @param string                  $id the ID of the thing we are fetching related stuff from
507
+	 * @param \EE_Model_Relation_Base $relation
508
+	 * @param WP_REST_Request         $request
509
+	 * @return array
510
+	 * @throws EE_Error
511
+	 */
512
+	public function getEntitiesFromRelation($id, $relation, $request)
513
+	{
514
+		if (! $relation->get_this_model()->has_primary_key_field()) {
515
+			throw new EE_Error(
516
+				sprintf(
517
+					__(
518
+					// @codingStandardsIgnoreStart
519
+						'Read::get_entities_from_relation should only be called from a model with a primary key, it was called from %1$s',
520
+						// @codingStandardsIgnoreEnd
521
+						'event_espresso'
522
+					),
523
+					$relation->get_this_model()->get_this_model_name()
524
+				)
525
+			);
526
+		}
527
+		// can we edit that main item?
528
+		// if not, show nothing but an error
529
+		// otherwise, please proceed
530
+		return $this->getEntitiesFromRelationUsingModelQueryParams(
531
+			array(
532
+				array(
533
+					$relation->get_this_model()->primary_key_name() => $id,
534
+				),
535
+			),
536
+			$relation,
537
+			$request
538
+		);
539
+	}
540
+
541
+
542
+	/**
543
+	 * Sets the headers that are based on the model and query params,
544
+	 * like the total records. This should only be called on the original request
545
+	 * from the client, not on subsequent internal
546
+	 *
547
+	 * @param EEM_Base $model
548
+	 * @param array    $query_params
549
+	 * @return void
550
+	 */
551
+	protected function setHeadersFromQueryParams($model, $query_params)
552
+	{
553
+		$this->setDebugInfo('model query params', $query_params);
554
+		$this->setDebugInfo(
555
+			'missing caps',
556
+			Capabilities::getMissingPermissionsString($model, $query_params['caps'])
557
+		);
558
+		// normally the limit to a 2-part array, where the 2nd item is the limit
559
+		if (! isset($query_params['limit'])) {
560
+			$query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
561
+		}
562
+		if (is_array($query_params['limit'])) {
563
+			$limit_parts = $query_params['limit'];
564
+		} else {
565
+			$limit_parts = explode(',', $query_params['limit']);
566
+			if (count($limit_parts) == 1) {
567
+				$limit_parts = array(0, $limit_parts[0]);
568
+			}
569
+		}
570
+		// remove the group by and having parts of the query, as those will
571
+		// make the sql query return an array of values, instead of just a single value
572
+		unset($query_params['group_by'], $query_params['having'], $query_params['limit']);
573
+		$count = $model->count($query_params, null, true);
574
+		$pages = $count / $limit_parts[1];
575
+		$this->setResponseHeader('Total', $count, false);
576
+		$this->setResponseHeader('PageSize', $limit_parts[1], false);
577
+		$this->setResponseHeader('TotalPages', ceil($pages), false);
578
+	}
579
+
580
+
581
+	/**
582
+	 * Changes database results into REST API entities
583
+	 *
584
+	 * @param EEM_Base $model
585
+	 * @param array $db_row like results from $wpdb->get_results()
586
+	 * @param WP_REST_Request $rest_request
587
+	 * @param string $deprecated no longer used
588
+	 * @return array ready for being converted into json for sending to client
589
+	 * @throws EE_Error
590
+	 * @throws RestException
591
+	 * @throws InvalidDataTypeException
592
+	 * @throws InvalidInterfaceException
593
+	 * @throws InvalidArgumentException
594
+	 * @throws ReflectionException
595
+	 */
596
+	public function createEntityFromWpdbResult($model, $db_row, $rest_request, $deprecated = null)
597
+	{
598
+		if (! $rest_request instanceof WP_REST_Request) {
599
+			// ok so this was called in the old style, where the 3rd arg was
600
+			// $include, and the 4th arg was $context
601
+			// now setup the request just to avoid fatal errors, although we won't be able
602
+			// to truly make use of it because it's kinda devoid of info
603
+			$rest_request = new WP_REST_Request();
604
+			$rest_request->set_param('include', $rest_request);
605
+			$rest_request->set_param('caps', $deprecated);
606
+		}
607
+		if ($rest_request->get_param('caps') == null) {
608
+			$rest_request->set_param('caps', EEM_Base::caps_read);
609
+		}
610
+		$current_user_full_access_to_entity = $model->currentUserCan(
611
+			EEM_Base::caps_read_admin,
612
+			$model->deduce_fields_n_values_from_cols_n_values($db_row)
613
+		);
614
+		$entity_array = $this->createBareEntityFromWpdbResults($model, $db_row);
615
+		$entity_array = $this->addExtraFields($model, $db_row, $entity_array);
616
+		$entity_array['_links'] = $this->getEntityLinks($model, $db_row, $entity_array);
617
+		// when it's a regular read request for a model with a password and the password wasn't provided
618
+		// remove the password protected fields
619
+		$has_protected_fields = false;
620
+		try {
621
+			$this->checkPassword(
622
+				$model,
623
+				$db_row,
624
+				$model->alter_query_params_to_restrict_by_ID(
625
+					$model->get_index_primary_key_string(
626
+						$model->deduce_fields_n_values_from_cols_n_values($db_row)
627
+					)
628
+				),
629
+				$rest_request
630
+			);
631
+		} catch (RestPasswordRequiredException $e) {
632
+			if ($model->hasPassword()) {
633
+				// just remove protected fields
634
+				$has_protected_fields = true;
635
+				$entity_array = Capabilities::filterOutPasswordProtectedFields(
636
+					$entity_array,
637
+					$model,
638
+					$this->getModelVersionInfo()
639
+				);
640
+			} else {
641
+				// that's a problem. None of this should be accessible if no password was provided
642
+				throw $e;
643
+			}
644
+		}
645
+
646
+		$entity_array['_calculated_fields'] = $this->getEntityCalculations($model, $db_row, $rest_request, $has_protected_fields);
647
+		$entity_array = apply_filters(
648
+			'FHEE__Read__create_entity_from_wpdb_results__entity_before_including_requested_models',
649
+			$entity_array,
650
+			$model,
651
+			$rest_request->get_param('caps'),
652
+			$rest_request,
653
+			$this
654
+		);
655
+		// add an empty protected property for now. If it's still around after we remove everything the request didn't
656
+		// want, we'll populate it then. k?
657
+		$entity_array['_protected'] = array();
658
+		// remove any properties the request didn't want. This way _protected won't bother mentioning them
659
+		$entity_array = $this->includeOnlyRequestedProperties($model, $rest_request, $entity_array);
660
+		$entity_array = $this->includeRequestedModels($model, $rest_request, $entity_array, $db_row, $has_protected_fields);
661
+		// if they still wanted the _protected property, add it.
662
+		if (isset($entity_array['_protected'])) {
663
+			$entity_array = $this->addProtectedProperty($model, $entity_array, $has_protected_fields);
664
+		}
665
+		$entity_array = apply_filters(
666
+			'FHEE__Read__create_entity_from_wpdb_results__entity_before_inaccessible_field_removal',
667
+			$entity_array,
668
+			$model,
669
+			$rest_request->get_param('caps'),
670
+			$rest_request,
671
+			$this
672
+		);
673
+		if (! $current_user_full_access_to_entity) {
674
+			$result_without_inaccessible_fields = Capabilities::filterOutInaccessibleEntityFields(
675
+				$entity_array,
676
+				$model,
677
+				$rest_request->get_param('caps'),
678
+				$this->getModelVersionInfo()
679
+			);
680
+		} else {
681
+			$result_without_inaccessible_fields = $entity_array;
682
+		}
683
+		$this->setDebugInfo(
684
+			'inaccessible fields',
685
+			array_keys(array_diff_key((array) $entity_array, (array) $result_without_inaccessible_fields))
686
+		);
687
+		return apply_filters(
688
+			'FHEE__Read__create_entity_from_wpdb_results__entity_return',
689
+			$result_without_inaccessible_fields,
690
+			$model,
691
+			$rest_request->get_param('caps')
692
+		);
693
+	}
694
+
695
+	/**
696
+	 * Returns an array describing which fields can be protected, and which actually were removed this request
697
+	 * @since 4.9.74.p
698
+	 * @param $model
699
+	 * @param $results_so_far
700
+	 * @param $protected
701
+	 * @return array results
702
+	 */
703
+	protected function addProtectedProperty(EEM_Base $model, $results_so_far, $protected)
704
+	{
705
+		if (! $model->hasPassword() || ! $protected) {
706
+			return $results_so_far;
707
+		}
708
+		$password_field = $model->getPasswordField();
709
+		$all_protected = array_merge(
710
+			array($password_field->get_name()),
711
+			$password_field->protectedFields()
712
+		);
713
+		$fields_included = array_keys($results_so_far);
714
+		$fields_included = array_intersect(
715
+			$all_protected,
716
+			$fields_included
717
+		);
718
+		foreach ($fields_included as $field_name) {
719
+			$results_so_far['_protected'][] = $field_name ;
720
+		}
721
+		return $results_so_far;
722
+	}
723
+
724
+	/**
725
+	 * Creates a REST entity array (JSON object we're going to return in the response, but
726
+	 * for now still a PHP array, but soon enough we'll call json_encode on it, don't worry),
727
+	 * from $wpdb->get_row( $sql, ARRAY_A)
728
+	 *
729
+	 * @param EEM_Base $model
730
+	 * @param array    $db_row
731
+	 * @return array entity mostly ready for converting to JSON and sending in the response
732
+	 */
733
+	protected function createBareEntityFromWpdbResults(EEM_Base $model, $db_row)
734
+	{
735
+		$result = $model->deduce_fields_n_values_from_cols_n_values($db_row);
736
+		$result = array_intersect_key(
737
+			$result,
738
+			$this->getModelVersionInfo()->fieldsOnModelInThisVersion($model)
739
+		);
740
+		// if this is a CPT, we need to set the global $post to it,
741
+		// otherwise shortcodes etc won't work properly while rendering it
742
+		if ($model instanceof \EEM_CPT_Base) {
743
+			$do_chevy_shuffle = true;
744
+		} else {
745
+			$do_chevy_shuffle = false;
746
+		}
747
+		if ($do_chevy_shuffle) {
748
+			global $post;
749
+			$old_post = $post;
750
+			$post = get_post($result[ $model->primary_key_name() ]);
751
+			if (! $post instanceof \WP_Post) {
752
+				// well that's weird, because $result is what we JUST fetched from the database
753
+				throw new RestException(
754
+					'error_fetching_post_from_database_results',
755
+					esc_html__(
756
+						'An item was retrieved from the database but it\'s not a WP_Post like it should be.',
757
+						'event_espresso'
758
+					)
759
+				);
760
+			}
761
+			$model_object_classname = 'EE_' . $model->get_this_model_name();
762
+			$post->{$model_object_classname} = \EE_Registry::instance()->load_class(
763
+				$model_object_classname,
764
+				$result,
765
+				false,
766
+				false
767
+			);
768
+		}
769
+		foreach ($result as $field_name => $field_value) {
770
+			$field_obj = $model->field_settings_for($field_name);
771
+			if ($this->isSubclassOfOne($field_obj, $this->getModelVersionInfo()->fieldsIgnored())) {
772
+				unset($result[ $field_name ]);
773
+			} elseif ($this->isSubclassOfOne(
774
+				$field_obj,
775
+				$this->getModelVersionInfo()->fieldsThatHaveRenderedFormat()
776
+			)
777
+			) {
778
+				$result[ $field_name ] = array(
779
+					'raw'      => $this->prepareFieldObjValueForJson($field_obj, $field_value),
780
+					'rendered' => $this->prepareFieldObjValueForJson($field_obj, $field_value, 'pretty'),
781
+				);
782
+			} elseif ($this->isSubclassOfOne(
783
+				$field_obj,
784
+				$this->getModelVersionInfo()->fieldsThatHavePrettyFormat()
785
+			)
786
+			) {
787
+				$result[ $field_name ] = array(
788
+					'raw'    => $this->prepareFieldObjValueForJson($field_obj, $field_value),
789
+					'pretty' => $this->prepareFieldObjValueForJson($field_obj, $field_value, 'pretty'),
790
+				);
791
+			} elseif ($field_obj instanceof \EE_Datetime_Field) {
792
+				$field_value = $field_obj->prepare_for_set_from_db($field_value);
793
+				// if the value is null, but we're not supposed to permit null, then set to the field's default
794
+				if (is_null($field_value)) {
795
+					$field_value = $field_obj->getDefaultDateTimeObj();
796
+				}
797
+				if (is_null($field_value)) {
798
+					$gmt_date = $local_date = ModelDataTranslator::prepareFieldValuesForJson(
799
+						$field_obj,
800
+						$field_value,
801
+						$this->getModelVersionInfo()->requestedVersion()
802
+					);
803
+				} else {
804
+					$timezone = $field_value->getTimezone();
805
+					EEH_DTT_Helper::setTimezone($field_value, new DateTimeZone('UTC'));
806
+					$gmt_date = ModelDataTranslator::prepareFieldValuesForJson(
807
+						$field_obj,
808
+						$field_value,
809
+						$this->getModelVersionInfo()->requestedVersion()
810
+					);
811
+					EEH_DTT_Helper::setTimezone($field_value, $timezone);
812
+					$local_date = ModelDataTranslator::prepareFieldValuesForJson(
813
+						$field_obj,
814
+						$field_value,
815
+						$this->getModelVersionInfo()->requestedVersion()
816
+					);
817
+				}
818
+				$result[ $field_name . '_gmt' ] = $gmt_date;
819
+				$result[ $field_name ] = $local_date;
820
+			} else {
821
+				$result[ $field_name ] = $this->prepareFieldObjValueForJson($field_obj, $field_value);
822
+			}
823
+		}
824
+		if ($do_chevy_shuffle) {
825
+			$post = $old_post;
826
+		}
827
+		return $result;
828
+	}
829
+
830
+
831
+	/**
832
+	 * Takes a value all the way from the DB representation, to the model object's representation, to the
833
+	 * user-facing PHP representation, to the REST API representation. (Assumes you've already taken from the DB
834
+	 * representation using $field_obj->prepare_for_set_from_db())
835
+	 *
836
+	 * @param EE_Model_Field_Base $field_obj
837
+	 * @param mixed               $value  as it's stored on a model object
838
+	 * @param string              $format valid values are 'normal' (default), 'pretty', 'datetime_obj'
839
+	 * @return mixed
840
+	 * @throws ObjectDetectedException if $value contains a PHP object
841
+	 */
842
+	protected function prepareFieldObjValueForJson(EE_Model_Field_Base $field_obj, $value, $format = 'normal')
843
+	{
844
+		$value = $field_obj->prepare_for_set_from_db($value);
845
+		switch ($format) {
846
+			case 'pretty':
847
+				$value = $field_obj->prepare_for_pretty_echoing($value);
848
+				break;
849
+			case 'normal':
850
+			default:
851
+				$value = $field_obj->prepare_for_get($value);
852
+				break;
853
+		}
854
+		return ModelDataTranslator::prepareFieldValuesForJson(
855
+			$field_obj,
856
+			$value,
857
+			$this->getModelVersionInfo()->requestedVersion()
858
+		);
859
+	}
860
+
861
+
862
+	/**
863
+	 * Adds a few extra fields to the entity response
864
+	 *
865
+	 * @param EEM_Base $model
866
+	 * @param array    $db_row
867
+	 * @param array    $entity_array
868
+	 * @return array modified entity
869
+	 */
870
+	protected function addExtraFields(EEM_Base $model, $db_row, $entity_array)
871
+	{
872
+		if ($model instanceof EEM_CPT_Base) {
873
+			$entity_array['link'] = get_permalink($db_row[ $model->get_primary_key_field()->get_qualified_column() ]);
874
+		}
875
+		return $entity_array;
876
+	}
877
+
878
+
879
+	/**
880
+	 * Gets links we want to add to the response
881
+	 *
882
+	 * @global \WP_REST_Server $wp_rest_server
883
+	 * @param EEM_Base         $model
884
+	 * @param array            $db_row
885
+	 * @param array            $entity_array
886
+	 * @return array the _links item in the entity
887
+	 */
888
+	protected function getEntityLinks($model, $db_row, $entity_array)
889
+	{
890
+		// add basic links
891
+		$links = array();
892
+		if ($model->has_primary_key_field()) {
893
+			$links['self'] = array(
894
+				array(
895
+					'href' => $this->getVersionedLinkTo(
896
+						EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
897
+						. '/'
898
+						. $entity_array[ $model->primary_key_name() ]
899
+					),
900
+				),
901
+			);
902
+		}
903
+		$links['collection'] = array(
904
+			array(
905
+				'href' => $this->getVersionedLinkTo(
906
+					EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
907
+				),
908
+			),
909
+		);
910
+		// add links to related models
911
+		if ($model->has_primary_key_field()) {
912
+			foreach ($this->getModelVersionInfo()->relationSettings($model) as $relation_name => $relation_obj) {
913
+				$related_model_part = Read::getRelatedEntityName($relation_name, $relation_obj);
914
+				$links[ EED_Core_Rest_Api::ee_api_link_namespace . $related_model_part ] = array(
915
+					array(
916
+						'href'   => $this->getVersionedLinkTo(
917
+							EEH_Inflector::pluralize_and_lower($model->get_this_model_name())
918
+							. '/'
919
+							. $entity_array[ $model->primary_key_name() ]
920
+							. '/'
921
+							. $related_model_part
922
+						),
923
+						'single' => $relation_obj instanceof EE_Belongs_To_Relation ? true : false,
924
+					),
925
+				);
926
+			}
927
+		}
928
+		return $links;
929
+	}
930
+
931
+
932
+	/**
933
+	 * Adds the included models indicated in the request to the entity provided
934
+	 *
935
+	 * @param EEM_Base $model
936
+	 * @param WP_REST_Request $rest_request
937
+	 * @param array $entity_array
938
+	 * @param array $db_row
939
+	 * @param boolean $included_items_protected if the original item is password protected, don't include any related models.
940
+	 * @return array the modified entity
941
+	 * @throws RestException
942
+	 */
943
+	protected function includeRequestedModels(
944
+		EEM_Base $model,
945
+		WP_REST_Request $rest_request,
946
+		$entity_array,
947
+		$db_row = array(),
948
+		$included_items_protected = false
949
+	) {
950
+		// if $db_row not included, hope the entity array has what we need
951
+		if (! $db_row) {
952
+			$db_row = $entity_array;
953
+		}
954
+		$relation_settings = $this->getModelVersionInfo()->relationSettings($model);
955
+		foreach ($relation_settings as $relation_name => $relation_obj) {
956
+			$related_fields_to_include = $this->explodeAndGetItemsPrefixedWith(
957
+				$rest_request->get_param('include'),
958
+				$relation_name
959
+			);
960
+			$related_fields_to_calculate = $this->explodeAndGetItemsPrefixedWith(
961
+				$rest_request->get_param('calculate'),
962
+				$relation_name
963
+			);
964
+			// did they specify they wanted to include a related model, or
965
+			// specific fields from a related model?
966
+			// or did they specify to calculate a field from a related model?
967
+			if ($related_fields_to_include || $related_fields_to_calculate) {
968
+				// if so, we should include at least some part of the related model
969
+				$pretend_related_request = new WP_REST_Request();
970
+				$pretend_related_request->set_query_params(
971
+					array(
972
+						'caps'      => $rest_request->get_param('caps'),
973
+						'include'   => $related_fields_to_include,
974
+						'calculate' => $related_fields_to_calculate,
975
+						'password' => $rest_request->get_param('password')
976
+					)
977
+				);
978
+				$pretend_related_request->add_header('no_rest_headers', true);
979
+				$primary_model_query_params = $model->alter_query_params_to_restrict_by_ID(
980
+					$model->get_index_primary_key_string(
981
+						$model->deduce_fields_n_values_from_cols_n_values($db_row)
982
+					)
983
+				);
984
+				if (! $included_items_protected) {
985
+					try {
986
+						$related_results = $this->getEntitiesFromRelationUsingModelQueryParams(
987
+							$primary_model_query_params,
988
+							$relation_obj,
989
+							$pretend_related_request
990
+						);
991
+					} catch (RestException $e) {
992
+						$related_results = null;
993
+					}
994
+				} else {
995
+					// they're protected, hide them.
996
+					$related_results = null;
997
+					$entity_array['_protected'][] = Read::getRelatedEntityName($relation_name, $relation_obj);
998
+				}
999
+				if ($related_results instanceof WP_Error || $related_results === null) {
1000
+					$related_results = $relation_obj instanceof EE_Belongs_To_Relation ? null : array();
1001
+				}
1002
+				$entity_array[ Read::getRelatedEntityName($relation_name, $relation_obj) ] = $related_results;
1003
+			}
1004
+		}
1005
+		return $entity_array;
1006
+	}
1007
+
1008
+	/**
1009
+	 * If the user has requested only specific properties (including meta properties like _links or _protected)
1010
+	 * remove everything else.
1011
+	 * @since 4.9.74.p
1012
+	 * @param EEM_Base $model
1013
+	 * @param WP_REST_Request $rest_request
1014
+	 * @param $entity_array
1015
+	 * @return array
1016
+	 * @throws EE_Error
1017
+	 */
1018
+	protected function includeOnlyRequestedProperties(
1019
+		EEM_Base $model,
1020
+		WP_REST_Request $rest_request,
1021
+		$entity_array
1022
+	) {
1023
+
1024
+		$includes_for_this_model = $this->explodeAndGetItemsPrefixedWith($rest_request->get_param('include'), '');
1025
+		$includes_for_this_model = $this->removeModelNamesFromArray($includes_for_this_model);
1026
+		// if they passed in * or didn't specify any includes, return everything
1027
+		if (! in_array('*', $includes_for_this_model)
1028
+			&& ! empty($includes_for_this_model)
1029
+		) {
1030
+			if ($model->has_primary_key_field()) {
1031
+				// always include the primary key. ya just gotta know that at least
1032
+				$includes_for_this_model[] = $model->primary_key_name();
1033
+			}
1034
+			if ($this->explodeAndGetItemsPrefixedWith($rest_request->get_param('calculate'), '')) {
1035
+				$includes_for_this_model[] = '_calculated_fields';
1036
+			}
1037
+			$entity_array = array_intersect_key($entity_array, array_flip($includes_for_this_model));
1038
+		}
1039
+		return $entity_array;
1040
+	}
1041
+
1042
+
1043
+	/**
1044
+	 * Returns a new array with all the names of models removed. Eg
1045
+	 * array( 'Event', 'Datetime.*', 'foobar' ) would become array( 'Datetime.*', 'foobar' )
1046
+	 *
1047
+	 * @param array $arr
1048
+	 * @return array
1049
+	 */
1050
+	private function removeModelNamesFromArray($arr)
1051
+	{
1052
+		return array_diff($arr, array_keys(EE_Registry::instance()->non_abstract_db_models));
1053
+	}
1054
+
1055
+
1056
+	/**
1057
+	 * Gets the calculated fields for the response
1058
+	 *
1059
+	 * @param EEM_Base        $model
1060
+	 * @param array           $wpdb_row
1061
+	 * @param WP_REST_Request $rest_request
1062
+	 * @param boolean $row_is_protected whether this row is password protected or not
1063
+	 * @return \stdClass the _calculations item in the entity
1064
+	 * @throws ObjectDetectedException if a default value has a PHP object, which should never do (and if we
1065
+	 * did, let's know about it ASAP, so let the exception bubble up)
1066
+	 */
1067
+	protected function getEntityCalculations($model, $wpdb_row, $rest_request, $row_is_protected = false)
1068
+	{
1069
+		$calculated_fields = $this->explodeAndGetItemsPrefixedWith(
1070
+			$rest_request->get_param('calculate'),
1071
+			''
1072
+		);
1073
+		// note: setting calculate=* doesn't do anything
1074
+		$calculated_fields_to_return = new \stdClass();
1075
+		$protected_fields = array();
1076
+		foreach ($calculated_fields as $field_to_calculate) {
1077
+			try {
1078
+				// it's password protected, so they shouldn't be able to read this. Remove the value
1079
+				$schema = $this->fields_calculator->getJsonSchemaForModel($model);
1080
+				if ($row_is_protected
1081
+					&& isset($schema['properties'][ $field_to_calculate ]['protected'])
1082
+					&& $schema['properties'][ $field_to_calculate ]['protected']) {
1083
+					$calculated_value = null;
1084
+					$protected_fields[] = $field_to_calculate;
1085
+					if ($schema['properties'][ $field_to_calculate ]['type']) {
1086
+						switch ($schema['properties'][ $field_to_calculate ]['type']) {
1087
+							case 'boolean':
1088
+								$calculated_value = false;
1089
+								break;
1090
+							case 'integer':
1091
+								$calculated_value = 0;
1092
+								break;
1093
+							case 'string':
1094
+								$calculated_value = '';
1095
+								break;
1096
+							case 'array':
1097
+								$calculated_value = array();
1098
+								break;
1099
+							case 'object':
1100
+								$calculated_value = new stdClass();
1101
+								break;
1102
+						}
1103
+					}
1104
+				} else {
1105
+					$calculated_value = ModelDataTranslator::prepareFieldValueForJson(
1106
+						null,
1107
+						$this->fields_calculator->retrieveCalculatedFieldValue(
1108
+							$model,
1109
+							$field_to_calculate,
1110
+							$wpdb_row,
1111
+							$rest_request,
1112
+							$this
1113
+						),
1114
+						$this->getModelVersionInfo()->requestedVersion()
1115
+					);
1116
+				}
1117
+				$calculated_fields_to_return->{$field_to_calculate} = $calculated_value;
1118
+			} catch (RestException $e) {
1119
+				// if we don't have permission to read it, just leave it out. but let devs know about the problem
1120
+				$this->setResponseHeader(
1121
+					'Notices-Field-Calculation-Errors['
1122
+					. $e->getStringCode()
1123
+					. ']['
1124
+					. $model->get_this_model_name()
1125
+					. ']['
1126
+					. $field_to_calculate
1127
+					. ']',
1128
+					$e->getMessage(),
1129
+					true
1130
+				);
1131
+			}
1132
+		}
1133
+		$calculated_fields_to_return->_protected = $protected_fields;
1134
+		return $calculated_fields_to_return;
1135
+	}
1136
+
1137
+
1138
+	/**
1139
+	 * Gets the full URL to the resource, taking the requested version into account
1140
+	 *
1141
+	 * @param string $link_part_after_version_and_slash eg "events/10/datetimes"
1142
+	 * @return string url eg "http://mysite.com/wp-json/ee/v4.6/events/10/datetimes"
1143
+	 */
1144
+	public function getVersionedLinkTo($link_part_after_version_and_slash)
1145
+	{
1146
+		return rest_url(
1147
+			EED_Core_Rest_Api::get_versioned_route_to(
1148
+				$link_part_after_version_and_slash,
1149
+				$this->getModelVersionInfo()->requestedVersion()
1150
+			)
1151
+		);
1152
+	}
1153
+
1154
+
1155
+	/**
1156
+	 * Gets the correct lowercase name for the relation in the API according
1157
+	 * to the relation's type
1158
+	 *
1159
+	 * @param string                  $relation_name
1160
+	 * @param \EE_Model_Relation_Base $relation_obj
1161
+	 * @return string
1162
+	 */
1163
+	public static function getRelatedEntityName($relation_name, $relation_obj)
1164
+	{
1165
+		if ($relation_obj instanceof EE_Belongs_To_Relation) {
1166
+			return strtolower($relation_name);
1167
+		} else {
1168
+			return EEH_Inflector::pluralize_and_lower($relation_name);
1169
+		}
1170
+	}
1171
+
1172
+
1173
+	/**
1174
+	 * Gets the one model object with the specified id for the specified model
1175
+	 *
1176
+	 * @param EEM_Base        $model
1177
+	 * @param WP_REST_Request $request
1178
+	 * @return array
1179
+	 */
1180
+	public function getEntityFromModel($model, $request)
1181
+	{
1182
+		$context = $this->validateContext($request->get_param('caps'));
1183
+		return $this->getOneOrReportPermissionError($model, $request, $context);
1184
+	}
1185
+
1186
+
1187
+	/**
1188
+	 * If a context is provided which isn't valid, maybe it was added in a future
1189
+	 * version so just treat it as a default read
1190
+	 *
1191
+	 * @param string $context
1192
+	 * @return string array key of EEM_Base::cap_contexts_to_cap_action_map()
1193
+	 */
1194
+	public function validateContext($context)
1195
+	{
1196
+		if (! $context) {
1197
+			$context = EEM_Base::caps_read;
1198
+		}
1199
+		$valid_contexts = EEM_Base::valid_cap_contexts();
1200
+		if (in_array($context, $valid_contexts)) {
1201
+			return $context;
1202
+		} else {
1203
+			return EEM_Base::caps_read;
1204
+		}
1205
+	}
1206
+
1207
+
1208
+	/**
1209
+	 * Verifies the passed in value is an allowable default where conditions value.
1210
+	 *
1211
+	 * @param $default_query_params
1212
+	 * @return string
1213
+	 */
1214
+	public function validateDefaultQueryParams($default_query_params)
1215
+	{
1216
+		$valid_default_where_conditions_for_api_calls = array(
1217
+			EEM_Base::default_where_conditions_all,
1218
+			EEM_Base::default_where_conditions_minimum_all,
1219
+			EEM_Base::default_where_conditions_minimum_others,
1220
+		);
1221
+		if (! $default_query_params) {
1222
+			$default_query_params = EEM_Base::default_where_conditions_all;
1223
+		}
1224
+		if (in_array(
1225
+			$default_query_params,
1226
+			$valid_default_where_conditions_for_api_calls,
1227
+			true
1228
+		)) {
1229
+			return $default_query_params;
1230
+		} else {
1231
+			return EEM_Base::default_where_conditions_all;
1232
+		}
1233
+	}
1234
+
1235
+
1236
+	/**
1237
+	 * 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.
1238
+	 * Note: right now the query parameter keys for fields (and related fields)
1239
+	 * can be left as-is, but it's quite possible this will change someday.
1240
+	 * Also, this method's contents might be candidate for moving to Model_Data_Translator
1241
+	 *
1242
+	 * @param EEM_Base $model
1243
+	 * @param array    $query_parameters  from $_GET parameter @see Read:handle_request_get_all
1244
+	 * @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)
1245
+	 *                                    or FALSE to indicate that absolutely no results should be returned
1246
+	 * @throws EE_Error
1247
+	 * @throws RestException
1248
+	 */
1249
+	public function createModelQueryParams($model, $query_params)
1250
+	{
1251
+		$model_query_params = array();
1252
+		if (isset($query_params['where'])) {
1253
+			$model_query_params[0] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1254
+				$query_params['where'],
1255
+				$model,
1256
+				$this->getModelVersionInfo()->requestedVersion()
1257
+			);
1258
+		}
1259
+		if (isset($query_params['order_by'])) {
1260
+			$order_by = $query_params['order_by'];
1261
+		} elseif (isset($query_params['orderby'])) {
1262
+			$order_by = $query_params['orderby'];
1263
+		} else {
1264
+			$order_by = null;
1265
+		}
1266
+		if ($order_by !== null) {
1267
+			if (is_array($order_by)) {
1268
+				$order_by = ModelDataTranslator::prepareFieldNamesInArrayKeysFromJson($order_by);
1269
+			} else {
1270
+				// it's a single item
1271
+				$order_by = ModelDataTranslator::prepareFieldNameFromJson($order_by);
1272
+			}
1273
+			$model_query_params['order_by'] = $order_by;
1274
+		}
1275
+		if (isset($query_params['group_by'])) {
1276
+			$group_by = $query_params['group_by'];
1277
+		} elseif (isset($query_params['groupby'])) {
1278
+			$group_by = $query_params['groupby'];
1279
+		} else {
1280
+			$group_by = array_keys($model->get_combined_primary_key_fields());
1281
+		}
1282
+		// make sure they're all real names
1283
+		if (is_array($group_by)) {
1284
+			$group_by = ModelDataTranslator::prepareFieldNamesFromJson($group_by);
1285
+		}
1286
+		if ($group_by !== null) {
1287
+			$model_query_params['group_by'] = $group_by;
1288
+		}
1289
+		if (isset($query_params['having'])) {
1290
+			$model_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForModels(
1291
+				$query_params['having'],
1292
+				$model,
1293
+				$this->getModelVersionInfo()->requestedVersion()
1294
+			);
1295
+		}
1296
+		if (isset($query_params['order'])) {
1297
+			$model_query_params['order'] = $query_params['order'];
1298
+		}
1299
+		if (isset($query_params['mine'])) {
1300
+			$model_query_params = $model->alter_query_params_to_only_include_mine($model_query_params);
1301
+		}
1302
+		if (isset($query_params['limit'])) {
1303
+			// limit should be either a string like '23' or '23,43', or an array with two items in it
1304
+			if (! is_array($query_params['limit'])) {
1305
+				$limit_array = explode(',', (string) $query_params['limit']);
1306
+			} else {
1307
+				$limit_array = $query_params['limit'];
1308
+			}
1309
+			$sanitized_limit = array();
1310
+			foreach ($limit_array as $key => $limit_part) {
1311
+				if ($this->debug_mode && (! is_numeric($limit_part) || count($sanitized_limit) > 2)) {
1312
+					throw new EE_Error(
1313
+						sprintf(
1314
+							__(
1315
+							// @codingStandardsIgnoreStart
1316
+								'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.',
1317
+								// @codingStandardsIgnoreEnd
1318
+								'event_espresso'
1319
+							),
1320
+							wp_json_encode($query_params['limit'])
1321
+						)
1322
+					);
1323
+				}
1324
+				$sanitized_limit[] = (int) $limit_part;
1325
+			}
1326
+			$model_query_params['limit'] = implode(',', $sanitized_limit);
1327
+		} else {
1328
+			$model_query_params['limit'] = EED_Core_Rest_Api::get_default_query_limit();
1329
+		}
1330
+		if (isset($query_params['caps'])) {
1331
+			$model_query_params['caps'] = $this->validateContext($query_params['caps']);
1332
+		} else {
1333
+			$model_query_params['caps'] = EEM_Base::caps_read;
1334
+		}
1335
+		if (isset($query_params['default_where_conditions'])) {
1336
+			$model_query_params['default_where_conditions'] = $this->validateDefaultQueryParams(
1337
+				$query_params['default_where_conditions']
1338
+			);
1339
+		}
1340
+		// if this is a model protected by a password on another model, exclude the password protected
1341
+		// entities by default. But if they passed in a password, try to show them all. If the password is wrong,
1342
+		// though, they'll get an error (see Read::createEntityFromWpdbResult() which calls Read::checkPassword)
1343
+		if (! $model->hasPassword()
1344
+			&& $model->restrictedByRelatedModelPassword()
1345
+			&& $model_query_params['caps'] === EEM_Base::caps_read) {
1346
+			if (empty($query_params['password'])) {
1347
+				$model_query_params['exclude_protected'] = true;
1348
+			}
1349
+		}
1350
+
1351
+		return apply_filters('FHEE__Read__create_model_query_params', $model_query_params, $query_params, $model);
1352
+	}
1353
+
1354
+
1355
+	/**
1356
+	 * Changes the REST-style query params for use in the models
1357
+	 *
1358
+	 * @deprecated
1359
+	 * @param EEM_Base $model
1360
+	 * @param array    $query_params sub-array from @see EEM_Base::get_all()
1361
+	 * @return array
1362
+	 */
1363
+	public function prepareRestQueryParamsKeyForModels($model, $query_params)
1364
+	{
1365
+		$model_ready_query_params = array();
1366
+		foreach ($query_params as $key => $value) {
1367
+			if (is_array($value)) {
1368
+				$model_ready_query_params[ $key ] = $this->prepareRestQueryParamsKeyForModels($model, $value);
1369
+			} else {
1370
+				$model_ready_query_params[ $key ] = $value;
1371
+			}
1372
+		}
1373
+		return $model_ready_query_params;
1374
+	}
1375
+
1376
+
1377
+	/**
1378
+	 * @deprecated instead use ModelDataTranslator::prepareFieldValuesFromJson()
1379
+	 * @param $model
1380
+	 * @param $query_params
1381
+	 * @return array
1382
+	 */
1383
+	public function prepareRestQueryParamsValuesForModels($model, $query_params)
1384
+	{
1385
+		$model_ready_query_params = array();
1386
+		foreach ($query_params as $key => $value) {
1387
+			if (is_array($value)) {
1388
+				$model_ready_query_params[ $key ] = $this->prepareRestQueryParamsValuesForModels($model, $value);
1389
+			} else {
1390
+				$model_ready_query_params[ $key ] = $value;
1391
+			}
1392
+		}
1393
+		return $model_ready_query_params;
1394
+	}
1395
+
1396
+
1397
+	/**
1398
+	 * Explodes the string on commas, and only returns items with $prefix followed by a period.
1399
+	 * If no prefix is specified, returns items with no period.
1400
+	 *
1401
+	 * @param string|array $string_to_explode eg "jibba,jabba, blah, blah, blah" or array('jibba', 'jabba' )
1402
+	 * @param string       $prefix            "Event" or "foobar"
1403
+	 * @return array $string_to_exploded exploded on COMMAS, and if a prefix was specified
1404
+	 *                                        we only return strings starting with that and a period; if no prefix was
1405
+	 *                                        specified we return all items containing NO periods
1406
+	 */
1407
+	public function explodeAndGetItemsPrefixedWith($string_to_explode, $prefix)
1408
+	{
1409
+		if (is_string($string_to_explode)) {
1410
+			$exploded_contents = explode(',', $string_to_explode);
1411
+		} elseif (is_array($string_to_explode)) {
1412
+			$exploded_contents = $string_to_explode;
1413
+		} else {
1414
+			$exploded_contents = array();
1415
+		}
1416
+		// if the string was empty, we want an empty array
1417
+		$exploded_contents = array_filter($exploded_contents);
1418
+		$contents_with_prefix = array();
1419
+		foreach ($exploded_contents as $item) {
1420
+			$item = trim($item);
1421
+			// if no prefix was provided, so we look for items with no "." in them
1422
+			if (! $prefix) {
1423
+				// does this item have a period?
1424
+				if (strpos($item, '.') === false) {
1425
+					// if not, then its what we're looking for
1426
+					$contents_with_prefix[] = $item;
1427
+				}
1428
+			} elseif (strpos($item, $prefix . '.') === 0) {
1429
+				// this item has the prefix and a period, grab it
1430
+				$contents_with_prefix[] = substr(
1431
+					$item,
1432
+					strpos($item, $prefix . '.') + strlen($prefix . '.')
1433
+				);
1434
+			} elseif ($item === $prefix) {
1435
+				// this item is JUST the prefix
1436
+				// so let's grab everything after, which is a blank string
1437
+				$contents_with_prefix[] = '';
1438
+			}
1439
+		}
1440
+		return $contents_with_prefix;
1441
+	}
1442
+
1443
+
1444
+	/**
1445
+	 * @deprecated since 4.8.36.rc.001 You should instead use Read::explode_and_get_items_prefixed_with.
1446
+	 * Deprecated because its return values were really quite confusing- sometimes it returned
1447
+	 * an empty array (when the include string was blank or '*') or sometimes it returned
1448
+	 * array('*') (when you provided a model and a model of that kind was found).
1449
+	 * Parses the $include_string so we fetch all the field names relating to THIS model
1450
+	 * (ie have NO period in them), or for the provided model (ie start with the model
1451
+	 * name and then a period).
1452
+	 * @param string $include_string @see Read:handle_request_get_all
1453
+	 * @param string $model_name
1454
+	 * @return array of fields for this model. If $model_name is provided, then
1455
+	 *                               the fields for that model, with the model's name removed from each.
1456
+	 *                               If $include_string was blank or '*' returns an empty array
1457
+	 */
1458
+	public function extractIncludesForThisModel($include_string, $model_name = null)
1459
+	{
1460
+		if (is_array($include_string)) {
1461
+			$include_string = implode(',', $include_string);
1462
+		}
1463
+		if ($include_string === '*' || $include_string === '') {
1464
+			return array();
1465
+		}
1466
+		$includes = explode(',', $include_string);
1467
+		$extracted_fields_to_include = array();
1468
+		if ($model_name) {
1469
+			foreach ($includes as $field_to_include) {
1470
+				$field_to_include = trim($field_to_include);
1471
+				if (strpos($field_to_include, $model_name . '.') === 0) {
1472
+					// found the model name at the exact start
1473
+					$field_sans_model_name = str_replace($model_name . '.', '', $field_to_include);
1474
+					$extracted_fields_to_include[] = $field_sans_model_name;
1475
+				} elseif ($field_to_include == $model_name) {
1476
+					$extracted_fields_to_include[] = '*';
1477
+				}
1478
+			}
1479
+		} else {
1480
+			// look for ones with no period
1481
+			foreach ($includes as $field_to_include) {
1482
+				$field_to_include = trim($field_to_include);
1483
+				if (strpos($field_to_include, '.') === false
1484
+					&& ! $this->getModelVersionInfo()->isModelNameInThisVersion($field_to_include)
1485
+				) {
1486
+					$extracted_fields_to_include[] = $field_to_include;
1487
+				}
1488
+			}
1489
+		}
1490
+		return $extracted_fields_to_include;
1491
+	}
1492
+
1493
+
1494
+	/**
1495
+	 * Gets the single item using the model according to the request in the context given, otherwise
1496
+	 * returns that it's inaccessible to the current user
1497
+	 *
1498
+	 * @param EEM_Base $model
1499
+	 * @param WP_REST_Request $request
1500
+	 * @param null $context
1501
+	 * @return array
1502
+	 * @throws EE_Error
1503
+	 */
1504
+	public function getOneOrReportPermissionError(EEM_Base $model, WP_REST_Request $request, $context = null)
1505
+	{
1506
+		$query_params = array(array($model->primary_key_name() => $request->get_param('id')), 'limit' => 1);
1507
+		if ($model instanceof EEM_Soft_Delete_Base) {
1508
+			$query_params = $model->alter_query_params_so_deleted_and_undeleted_items_included($query_params);
1509
+		}
1510
+		$restricted_query_params = $query_params;
1511
+		$restricted_query_params['caps'] = $context;
1512
+		$this->setDebugInfo('model query params', $restricted_query_params);
1513
+		$model_rows = $model->get_all_wpdb_results($restricted_query_params);
1514
+		if (! empty($model_rows)) {
1515
+			return $this->createEntityFromWpdbResult(
1516
+				$model,
1517
+				reset($model_rows),
1518
+				$request
1519
+			);
1520
+		} else {
1521
+			// ok let's test to see if we WOULD have found it, had we not had restrictions from missing capabilities
1522
+			$lowercase_model_name = strtolower($model->get_this_model_name());
1523
+			if ($model->exists($query_params)) {
1524
+				// you got shafted- it existed but we didn't want to tell you!
1525
+				throw new RestException(
1526
+					'rest_user_cannot_' . $context,
1527
+					sprintf(
1528
+						__('Sorry, you cannot %1$s this %2$s. Missing permissions are: %3$s', 'event_espresso'),
1529
+						$context,
1530
+						$lowercase_model_name,
1531
+						Capabilities::getMissingPermissionsString(
1532
+							$model,
1533
+							$context
1534
+						)
1535
+					),
1536
+					array('status' => 403)
1537
+				);
1538
+			} else {
1539
+				// it's not you. It just doesn't exist
1540
+				throw new RestException(
1541
+					sprintf('rest_%s_invalid_id', $lowercase_model_name),
1542
+					sprintf(__('Invalid %s ID.', 'event_espresso'), $lowercase_model_name),
1543
+					array('status' => 404)
1544
+				);
1545
+			}
1546
+		}
1547
+	}
1548
+
1549
+	/**
1550
+	 * Checks that if this content requires a password to be read, that it's been provided and is correct.
1551
+	 * @since 4.9.74.p
1552
+	 * @param EEM_Base $model
1553
+	 * @param $model_row
1554
+	 * @param $query_params Adds 'default_where_conditions' => 'minimum' to ensure we don't confuse trashed with
1555
+	 *                      password protected.
1556
+	 * @param WP_REST_Request $request
1557
+	 * @throws EE_Error
1558
+	 * @throws InvalidArgumentException
1559
+	 * @throws InvalidDataTypeException
1560
+	 * @throws InvalidInterfaceException
1561
+	 * @throws RestPasswordRequiredException
1562
+	 * @throws RestPasswordIncorrectException
1563
+	 * @throws \EventEspresso\core\exceptions\ModelConfigurationException
1564
+	 * @throws ReflectionException
1565
+	 */
1566
+	protected function checkPassword(EEM_Base $model, $model_row, $query_params, WP_REST_Request $request)
1567
+	{
1568
+		$query_params['default_where_conditions'] = 'minimum';
1569
+		// stuff is only "protected" for front-end requests. Elsewhere, you either get full permission to access the object
1570
+		// or you don't.
1571
+		$request_caps = $request->get_param('caps');
1572
+		if (isset($request_caps) && $request_caps !== EEM_Base::caps_read) {
1573
+			return;
1574
+		}
1575
+		// if this entity requires a password, they better give it and it better be right!
1576
+		if ($model->hasPassword()
1577
+			&& $model_row[ $model->getPasswordField()->get_qualified_column() ] !== '') {
1578
+			if (empty($request['password'])) {
1579
+				throw new RestPasswordRequiredException();
1580
+			} elseif (!hash_equals(
1581
+				$model_row[ $model->getPasswordField()->get_qualified_column() ],
1582
+				$request['password']
1583
+			)) {
1584
+				throw new RestPasswordIncorrectException();
1585
+			}
1586
+		} // wait! maybe this content is password protected
1587
+		elseif ($model->restrictedByRelatedModelPassword()
1588
+			&& $request->get_param('caps') === EEM_Base::caps_read) {
1589
+			$password_supplied = $request->get_param('password');
1590
+			if (empty($password_supplied)) {
1591
+				$query_params['exclude_protected'] = true;
1592
+				if (!$model->exists($query_params)) {
1593
+					throw new RestPasswordRequiredException();
1594
+				}
1595
+			} else {
1596
+				$query_params[0][ $model->modelChainAndPassword() ] = $password_supplied;
1597
+				if (!$model->exists($query_params)) {
1598
+					throw new RestPasswordIncorrectException();
1599
+				}
1600
+			}
1601
+		}
1602
+	}
1603 1603
 }
Please login to merge, or discard this patch.