PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.1
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.1
3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 2.8.17 2.8.18 All 259 releases
wpfunnels / includes / core / rest-api / Controllers / class-templatelibrary-controller.php

class-templatelibrary-controller.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.13.1, at includes/core/rest-api/Controllers/class-templatelibrary-controller.php

696 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Template library controller
4 *
5 * @package WPFunnels\Rest\Controllers
6 */
7
8 namespace WPFunnels\Rest\Controllers;
9
10 use WP_Error;
11 use WP_REST_Request;
12 use WP_REST_Response;
13 use WP_REST_Server;
14 use WPFunnels\Wpfnl_functions;
15 use WPFunnels\lms\helper\Wpfnl_lms_learndash_functions;
16
17 /**
18 * Class TemplateLibraryController
19 * This class represents the controller for the template library in the WP Funnels plugin
20 * It extends the Wpfnl_REST_Controller class
21 *
22 * @package WPFunnels\Rest\Controllers
23 * @since 1.0.0
24 */
25 class TemplateLibraryController extends Wpfnl_REST_Controller {
26
27 /**
28 * The URL for the WP Funnels API.
29 *
30 * This variable holds the URL for the WP Funnels API, which is used to retrieve templates.
31 *
32 * @var string
33 */
34 public static $funnel_api_url = 'https://templates.getwpfunnels.com/wp-json/wp/v2/wpfunnels/';
35
36 /**
37 * The URL for the WP Funnels API.
38 *
39 * This variable holds the URL for the WP Funnels API, which is used to retrieve templates.
40 *
41 * @var string
42 */
43 public static $funnel_categories_api_url = 'https://templates.getwpfunnels.com/wp-json/wp/v2/template_industries/';
44
45 /**
46 * The URL for the WP Funnels API.
47 *
48 * This variable holds the URL for the WP Funnels API, which is used to retrieve templates.
49 *
50 * @var string
51 */
52 public static $funnel_steps_api_url = 'https://templates.getwpfunnels.com/wp-json/wp/v2/wpfunnel_steps/';
53
54 /**
55 * The URL for the WP Funnels API.
56 *
57 * This variable holds the URL for the WP Funnels API, which is used to retrieve templates.
58 *
59 * @var string
60 */
61 public static $all_funnels_api_url = 'https://templates.getwpfunnels.com/wp-json/wpfunnels/v1/get_all_funnels/';
62
63
64 /**
65 * Endpoint namespace.
66 *
67 * @var string
68 */
69 protected $namespace = 'wpfunnels/v1';
70
71 /**
72 * Route base.
73 *
74 * @var string
75 */
76 protected $rest_base = 'templates/';
77
78
79 /**
80 * Get remote funnel api url
81 *
82 * @return string
83 * @since 2.2.8
84 */
85 private static function get_remote_funnel_api_url() {
86 if ( 'oxygen' === Wpfnl_functions::get_builder_type() ) {
87 return 'https://oxygentemplates.getwpfunnels.com/wp-json/wp/v2/wpfunnels/';
88 } elseif ( 'bricks' === Wpfnl_functions::get_builder_type() ) {
89 return 'https://brickstemplates.getwpfunnels.com/wp-json/wp/v2/wpfunnels/';
90 }
91 return self::$funnel_api_url;
92 }
93
94
95 /**
96 * Get remote funnel builders categories url
97 *
98 * @return string
99 *
100 * @since 2.2.8
101 */
102 private static function get_remote_funnel_categories_api_url() {
103 if ( 'oxygen' === Wpfnl_functions::get_builder_type() ) {
104 return 'https://oxygentemplates.getwpfunnels.com/wp-json/wp/v2/template_industries/';
105 } elseif ( 'bricks' === Wpfnl_functions::get_builder_type() ) {
106 return 'https://brickstemplates.getwpfunnels.com/wp-json/wp/v2/template_industries/';
107 }
108 return self::$funnel_categories_api_url;
109 }
110
111
112 /**
113 * Get remote funnel steps categories url
114 *
115 * @return string
116 *
117 * @since 2.2.8
118 */
119 private static function get_remote_funnel_steps_api_url() {
120 if ( 'oxygen' === Wpfnl_functions::get_builder_type() ) {
121 return 'https://oxygentemplates.getwpfunnels.com/wp-json/wp/v2/wpfunnel_steps/';
122 } elseif ( 'bricks' === Wpfnl_functions::get_builder_type() ) {
123 return 'https://brickstemplates.getwpfunnels.com/wp-json/wp/v2/wpfunnel_steps/';
124 }
125 return self::$funnel_steps_api_url;
126 }
127
128
129 /**
130 * Get all templates API url
131 *
132 * @return string
133 * @since 2.2.8
134 */
135 private static function get_all_templates_api_url() {
136 if ( 'oxygen' === Wpfnl_functions::get_builder_type() ) {
137 return 'https://oxygentemplates.getwpfunnels.com/wp-json/wpfunnels/v1/get_all_funnels/';
138 } elseif ( 'bricks' === Wpfnl_functions::get_builder_type() ) {
139 return 'https://brickstemplates.getwpfunnels.com/wp-json/wpfunnels/v1/get_all_funnels/';
140 }
141 return self::$all_funnels_api_url;
142 }
143
144
145 /**
146 * Check user permission
147 *
148 * @param $request
149 *
150 * @return bool|WP_Error
151 */
152 public function update_items_permissions_check( $request ) {
153 $permission = current_user_can( 'wpf_manage_funnels' );
154 if ( ! Wpfnl_functions::wpfnl_rest_check_manager_permissions( 'templates' ) ) {
155 return new WP_Error( 'wpfunnels_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'wpfnl' ), array( 'status' => rest_authorization_required_code() ) );
156 }
157 return true;
158 }
159
160
161 /**
162 * Makes sure the current user has access to READ the settings APIs.
163 *
164 * @param WP_REST_Request $request Full data about the request.
165 *
166 * @return WP_Error|boolean
167 * @since 3.0.0
168 */
169 public function get_items_permissions_check( $request ) {
170 if ( ! Wpfnl_functions::wpfnl_rest_check_manager_permissions( 'templates' ) ) {
171 return new WP_Error( 'wpfunnels_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'wpfnl' ), array( 'status' => rest_authorization_required_code() ) );
172 }
173 return true;
174 }
175
176
177
178 public function register_routes() {
179 register_rest_route(
180 $this->namespace,
181 '/' . $this->rest_base . 'get_templates',
182 array(
183 array(
184 'methods' => WP_REST_Server::READABLE,
185 'callback' => array( $this, 'get_templates' ),
186 'permission_callback' => array( $this, 'get_items_permissions_check' ),
187 ),
188 )
189 );
190
191 register_rest_route(
192 $this->namespace,
193 '/' . $this->rest_base . 'get_template_type_id',
194 array(
195 array(
196 'methods' => WP_REST_Server::READABLE,
197 'callback' => array( $this, 'get_template_type_id' ),
198 'permission_callback' => array( $this, 'get_items_permissions_check' ),
199
200 ),
201 )
202 );
203
204 }
205
206
207 /**
208 * Prepare a single setting object for response.
209 *
210 * @param $request
211 *
212 * @return WP_REST_Response
213 */
214 /**
215 * Prepare a single setting object for response.
216 *
217 * @param $request
218 *
219 * @return WP_REST_Response
220 */
221 public function get_templates( $request ) {
222 $type_param = isset( $_GET['type'] ) ? sanitize_key( $_GET['type'] ) : 'wc';
223 $store_checkout_param = isset( $_GET['store_checkout'] ) ? sanitize_key( $_GET['store_checkout'] ) : 'no';
224
225 // Map frontend goal types to API template types.
226 // 'store_checkout' (improve-checkout goal) passes through as-is.
227 // 'sales' / 'order-value' goals send 'wc'; keep backward-compat mapping here too.
228 $type_map = array(
229 'sales' => 'wc',
230 'lead' => 'lead',
231 );
232 $funnel_template_type = isset( $type_map[ $type_param ] ) ? $type_map[ $type_param ] : $type_param;
233
234 $step = isset( $_GET['step'] ) ? $_GET['step'] : false;
235 $builder = isset( $_GET['builder'] ) ? $_GET['builder'] : false;
236 if( 'yes' === $store_checkout_param ) {
237 $funnel_template_type = 'store_checkout';
238 }
239
240 $templates = $this->get_funnels_data( $funnel_template_type, $step, array(), false, $builder );
241 $templates = $this->prepare_custom_step($templates);
242 $templates['success'] = true;
243 return $this->prepare_item_for_response( $templates, $request );
244 }
245
246
247 /**
248 * Prepares custom steps.
249 * Allow user to create custom step from landing page.
250 *
251 * @param array $templates The templates to prepare.
252 * @return array
253 *
254 * @since 3.4.15
255 */
256 public function prepare_custom_step($templates){
257 if( is_array( $templates ) ){
258 if( isset( $templates['steps'] ) && is_array( $templates['steps'] ) ){
259 foreach( $templates['steps'] as $key => $step ){
260 if( is_array( $step ) && isset( $step['step_type'] ) && 'landing' === $step['step_type'] ){
261 $custom_step = $step;
262 $custom_step['step_type'] = 'custom';
263 if( isset( $custom_step['title'] ) ){
264 $custom_step['title'] = str_ireplace('Landing', '', $custom_step['title']);
265 }
266 $templates['steps'][] = $custom_step;
267 }
268 }
269 }
270 }
271 return $templates;
272 }
273
274
275
276 /**
277 * Send http request
278 *
279 * @param $url
280 * @param $args
281 *
282 * @return array
283 */
284 public static function remote_get( $url, $args ) {
285 $response = wp_remote_get( $url, $args );
286 // bail if there is an error
287 if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) {
288 return array(
289 'success' => false,
290 'message' => $response->get_error_message(),
291 'data' => $response,
292 );
293 }
294
295 // Decode the results.
296 $results = json_decode( $response['body'], true );
297
298 // bail if there is no result found
299 if ( ! is_array( $results ) ) {
300 return new \WP_Error( 'unexpected_data_format', 'Data was not returned in the expected format.' );
301 }
302
303 return array(
304 'success' => true,
305 'message' => 'Data successfully retrieved',
306 'data' => json_decode( wp_remote_retrieve_body( $response ), true ),
307 );
308 }
309
310
311 /**
312 * Get all funnels of the specific builder
313 *
314 * @param bool $type
315 * @param bool $isStep
316 * @param array $args
317 * @param bool $force_update
318 * @param bool $builder
319 *
320 * @return bool|mixed|void
321 */
322 public static function get_funnels( $type = false, $isStep = false, $args = array(), $force_update = false, $builder = false ) {
323 $builder_type = $builder ? $builder : Wpfnl_functions::get_builder_type();
324 $cache_key = 'wpfunnels_remote_template_data_' . $type . '_' . $builder_type . '_' . WPFNL_VERSION;
325 $data = get_transient( $cache_key );
326 $data = false; // Disable cache to make sure we are always getting the latest templates. We can remove this line once we are sure there is no issue with the API response time.
327 if ( $data ) {
328 return;
329 }
330 if ( $type && ( $force_update || false === $data ) ) {
331 if ( false === $data ) {
332 $data = array();
333 }
334 $timeout = ( $force_update ) ? 40 : 55;
335
336 $template_url = self::$all_funnels_api_url;
337 if ( 'oxygen' === $builder_type ) {
338 $template_url = 'https://oxygentemplates.getwpfunnels.com/wp-json/wpfunnels/v1/get_all_funnels/';
339 } elseif ( 'bricks' === $builder_type ) {
340 $template_url = 'https://brickstemplates.getwpfunnels.com/wp-json/wpfunnels/v1/get_all_funnels/';
341 }
342
343 // get all templates
344 $params = array(
345 'per_page' => 100,
346 'offset' => 0,
347 'builder' => $builder_type,
348 'template_type' => $type,
349 'template_url' => $template_url,
350 );
351 if( !defined('QUBELY_VERSION') ){
352 $params['gutenberg_type'] = 'wpf_native_gutenberg';
353 }
354 $url = add_query_arg( $params, $template_url );
355
356 $template_data = self::remote_get(
357 $url,
358 array(
359 'timeout' => $timeout,
360 )
361 );
362
363 if ( ! is_array( $template_data ) || ! $template_data['success'] ) {
364 $url = add_query_arg( $params, WPFNL_MIDDLEMAN_TEMPLATE_URL );
365 $template_data = self::remote_get(
366 $url,
367 array(
368 'timeout' => $timeout,
369 )
370 );
371
372 if ( ! is_array( $template_data ) || ! $template_data['success'] ) {
373 set_transient( $cache_key, array(), 24 * HOUR_IN_SECONDS );
374 return false;
375 }
376 }
377
378 // get all steps
379 $steps = array();
380 $templates = array();
381 if ( $template_data['data'] ) {
382 foreach ( $template_data['data'] as $key => $template ) {
383
384 $i = 0;
385 $thankyou_count = 0;
386 if( isset($template['page_builder']) && 'gutenberg' === $template['page_builder'] ){
387 if( !defined('QUBELY_VERSION') && 'wpf_native_gutenberg' !== $template['gutenberg_type'] ){
388 continue;
389 }
390 }
391 if( isset($template['is_store_checkout']) && $template['is_store_checkout'] && 'store_checkout' !== $type ) {
392 continue;
393 }
394
395 foreach ( $template['steps'] as $_step ) {
396 if ( $thankyou_count && 'thankyou' === $_step['step_type'] ) {
397 continue;
398 }
399 if ( 0 == $i ) {
400 $template_data['data'][ $key ]['link'] = $_step['link'];
401 }
402 $_step['funnel_name'] = $template['title'];
403 $_step['template_type'] = isset( $template['templateType'] ) ? ( 'free' === $template['templateType'] ? 'free' : 'pro' ) : 'free';
404 $steps[] = $_step;
405 if ( 'thankyou' === $_step['step_type'] ) {
406 $thankyou_count++;
407 }
408 $i++;
409 }
410 array_push($templates, $template_data['data'][ $key ]);
411 }
412 }
413
414 // fetch the funnel categories from the remote server
415 $cat_url = self::$funnel_categories_api_url;
416 if ( 'oxygen' === $builder_type ) {
417 $cat_url = 'https://oxygentemplates.getwpfunnels.com/wp-json/wp/v2/template_industries/';
418 } elseif ( 'bricks' === $builder_type ) {
419 $cat_url = 'https://brickstemplates.getwpfunnels.com/wp-json/wp/v2/template_industries/';
420 }
421
422 $params = array(
423 'per_page' => 100,
424 'category_url' => $cat_url,
425 );
426
427 $url = add_query_arg( $params, $cat_url );
428 $categories_data = self::remote_get(
429 $url,
430 array(
431 'timeout' => $timeout,
432 )
433 );
434
435 if ( ! is_array( $categories_data ) || ! $categories_data['success'] ) {
436 $url = add_query_arg( $params, WPFNL_MIDDLEMAN_TEMPLATE_CATEGORY_URL );
437 $categories_data = self::remote_get(
438 $url,
439 array(
440 'timeout' => $timeout,
441 )
442 );
443
444 if ( ! is_array( $categories_data ) || ! $categories_data['success'] ) {
445 set_transient( $cache_key, array(), 24 * HOUR_IN_SECONDS );
446 return false;
447 }
448 }
449
450 $data['templates'] = $templates;
451 $data['steps'] = $steps;
452 $data['categories'] = $categories_data['data'];
453 update_option( WPFNL_TEMPLATES_OPTION_KEY . '_' . $type . '_' . $builder_type, $data, 'no' );
454 set_transient( $cache_key, $data, 24 * HOUR_IN_SECONDS );
455 return false;
456 }
457 }
458
459
460
461 /**
462 * Get funnel templates data
463 *
464 * @param array $args
465 * @param bool $force_update
466 *
467 * @return array|mixed|void
468 * @since 1.0.0
469 */
470 public function get_funnels_data( $type = false, $step = false, $args = array(), $force_update = false, $builder = false ) {
471
472 self::get_funnels( $type, $step, $args, $force_update, $builder );
473 $builder_type = $builder ? $builder : Wpfnl_functions::get_builder_type();
474 $template_data = get_option( WPFNL_TEMPLATES_OPTION_KEY . '_' . $type . '_' . $builder_type );
475
476 // Fallback for backward compatibility if data is not found with builder
477 if ( empty( $template_data ) && !$builder ) {
478 $template_data = get_option( WPFNL_TEMPLATES_OPTION_KEY . '_' . $type );
479 }
480
481 if ( empty( $template_data ) ) {
482 return array();
483 }
484 return $template_data;
485 }
486
487
488 /**
489 * Get step from template library site by step ID
490 *
491 * @param $step_id
492 * @param bool $force_update
493 *
494 * @return array
495 * @since 1.0.0
496 */
497 public static function get_step( $step_id, $force_update = false ) {
498 $timeout = ( $force_update ) ? 25 : 8;
499
500 $params = array(
501 '_fields' => 'id,title,link,slug,featured_media,post_meta,rawData,steps,divi_content,featured_image,steps_order,builder,industry,is_pro,type,step_type,funnel_id,funnel_name,_qubely_css,_qubely_interaction_json,__qubely_available_blocks',
502 'url' => self::get_remote_funnel_steps_api_url() . $step_id,
503 );
504
505 $url = add_query_arg( $params, self::get_remote_funnel_steps_api_url() . $step_id );
506 $api_args = array(
507 'timeout' => $timeout,
508 );
509 $response = ( new TemplateLibraryController() )->remote_get( $url, $api_args );
510
511 if ( is_array($response) && isset( $response['success'] ) ) {
512 $step = $response['data'];
513 return array(
514 'title' => ( isset( $step['title']['rendered'] ) ) ? $step['title']['rendered'] : '',
515 'post_meta' => ( isset( $step['post_meta'] ) ) ? $step['post_meta'] : '',
516 'data' => $step,
517 'content' => isset( $response['data']['content']['rendered'] ) ? $response['data']['content']['rendered'] : '',
518 'rawData' => isset( $response['data']['rawData'] ) ? $response['data']['rawData'] : '',
519 'divi_content' => isset( $response['data']['divi_content'] ) ? $response['data']['divi_content'] : '',
520 'message' => $response['message'],
521 'success' => $response['success'],
522 );
523 }
524
525 $url = add_query_arg( $params, WPFNL_MIDDLEMAN_STEP_URL );
526 $response = ( new TemplateLibraryController() )->remote_get( $url, $api_args );
527
528 if ( is_array($response) && isset( $response['success'] ) ) {
529 $step = $response['data'];
530 return array(
531 'title' => ( isset( $step['title']['rendered'] ) ) ? $step['title']['rendered'] : '',
532 'post_meta' => ( isset( $step['post_meta'] ) ) ? $step['post_meta'] : '',
533 'data' => $step,
534 'content' => isset( $response['data']['content']['rendered'] ) ? $response['data']['content']['rendered'] : '',
535 'rawData' => isset( $response['data']['rawData'] ) ? $response['data']['rawData'] : '',
536 'divi_content' => isset( $response['data']['divi_content'] ) ? $response['data']['divi_content'] : '',
537 'message' => $response['message'],
538 'success' => $response['success'],
539 );
540 }
541
542 if ( is_wp_error( $response ) ) {
543 return array(
544 'title' => '',
545 'post_meta' => array(),
546 'message' => $response->get_error_message(),
547 'data' => array(),
548 'success' => false,
549 'content' => '',
550 );
551 }
552
553 return array(
554 'title' => '',
555 'post_meta' => array(),
556 'message' => isset( $response['message'] ) ? $response['message'] : '',
557 'data' => isset( $response['data'] ) ? $response['data'] : array(),
558 'success' => isset( $response['success'] ) ? $response['success'] : false,
559 'content' => '',
560 );
561 }
562
563
564
565
566 public static function get_funnel( $funnel_id, $force_update = false ) {
567 $timeout = ( $force_update ) ? 25 : 8;
568 $params = array(
569 '_fields' => 'id,title,link,slug,featured_media,funnel_data,_funnel_data',
570 'url' => self::get_remote_funnel_api_url().$funnel_id,
571 );
572
573 $url = add_query_arg( $params, self::get_remote_funnel_api_url() . $funnel_id );
574
575 $api_args = array(
576 'timeout' => $timeout,
577 );
578 $response = ( new TemplateLibraryController() )->remote_get( $url, $api_args );
579
580 if (is_array($response) && isset( $response['success'] )) {
581 $funnel = $response['data'];
582 return array(
583 'funnel_id' => $funnel_id,
584 'funnel_data' => $funnel['funnel_data'] ?? '',
585 '_funnel_data' => $funnel['_funnel_data'] ?? '',
586 'title' => ( isset( $funnel['title']['rendered'] ) ) ? $funnel['title']['rendered'] : '',
587 'success' => $response['success'],
588 );
589 }
590
591 $url = add_query_arg( $params, WPFNL_MIDDLEMAN_SINGLE_TEMPLATE_URL );
592 $response = ( new TemplateLibraryController() )->remote_get( $url, $api_args );
593
594 if ( is_array($response) && isset( $response['success'] ) ) {
595 $funnel = $response['data'];
596 return array(
597 'funnel_id' => $funnel_id,
598 'funnel_data' => $funnel['funnel_data'] ?? '',
599 '_funnel_data' => $funnel['_funnel_data'] ?? '',
600 'title' => ( isset( $funnel['title']['rendered'] ) ) ? $funnel['title']['rendered'] : '',
601 'success' => $response['success'],
602 );
603 }
604
605 if ( is_wp_error( $response ) ) {
606 return array(
607 'title' => '',
608 'post_meta' => array(),
609 'message' => $response->get_error_message(),
610 'data' => array(),
611 'success' => false,
612 'content' => '',
613 );
614 }
615
616 return array(
617 'title' => '',
618 'post_meta' => array(),
619 'message' => isset( $response['message'] ) ? $response['message'] : '',
620 'data' => isset( $response['data'] ) ? $response['data'] : array(),
621 'success' => isset( $response['success'] ) ? $response['success'] : false,
622 'content' => '',
623 );
624 }
625
626
627 /**
628 * Get funnel type id
629 */
630 public function get_template_type_id( $request ) {
631
632 $response = array(
633 'success' => false,
634 'type_id' => '',
635 );
636
637 if ( $request['type'] ) {
638 $type = $request['type'];
639 $force_update = false;
640 $timeout = ( $force_update ) ? 40 : 55;
641 $teplate_type_url = self::get_remote_funnel_template_type_api_url();
642 $response = self::remote_get(
643 $teplate_type_url,
644 array(
645 'timeout' => $timeout,
646 )
647 );
648
649 if ( isset( $response['data'] ) && is_array( $response['data'] ) && !empty($response['data'])) {
650 foreach ( $response['data'] as $types ) {
651 if ( $type === $types['name'] ) {
652 $response['success'] = true;
653 $response['type_id'] = $types['id'];
654 }
655 }
656 }
657
658 $params = array(
659 'url' => self::get_remote_funnel_template_type_api_url()
660 );
661
662 $url = add_query_arg( $params, WPFNL_MIDDLEMAN_TEMPLATE_TYPE_URL );
663 $response = ( new TemplateLibraryController() )->remote_get( $url , array(
664 'timeout' => $timeout,
665 ) );
666
667 if ( isset( $response['data'] ) && is_array( $response['data'] ) && !empty($response['data'])) {
668 foreach ( $response['data'] as $types ) {
669 if ( $type === $types['name'] ) {
670 $response['success'] = true;
671 $response['type_id'] = $types['id'];
672 }
673 }
674 }
675
676
677 }
678 return rest_ensure_response( $response );
679 }
680
681
682 /**
683 * Prepare a single setting object for response.
684 *
685 * @param object $item Setting object.
686 * @param WP_REST_Request $request Request object.
687 *
688 * @return WP_REST_Response $response Response data.
689 * @since 1.0.0
690 */
691 public function prepare_item_for_response( $item, $request ) {
692 $data = $this->add_additional_fields_to_object( $item, $request );
693 return rest_ensure_response( $data );
694 }
695 }
696