GravityView  2.17
The best, easiest way to display Gravity Forms entries on your website.
class-gv-rest-views-route.php
Go to the documentation of this file.
1 <?php
2 /**
3  * @package GravityView
4  * @license GPL2+
5  * @author Josh Pollock <[email protected]>
6  * @link http://gravityview.co
7  * @copyright Copyright 2015, Katz Web Services, Inc.
8  *
9  * @since 2.0
10  */
11 namespace GV\REST;
12 
13 /** If this file is called directly, abort. */
14 if ( ! defined( 'GRAVITYVIEW_DIR' ) ) {
15  die();
16 }
17 
18 class Views_Route extends Route {
19  /**
20  * Route Name
21  *
22  * @since 2.0
23  *
24  * @access protected
25  * @string
26  */
27  protected $route_name = 'views';
28 
29  /**
30  * Sub type, forms {$namespace}/route_name/{id}/sub_type type endpoints
31  *
32  * @since 2.0
33  * @access protected
34  * @var string
35  */
36  protected $sub_type = 'entries';
37 
38 
39  /**
40  * Get a collection of views
41  *
42  * Callback for GET /v1/views/
43  *
44  * @param \WP_REST_Request $request Full data about the request.
45  * @return \WP_Error|\WP_REST_Response
46  */
47  public function get_items( $request ) {
48 
49  $page = $request->get_param( 'page' );
50  $limit = $request->get_param( 'limit' );
51 
52  $items = \GVCommon::get_all_views( array(
53  'posts_per_page' => $limit,
54  'paged' => $page,
55  ) );
56 
57  if ( empty( $items ) ) {
58  return new \WP_Error( 'gravityview-no-views', __( 'No Views found.', 'gk-gravityview' ) ); //@todo message
59  }
60 
61  $data = array(
62  'views' => array(),
63  'total' => wp_count_posts( 'gravityview' )->publish,
64  );
65  foreach ( $items as $item ) {
66  $data['views'][] = $this->prepare_view_for_response( $item, $request );
67  }
68 
69  return new \WP_REST_Response( $data, 200 );
70  }
71 
72  /**
73  * Get one view
74  *
75  * Callback for /v1/views/{id}/
76  *
77  * @since 2.0
78  * @param \WP_REST_Request $request Full data about the request.
79  * @return \WP_Error|\WP_REST_Response
80  */
81  public function get_item( $request ) {
82 
83  $url = $request->get_url_params();
84 
85  $view_id = intval( $url['id'] );
86 
87  $item = get_post( $view_id );
88 
89  //return a response or error based on some conditional
90  if ( $item && ! is_wp_error( $item ) ) {
91  $data = $this->prepare_view_for_response( $item, $request );
92  return new \WP_REST_Response( $data, 200 );
93  }
94 
95  return new \WP_Error( 'code', sprintf( 'A View with ID #%d was not found.', $view_id ) );
96  }
97 
98  /**
99  * Prepare the item for the REST response
100  *
101  * @since 2.0
102  * @param \GV\View $view The view.
103  * @param \GV\Entry $entry WordPress representation of the item.
104  * @param \WP_REST_Request $request Request object.
105  * @param string $context The context (directory, single)
106  * @param string $class The value renderer. Default: null (raw value)
107  *
108  * @since 2.1 Add value renderer override $class parameter.
109  *
110  * @return mixed The data that is sent.
111  */
112  public function prepare_entry_for_response( $view, $entry, \WP_REST_Request $request, $context, $class = null ) {
113 
114  // Only output the fields that should be displayed.
115  $allowed = array();
116  foreach ( $view->fields->by_position( "{$context}_*" )->by_visible( $view )->all() as $field ) {
117  $allowed[] = $field;
118  }
119 
120  /**
121  * @filter `gravityview/rest/entry/fields` Allow more entry fields that are output in regular REST requests.
122  * @param array $allowed The allowed ones, default by_visible, by_position( "context_*" ), i.e. as set in the view.
123  * @param \GV\View $view The view.
124  * @param \GV\Entry $entry The entry.
125  * @param \WP_REST_Request $request Request object.
126  * @param string $context The context (directory, single)
127  */
128  $allowed_field_ids = apply_filters( 'gravityview/rest/entry/fields', wp_list_pluck( $allowed, 'ID' ), $view, $entry, $request, $context );
129 
130  $allowed = array_filter( $allowed, function( $field ) use ( $allowed_field_ids ) {
131  return in_array( $field->ID, $allowed_field_ids, true );
132  } );
133 
134  // Tack on additional fields if needed
135  foreach ( array_diff( $allowed_field_ids, wp_list_pluck( $allowed, 'ID' ) ) as $field_id ) {
136  $allowed[] = is_numeric( $field_id ) ? \GV\GF_Field::by_id( $view->form, $field_id ) : \GV\Internal_Field::by_id( $field_id );
137  }
138 
139  $r = new Request( $request );
140  $return = array();
141 
142  $renderer = new \GV\Field_Renderer();
143 
144  $used_ids = array();
145 
146  foreach ( $allowed as $field ) {
147  $source = is_numeric( $field->ID ) ? $view->form : new \GV\Internal_Source();
148 
149  $field_id = $field->ID;
150  $index = null;
151 
152  if ( ! isset( $used_ids[ $field_id ] ) ) {
153  $used_ids[ $field_id ] = 0;
154  } else {
155  $index = ++$used_ids[ $field_id ];
156  }
157 
158  if ( $index ) {
159  /**
160  * Modify non-unique IDs (custom, id, etc.) to be unique and not gobbled up.
161  */
162  $field_id = sprintf( '%s(%d)', $field_id, $index + 1 );
163  }
164 
165  /**
166  * @filter `gravityview/api/field/key` Filter the key name in the results for JSON output.
167  * @param string $field_id The ID. Should be unique or keys will be gobbled up.
168  * @param \GV\View $view The view.
169  * @param \GV\Entry $entry The entry.
170  * @param \WP_REST_Request $request Request object.
171  * @param string $context The context (directory, single)
172  */
173  $field_id = apply_filters( 'gravityview/api/field/key', $field_id, $view, $entry, $request, $context );
174 
175  if ( ! $class && in_array( $field->ID, array( 'custom' ) ) ) {
176  /**
177  * Custom fields (and perhaps some others) will require rendering as they don't
178  * contain an intrinsic value (for custom their value is stored in the view and requires a renderer).
179  * We force the CSV template to take over in such cases, it's good enough for most cases.
180  */
181  $return[ $field_id ] = $renderer->render( $field, $view, $source, $entry, $r, '\GV\Field_CSV_Template' );
182  } else if ( $class ) {
183  $return[ $field_id ] = $renderer->render( $field, $view, $source, $entry, $r, $class );
184  } else {
185  switch ( $field->type ):
186  case 'list':
187  $return[ $field_id ] = unserialize( $field->get_value( $view, $source, $entry, $r ) );
188  break;
189  case 'fileupload':
190  case 'business_hours':
191  $return[ $field_id ] = json_decode( $field->get_value( $view, $source, $entry, $r ) );
192  break;
193  default;
194  $return[ $field_id ] = $field->get_value( $view, $source, $entry, $r );
195  endswitch;
196  }
197  }
198 
199  return $return;
200  }
201 
202  /**
203  * Get entries from a view
204  *
205  * Callback for /v1/views/{id}/entries/
206  *
207  * @since 2.0
208  * @param \WP_REST_Request $request Full data about the request.
209  * @return \WP_Error|\WP_REST_Response
210  */
211  public function get_sub_items( $request ) {
212 
213  $url = $request->get_url_params();
214  $view_id = intval( $url['id'] );
215  $format = \GV\Utils::get( $url, 'format', 'json' );
216 
217  if( $post_id = $request->get_param('post_id') ) {
218  global $post;
219 
220  $post = get_post( $post_id );
221 
222  if ( ! $post || is_wp_error( $post ) ) {
223  return new \WP_Error( 'gravityview-post-not-found', sprintf( 'A post with ID #%d was not found.', $post_id ) );
224  }
225 
226  $collection = \GV\View_Collection::from_post( $post );
227 
228  if ( ! $collection->contains( $view_id ) ) {
229  return new \WP_Error( 'gravityview-post-not-contains', sprintf( 'The post with ID #%d does not contain a View with ID #%d', $post_id, $view_id ) );
230  }
231  }
232 
233  $view = \GV\View::by_id( $view_id );
234 
235  if ( 'html' === $format ) {
236 
237  $renderer = new \GV\View_Renderer();
238  $count = $total = 0;
239 
240  /** @var \GV\Template_Context $context */
241  add_action( 'gravityview/template/view/render', function( $context ) use ( &$count, &$total ) {
242  $count = $context->entries->count();
243  $total = $context->entries->total();
244  } );
245 
246  $output = $renderer->render( $view, new Request( $request ) );
247 
248  /**
249  * @filter `gravityview/rest/entries/html/insert_meta` Whether to include `http-equiv` meta tags in the HTML output describing the data
250  * @since 2.0
251  * @param bool $insert_meta Add <meta> tags? [Default: true]
252  * @param int $count The number of entries being rendered
253  * @param \GV\View $view The view.
254  * @param \WP_REST_Request $request Request object.
255  * @param int $total The number of total entries for the request
256  */
257  $insert_meta = apply_filters( 'gravityview/rest/entries/html/insert_meta', true, $count, $view, $request, $total );
258 
259  if ( $insert_meta ) {
260  $output = '<meta http-equiv="X-Item-Count" content="' . $count . '" />' . $output;
261  $output = '<meta http-equiv="X-Item-Total" content="' . $total . '" />' . $output;
262  }
263 
264  $response = new \WP_REST_Response( $output, 200 );
265  $response->header( 'X-Item-Count', $count );
266  $response->header( 'X-Item-Total', $total );
267 
268  return $response;
269  }
270 
271  $entries = $view->get_entries( new Request( $request ) );
272 
273  if ( ! $entries->all() ) {
274  return new \WP_Error( 'gravityview-no-entries', __( 'No Entries found.', 'gk-gravityview' ) );
275  }
276 
277  if ( in_array( $format, array( 'csv', 'tsv' ), true ) ) {
278 
279  ob_start();
280 
281  $csv_or_tsv = fopen( 'php://output', 'w' );
282 
283  /** Da' BOM :) */
284  if ( apply_filters( 'gform_include_bom_export_entries', true, $view->form ? $view->form->form : null ) ) {
285  fputs( $csv_or_tsv, "\xef\xbb\xbf" );
286  }
287 
288  $headers_done = false;
289 
290  // If not "tsv" then use comma
291  $delimiter = ( 'tsv' === $format ) ? "\t" : ',';
292 
293  foreach ( $entries->all() as $entry ) {
294  $entry = $this->prepare_entry_for_response( $view, $entry, $request, 'directory', '\GV\Field_CSV_Template' );
295 
296  if ( ! $headers_done ) {
297  $headers_done = fputcsv( $csv_or_tsv, array_map( array( '\GV\Utils', 'strip_excel_formulas' ), array_keys( $entry ) ), $delimiter );
298  }
299 
300  fputcsv( $csv_or_tsv, array_map( array( '\GV\Utils', 'strip_excel_formulas' ), $entry ), $delimiter );
301  }
302 
303  $response = new \WP_REST_Response( '', 200 );
304  $response->header( 'X-Item-Count', $entries->count() );
305  $response->header( 'X-Item-Total', $entries->total() );
306  $response->header( 'Content-Type', 'text/' . $format );
307 
308  fflush( $csv_or_tsv );
309 
310  $data = rtrim( ob_get_clean() );
311 
312  add_filter( 'rest_pre_serve_request', function() use ( $data ) {
313  echo $data;
314  return true;
315  } );
316 
317  if ( defined( 'DOING_GRAVITYVIEW_TESTS' ) && DOING_GRAVITYVIEW_TESTS ) {
318  echo $data; // rest_pre_serve_request is not called in tests
319  }
320 
321  return $response;
322  }
323 
324  $data = array( 'entries' => $entries->all(), 'total' => $entries->total() );
325 
326  foreach ( $data['entries'] as &$entry ) {
327  $entry = $this->prepare_entry_for_response( $view, $entry, $request, 'directory' );
328  }
329 
330  return new \WP_REST_Response( $data, 200 );
331  }
332 
333  /**
334  * Get one entry from view
335  *
336  * Callback for /v1/views/{id}/entries/{id}/
337  *
338  * @uses GVCommon::get_entry
339  * @since 2.0
340  * @param \WP_REST_Request $request Full data about the request.
341  * @return \WP_Error|\WP_REST_Response
342  */
343  public function get_sub_item( $request ) {
344  $url = $request->get_url_params();
345  $view_id = intval( $url['id'] );
346  $entry_id = intval( $url['s_id'] );
347  $format = \GV\Utils::get( $url, 'format', 'json' );
348 
349  $view = \GV\View::by_id( $view_id );
350  $entry = \GV\GF_Entry::by_id( $entry_id );
351 
352  if ( $format === 'html' ) {
353  $renderer = new \GV\Entry_Renderer();
354  return $renderer->render( $entry, $view, new Request( $request ) );
355  }
356 
357  return $this->prepare_entry_for_response( $view, $entry, $request, 'single' );
358  }
359 
360  /**
361  * Prepare the item for the REST response
362  *
363  * @since 2.0
364  * @param \WP_Post $view_post WordPress representation of the item.
365  * @param \WP_REST_Request $request Request object.
366  * @return mixed
367  */
368  public function prepare_view_for_response( $view_post, \WP_REST_Request $request ) {
369  if ( is_wp_error( $this->get_item_permissions_check( $request, $view_post->ID ) ) ) {
370  // Redacted out view.
371  return array( 'ID' => $view_post->ID, 'post_content' => __( 'You are not allowed to access this content.', 'gk-gravityview' ) );
372  }
373 
374  $view = \GV\View::from_post( $view_post );
375 
376  $item = $view->as_data();
377 
378  // Add all the WP_Post data
379  $view_post = $view_post->to_array();
380 
381  unset( $view_post['to_ping'], $view_post['ping_status'], $view_post['pinged'], $view_post['post_type'], $view_post['filter'], $view_post['post_category'], $view_post['tags_input'], $view_post['post_content'], $view_post['post_content_filtered'] );
382 
383  $return = wp_parse_args( $item, $view_post );
384 
385  $return['title'] = $return['post_title'];
386 
387  $return['settings'] = isset( $return['atts'] ) ? $return['atts'] : array();
388  unset( $return['atts'], $return['view_id'] );
389 
390  $return['search_criteria'] = array(
391  'page_size' => rgars( $return, 'settings/page_size' ),
392  'sort_field' => rgars( $return, 'settings/sort_field' ),
393  'sort_direction' => rgars( $return, 'settings/sort_direction' ),
394  'offset' => rgars( $return, 'settings/offset' ),
395  );
396 
397  unset( $return['settings']['page_size'], $return['settings']['sort_field'], $return['settings']['sort_direction'] );
398 
399  // Redact for non-logged ins
400  if ( ! \GVCommon::has_cap( 'edit_others_gravityviews' ) ) {
401  unset( $return['settings'] );
402  unset( $return['search_criteria'] );
403  }
404 
405  if ( ! \GFCommon::current_user_can_any( 'gravityforms_edit_forms' ) ) {
406  unset( $return['form'] );
407  }
408 
409  return $return;
410  }
411 
412  /**
413  * @param \WP_REST_Request $request
414  *
415  * @return bool|\WP_Error
416  */
417  public function get_item_permissions_check( $request ) {
418  if ( func_num_args() === 2 ) {
419  $view_id = func_get_arg( 1 ); // $view_id override
420  } else {
421  $url = $request->get_url_params();
422  $view_id = intval( $url['id'] );
423  }
424 
425  if ( ! $view = \GV\View::by_id( $view_id ) ) {
426  return new \WP_Error( 'rest_forbidden', __( 'You are not allowed to access this content.', 'gk-gravityview' ) );
427  }
428 
429  while ( $error = $view->can_render( array( 'rest' ), $request ) ) {
430 
431  if ( ! is_wp_error( $error ) ) {
432  break;
433  }
434 
435  switch ( str_replace( 'gravityview/', '', $error->get_error_code() ) ) {
436  case 'rest_disabled':
437  case 'post_password_required':
438  case 'not_public':
439  case 'embed_only':
440  case 'no_direct_access':
441  return new \WP_Error( 'rest_forbidden_access_denied', __( 'You are not allowed to access this content.', 'gk-gravityview' ) );
442  case 'no_form_attached':
443  return new \WP_Error( 'rest_forbidden_no_form_attached', __( 'This View is not configured properly.', 'gk-gravityview' ) );
444  default:
445  return new \WP_Error( 'rest_forbidden', __( 'You are not allowed to access this content.', 'gk-gravityview' ) );
446  }
447  }
448 
449  /**
450  * @filter `gravityview/view/output/rest` Disable rest output. Final chance.
451  * @param bool Enable or not.
452  * @param \GV\View $view The view.
453  */
454  if ( ! apply_filters( 'gravityview/view/output/rest', true, $view ) ) {
455  return new \WP_Error( 'rest_forbidden', __( 'You are not allowed to access this content.', 'gk-gravityview' ) );
456  }
457 
458  return true;
459  }
460 
461  public function get_sub_item_permissions_check( $request ) {
462  // Accessing a single entry needs the View access permissions.
463  if ( is_wp_error( $error = $this->get_items_permissions_check( $request ) ) ) {
464  return $error;
465  }
466 
467  $url = $request->get_url_params();
468  $view_id = intval( $url['id'] );
469  $entry_id = intval( $url['s_id'] );
470 
471  $view = \GV\View::by_id( $view_id );
472 
473  if ( ! $entry = \GV\GF_Entry::by_id( $entry_id ) ) {
474  return new \WP_Error( 'rest_forbidden', 'You are not allowed to view this content.', 'gravityview' );
475  }
476 
477  if ( $entry['form_id'] != $view->form->ID ) {
478  return new \WP_Error( 'rest_forbidden', 'You are not allowed to view this content.', 'gravityview' );
479  }
480 
481  if ( $entry['status'] != 'active' ) {
482  return new \WP_Error( 'rest_forbidden', 'You are not allowed to view this content.', 'gravityview' );
483  }
484 
485  if ( apply_filters( 'gravityview_custom_entry_slug', false ) && $entry->slug != get_query_var( \GV\Entry::get_endpoint_name() ) ) {
486  return new \WP_Error( 'rest_forbidden', 'You are not allowed to view this content.', 'gravityview' );
487  }
488 
489  $is_admin_and_can_view = $view->settings->get( 'admin_show_all_statuses' ) && \GVCommon::has_cap('gravityview_moderate_entries', $view->ID );
490 
491  if ( $view->settings->get( 'show_only_approved' ) && ! $is_admin_and_can_view ) {
493  return new \WP_Error( 'rest_forbidden', 'You are not allowed to view this content.', 'gravityview' );
494  }
495  }
496 
497  return true;
498  }
499 
500  public function get_items_permissions_check( $request ) {
501  // Getting a list of all Views is always possible.
502  return true;
503  }
504 
505  public function get_sub_items_permissions_check( $request ) {
506  // Accessing all entries of a View needs the same permissions as accessing the View.
507  return $this->get_item_permissions_check( $request );
508  }
509 }
$url
Definition: post_image.php:25
prepare_entry_for_response( $view, $entry, \WP_REST_Request $request, $context, $class=null)
Prepare the item for the REST response.
If this file is called directly, abort.
get_items( $request)
Get a collection of views.
if(gv_empty( $field['value'], false, false)) $format
$class
$entries
prepare_view_for_response( $view_post, \WP_REST_Request $request)
Prepare the item for the REST response.
If this file is called directly, abort.
static from_post(\WP_Post $post)
Get a list of objects inside the supplied .
global $post
Definition: delete-entry.php:7
static by_id( $form, $field_id)
Get a by and Field ID.
static get_endpoint_name()
Return the endpoint name for a single Entry.
get_item( $request)
Get one view.
If this file is called directly, abort.
static by_id( $post_id)
Construct a instance from a post ID.
get_sub_item( $request)
Get one entry from view.
get_sub_items( $request)
Get a collection of items.
static by_id( $field_id)
Get a from an internal Gravity Forms field ID.
static get( $array, $key, $default=null)
Grab a value from an array or an object or default.
static by_id( $entry_id, $form_id=0)
Construct a instance by ID.
static is_approved( $status)
static has_cap( $caps='', $object_id=null, $user_id=null)
Alias of GravityView_Roles_Capabilities::has_cap()
$entry
Definition: notes.php:27
if(false !==strpos( $value, '00:00')) $field_id
string $field_id ID of the field being displayed
Definition: time.php:22
static get_all_views( $args=array())
Get all existing Views.
static from_post( $post)
Construct a instance from a .
const meta_key