GravityView  2.5
The best, easiest way to display Gravity Forms entries on your website.
class-gv-view.php
Go to the documentation of this file.
1 <?php
2 namespace GV;
3 
4 /** If this file is called directly, abort. */
5 if ( ! defined( 'GRAVITYVIEW_DIR' ) ) {
6  die();
7 }
8 
9 /**
10  * The default GravityView View class.
11  *
12  * Houses all base View functionality.
13  *
14  * Can be accessed as an array for old compatibility's sake
15  * in line with the elements inside the \GravityView_View_Data::$views array.
16  */
17 class View implements \ArrayAccess {
18 
19  /**
20  * @var \WP_Post The backing post instance.
21  */
22  private $post;
23 
24  /**
25  * @var \GV\View_Settings The settings.
26  *
27  * @api
28  * @since 2.0
29  */
30  public $settings;
31 
32  /**
33  * @var \GV\Widget_Collection The widets attached here.
34  *
35  * @api
36  * @since 2.0
37  */
38  public $widgets;
39 
40  /**
41  * @var \GV\GF_Form|\GV\Form The backing form for this view.
42  *
43  * Contains the form that is sourced for entries in this view.
44  *
45  * @api
46  * @since 2.0
47  */
48  public $form;
49 
50  /**
51  * @var \GV\Field_Collection The fields for this view.
52  *
53  * Contains all the fields that are attached to this view.
54  *
55  * @api
56  * @since 2.0
57  */
58  public $fields;
59 
60  /**
61  * @var array
62  *
63  * Internal static cache for gets, and whatnot.
64  * This is not persistent, resets across requests.
65 
66  * @internal
67  */
68  private static $cache = array();
69 
70  /**
71  * @var \GV\Join[] The joins for all sources in this view.
72  *
73  * @api
74  * @since 2.0.1
75  */
76  public $joins = array();
77 
78  /**
79  * @var \GV\Field[][] The unions for all sources in this view.
80  * An array of fields grouped by form_id keyed by
81  * main field_id:
82  *
83  * array(
84  * $form_id => array(
85  * $field_id => $field,
86  * $field_id => $field,
87  * )
88  * )
89  *
90  * @api
91  * @since 2.2.2
92  */
93  public $unions = array();
94 
95  /**
96  * The constructor.
97  */
98  public function __construct() {
99  $this->settings = new View_Settings();
100  $this->fields = new Field_Collection();
101  $this->widgets = new Widget_Collection();
102  }
103 
104  /**
105  * Register the gravityview WordPress Custom Post Type.
106  *
107  * @internal
108  * @return void
109  */
110  public static function register_post_type() {
111 
112  /** Register only once */
113  if ( post_type_exists( 'gravityview' ) ) {
114  return;
115  }
116 
117  /**
118  * @filter `gravityview_is_hierarchical` Make GravityView Views hierarchical by returning TRUE
119  * This will allow for Views to be nested with Parents and also allows for menu order to be set in the Page Attributes metabox
120  * @since 1.13
121  * @param boolean $is_hierarchical Default: false
122  */
123  $is_hierarchical = (bool)apply_filters( 'gravityview_is_hierarchical', false );
124 
125  $supports = array( 'title', 'revisions' );
126 
127  if ( $is_hierarchical ) {
128  $supports[] = 'page-attributes';
129  }
130 
131  /**
132  * @filter `gravityview_post_type_supports` Modify post type support values for `gravityview` post type
133  * @see add_post_type_support()
134  * @since 1.15.2
135  * @param array $supports Array of features associated with a functional area of the edit screen. Default: 'title', 'revisions'. If $is_hierarchical, also 'page-attributes'
136  * @param[in] boolean $is_hierarchical Do Views support parent/child relationships? See `gravityview_is_hierarchical` filter.
137  */
138  $supports = apply_filters( 'gravityview_post_type_support', $supports, $is_hierarchical );
139 
140  /** Register Custom Post Type - gravityview */
141  $labels = array(
142  'name' => _x( 'Views', 'Post Type General Name', 'gravityview' ),
143  'singular_name' => _x( 'View', 'Post Type Singular Name', 'gravityview' ),
144  'menu_name' => _x( 'Views', 'Menu name', 'gravityview' ),
145  'parent_item_colon' => __( 'Parent View:', 'gravityview' ),
146  'all_items' => __( 'All Views', 'gravityview' ),
147  'view_item' => _x( 'View', 'View Item', 'gravityview' ),
148  'add_new_item' => __( 'Add New View', 'gravityview' ),
149  'add_new' => __( 'New View', 'gravityview' ),
150  'edit_item' => __( 'Edit View', 'gravityview' ),
151  'update_item' => __( 'Update View', 'gravityview' ),
152  'search_items' => __( 'Search Views', 'gravityview' ),
153  'not_found' => \GravityView_Admin::no_views_text(),
154  'not_found_in_trash' => __( 'No Views found in Trash', 'gravityview' ),
155  'filter_items_list' => __( 'Filter Views list', 'gravityview' ),
156  'items_list_navigation' => __( 'Views list navigation', 'gravityview' ),
157  'items_list' => __( 'Views list', 'gravityview' ),
158  'view_items' => __( 'See Views', 'gravityview' ),
159  'attributes' => __( 'View Attributes', 'gravityview' ),
160  );
161  $args = array(
162  'label' => __( 'view', 'gravityview' ),
163  'description' => __( 'Create views based on a Gravity Forms form', 'gravityview' ),
164  'labels' => $labels,
165  'supports' => $supports,
166  'hierarchical' => $is_hierarchical,
167  /**
168  * @filter `gravityview_direct_access` Should Views be directly accessible, or only visible using the shortcode?
169  * @see https://codex.wordpress.org/Function_Reference/register_post_type#public
170  * @since 1.15.2
171  * @param[in,out] boolean `true`: allow Views to be accessible directly. `false`: Only allow Views to be embedded via shortcode. Default: `true`
172  * @param int $view_id The ID of the View currently being requested. `0` for general setting
173  */
174  'public' => apply_filters( 'gravityview_direct_access', gravityview()->plugin->is_compatible(), 0 ),
175  'show_ui' => gravityview()->plugin->is_compatible(),
176  'show_in_menu' => gravityview()->plugin->is_compatible(),
177  'show_in_nav_menus' => true,
178  'show_in_admin_bar' => true,
179  'menu_position' => 17,
180  'menu_icon' => '',
181  'can_export' => true,
182  /**
183  * @filter `gravityview_has_archive` Enable Custom Post Type archive?
184  * @since 1.7.3
185  * @param boolean False: don't have frontend archive; True: yes, have archive. Default: false
186  */
187  'has_archive' => apply_filters( 'gravityview_has_archive', false ),
188  'exclude_from_search' => true,
189  'rewrite' => array(
190  /**
191  * @filter `gravityview_slug` Modify the url part for a View.
192  * @see https://docs.gravityview.co/article/62-changing-the-view-slug
193  * @param string $slug The slug shown in the URL
194  */
195  'slug' => apply_filters( 'gravityview_slug', 'view' ),
196 
197  /**
198  * @filter `gravityview/post_type/with_front` Should the permalink structure
199  * be prepended with the front base.
200  * (example: if your permalink structure is /blog/, then your links will be: false->/view/, true->/blog/view/).
201  * Defaults to true.
202  * @see https://codex.wordpress.org/Function_Reference/register_post_type
203  * @since 2.0
204  * @param bool $with_front
205  */
206  'with_front' => apply_filters( 'gravityview/post_type/with_front', true ),
207  ),
208  'capability_type' => 'gravityview',
209  'map_meta_cap' => true,
210  );
211 
212  register_post_type( 'gravityview', $args );
213  }
214 
215  /**
216  * Add extra rewrite endpoints.
217  *
218  * @return void
219  */
220  public static function add_rewrite_endpoint() {
221  /**
222  * CSV.
223  */
224  global $wp_rewrite;
225 
226  $slug = apply_filters( 'gravityview_slug', 'view' );
227  $rule = array( sprintf( '%s/([^/]+)/csv/?', $slug ), 'index.php?gravityview=$matches[1]&csv=1', 'top' );
228 
229  add_filter( 'query_vars', function( $query_vars ) {
230  $query_vars[] = 'csv';
231  return $query_vars;
232  } );
233 
234  if ( ! isset( $wp_rewrite->extra_rules_top[ $rule[0] ] ) ) {
235  call_user_func_array( 'add_rewrite_rule', $rule );
236  }
237  }
238 
239  /**
240  * A renderer filter for the View post type content.
241  *
242  * @param string $content Should be empty, as we don't store anything there.
243  *
244  * @return string $content The view content as output by the renderers.
245  */
246  public static function content( $content ) {
247  $request = gravityview()->request;
248 
249  // Plugins may run through the content in the header. WP SEO does this for its OpenGraph functionality.
250  if ( ! defined( 'DOING_GRAVITYVIEW_TESTS' ) ) {
251  if ( ! did_action( 'loop_start' ) ) {
252  gravityview()->log->debug( 'Not processing yet: loop_start hasn\'t run yet. Current action: {action}', array( 'action' => current_filter() ) );
253  return $content;
254  }
255 
256  // We don't want this filter to run infinite loop on any post content fields
257  remove_filter( 'the_content', array( __CLASS__, __METHOD__ ) );
258  }
259 
260  /**
261  * This is not a View. Bail.
262  *
263  * Shortcodes and oEmbeds and whatnot will be handled
264  * elsewhere.
265  */
266  if ( ! $view = $request->is_view() ) {
267  return $content;
268  }
269 
270  /**
271  * Check permissions.
272  */
273  while ( $error = $view->can_render( null, $request ) ) {
274  if ( ! is_wp_error( $error ) )
275  break;
276 
277  switch ( str_replace( 'gravityview/', '', $error->get_error_code() ) ) {
278  case 'post_password_required':
279  return get_the_password_form( $view->ID );
280  case 'no_form_attached':
281 
282  gravityview()->log->error( 'View #{view_id} cannot render: {error_code} {error_message}', array( 'error_code' => $error->get_error_code(), 'error_message' => $error->get_error_message() ) );
283 
284  /**
285  * This View has no data source. There's nothing to show really.
286  * ...apart from a nice message if the user can do anything about it.
287  */
288  if ( \GVCommon::has_cap( array( 'edit_gravityviews', 'edit_gravityview' ), $view->ID ) ) {
289  return __( sprintf( 'This View is not configured properly. Start by <a href="%s">selecting a form</a>.', esc_url( get_edit_post_link( $view->ID, false ) ) ), 'gravityview' );
290  }
291  break;
292  case 'in_trash':
293 
294  if ( \GVCommon::has_cap( array( 'edit_gravityviews', 'edit_gravityview' ), $view->ID ) ) {
295  return __( sprintf( 'This View is in the Trash. You can <a href="%s">restore the View here</a>.', esc_url( get_edit_post_link( $view->ID, false ) ) ), 'gravityview' );
296  }
297 
298  return ''; // Do not show
299  break;
300  case 'no_direct_access':
301  case 'embed_only':
302  case 'not_public':
303  default:
304  gravityview()->log->notice( 'View #{view_id} cannot render: {error_code} {error_message}', array( 'error_code' => $error->get_error_code(), 'error_message' => $error->get_error_message() ) );
305  return __( 'You are not allowed to view this content.', 'gravityview' );
306  }
307 
308  return $content;
309  }
310 
311  $is_admin_and_can_view = $view->settings->get( 'admin_show_all_statuses' ) && \GVCommon::has_cap('gravityview_moderate_entries', $view->ID );
312 
313  /**
314  * Editing a single entry.
315  */
316  if ( $entry = $request->is_edit_entry( $view->form ? $view->form->ID : 0 ) ) {
317  if ( $entry['status'] != 'active' ) {
318  gravityview()->log->notice( 'Entry ID #{entry_id} is not active', array( 'entry_id' => $entry->ID ) );
319  return __( 'You are not allowed to view this content.', 'gravityview' );
320  }
321 
322  if ( apply_filters( 'gravityview_custom_entry_slug', false ) && $entry->slug != get_query_var( \GV\Entry::get_endpoint_name() ) ) {
323  gravityview()->log->error( 'Entry ID #{entry_id} was accessed by a bad slug', array( 'entry_id' => $entry->ID ) );
324  return __( 'You are not allowed to view this content.', 'gravityview' );
325  }
326 
327  if ( $view->settings->get( 'show_only_approved' ) && ! $is_admin_and_can_view ) {
329  gravityview()->log->error( 'Entry ID #{entry_id} is not approved for viewing', array( 'entry_id' => $entry->ID ) );
330  return __( 'You are not allowed to view this content.', 'gravityview' );
331  }
332  }
333 
334  $renderer = new Edit_Entry_Renderer();
335  return $renderer->render( $entry, $view, $request );
336 
337  /**
338  * Viewing a single entry.
339  */
340  } else if ( $entry = $request->is_entry( $view->form ? $view->form->ID : 0 ) ) {
341 
342  $entryset = $entry->is_multi() ? $entry->entries : array( $entry );
343 
344  $custom_slug = apply_filters( 'gravityview_custom_entry_slug', false );
345  $ids = explode( ',', get_query_var( \GV\Entry::get_endpoint_name() ) );
346 
347  $show_only_approved = $view->settings->get( 'show_only_approved' );
348 
349  foreach ( $entryset as $e ) {
350 
351  if ( 'active' !== $e['status'] ) {
352  gravityview()->log->notice( 'Entry ID #{entry_id} is not active', array( 'entry_id' => $e->ID ) );
353  return __( 'You are not allowed to view this content.', 'gravityview' );
354  }
355 
356  if ( $custom_slug && ! in_array( $e->slug, $ids ) ) {
357  gravityview()->log->error( 'Entry ID #{entry_id} was accessed by a bad slug', array( 'entry_id' => $e->ID ) );
358  return __( 'You are not allowed to view this content.', 'gravityview' );
359  }
360 
361  if ( $show_only_approved && ! $is_admin_and_can_view ) {
363  gravityview()->log->error( 'Entry ID #{entry_id} is not approved for viewing', array( 'entry_id' => $e->ID ) );
364  return __( 'You are not allowed to view this content.', 'gravityview' );
365  }
366  }
367 
368  $error = \GVCommon::check_entry_display( $e->as_entry(), $view );
369 
370  if ( is_wp_error( $error ) ) {
371  gravityview()->log->error( 'Entry ID #{entry_id} is not approved for viewing: {message}', array( 'entry_id' => $e->ID, 'message' => $error->get_error_message() ) );
372  return __( 'You are not allowed to view this content.', 'gravityview' );
373  }
374  }
375 
376  $renderer = new Entry_Renderer();
377  return $renderer->render( $entry, $view, $request );
378  }
379 
380  /**
381  * Plain old View.
382  */
383  $renderer = new View_Renderer();
384  return $renderer->render( $view, $request );
385  }
386 
387  /**
388  * Checks whether this view can be accessed or not.
389  *
390  * @param string[] $context The context we're asking for access from.
391  * Can any and as many of one of:
392  * edit An edit context.
393  * single A single context.
394  * cpt The custom post type single page acessed.
395  * shortcode Embedded as a shortcode.
396  * oembed Embedded as an oEmbed.
397  * rest A REST call.
398  * @param \GV\Request $request The request
399  *
400  * @return bool|\WP_Error An error if this View shouldn't be rendered here.
401  */
402  public function can_render( $context = null, $request = null ) {
403  if ( ! $request ) {
404  $request = gravityview()->request;
405  }
406 
407  if ( ! is_array( $context ) ) {
408  $context = array();
409  }
410 
411  /**
412  * @filter `gravityview/view/can_render` Whether the view can be rendered or not.
413  * @param bool|\WP_Error $result The result. Default: null.
414  * @param \GV\View $view The view.
415  * @param string[] $context See \GV\View::can_render
416  * @param \GV\Request $request The request.
417  */
418  if ( ! is_null( $result = apply_filters( 'gravityview/view/can_render', null, $this, $context, $request ) ) ) {
419  return $result;
420  }
421 
422  if ( in_array( 'rest', $context ) ) {
423  // REST
424  if ( gravityview()->plugin->settings->get( 'rest_api' ) === '1' && $this->settings->get( 'rest_disable' ) === '1' ) {
425  return new \WP_Error( 'gravityview/rest_disabled' );
426  } elseif ( gravityview()->plugin->settings->get( 'rest_api' ) !== '1' && $this->settings->get( 'rest_enable' ) !== '1' ) {
427  return new \WP_Error( 'gravityview/rest_disabled' );
428  }
429  }
430 
431  if ( in_array( 'csv', $context ) ) {
432  if ( $this->settings->get( 'csv_enable' ) !== '1' ) {
433  return new \WP_Error( 'gravityview/csv_disabled', 'The CSV endpoint is not enabled for this View' );
434  }
435  }
436 
437  /**
438  * This View is password protected. Nothing to do here.
439  */
440  if ( post_password_required( $this->ID ) ) {
441  gravityview()->log->notice( 'Post password is required for View #{view_id}', array( 'view_id' => $this->ID ) );
442  return new \WP_Error( 'gravityview/post_password_required' );
443  }
444 
445  if ( ! $this->form ) {
446  gravityview()->log->notice( 'View #{id} has no form attached to it.', array( 'id' => $this->ID ) );
447  return new \WP_Error( 'gravityview/no_form_attached' );
448  }
449 
450  if ( ! in_array( 'shortcode', $context ) ) {
451  /**
452  * Is this View directly accessible via a post URL?
453  *
454  * @see https://codex.wordpress.org/Function_Reference/register_post_type#public
455  */
456 
457  /**
458  * @filter `gravityview_direct_access` Should Views be directly accessible, or only visible using the shortcode?
459  * @deprecated
460  * @param[in,out] boolean `true`: allow Views to be accessible directly. `false`: Only allow Views to be embedded. Default: `true`
461  * @param int $view_id The ID of the View currently being requested. `0` for general setting
462  */
463  $direct_access = apply_filters( 'gravityview_direct_access', true, $this->ID );
464 
465  /**
466  * @filter `gravityview/request/output/direct` Should this View be directly accessbile?
467  * @since 2.0
468  * @param[in,out] boolean Accessible or not. Default: accessbile.
469  * @param \GV\View $view The View we're trying to directly render here.
470  * @param \GV\Request $request The current request.
471  */
472  if ( ! apply_filters( 'gravityview/view/output/direct', $direct_access, $this, $request ) ) {
473  return new \WP_Error( 'gravityview/no_direct_access' );
474  }
475 
476  /**
477  * Is this View an embed-only View? If so, don't allow rendering here,
478  * as this is a direct request.
479  */
480  if ( $this->settings->get( 'embed_only' ) && ! \GVCommon::has_cap( 'read_private_gravityviews' ) ) {
481  return new \WP_Error( 'gravityview/embed_only' );
482  }
483  }
484 
485  /** Private, pending, draft, etc. */
486  $public_states = get_post_stati( array( 'public' => true ) );
487  if ( ! in_array( $this->post_status, $public_states ) && ! \GVCommon::has_cap( 'read_gravityview', $this->ID ) ) {
488  gravityview()->log->notice( 'The current user cannot access this View #{view_id}', array( 'view_id' => $this->ID ) );
489  return new \WP_Error( 'gravityview/not_public' );
490  }
491 
492  return true;
493  }
494 
495  /**
496  * Get joins associated with a view
497  *
498  * @param \WP_Post $post GravityView CPT to get joins for
499  *
500  * @api
501  * @since 2.0.11
502  *
503  * @return \GV\Join[] Array of \GV\Join instances
504  */
505  public static function get_joins( $post ) {
506  $joins = array();
507 
508  if ( ! gravityview()->plugin->supports( Plugin::FEATURE_JOINS ) ) {
509  gravityview()->log->error( 'Cannot get joined forms; joins feature not supported.' );
510  return $joins;
511  }
512 
513  if ( ! $post || 'gravityview' !== get_post_type( $post ) ) {
514  gravityview()->log->error( 'Only "gravityview" post types can be \GV\View instances.' );
515  return $joins;
516  }
517 
518  $joins_meta = get_post_meta( $post->ID, '_gravityview_form_joins', true );
519 
520  if ( empty( $joins_meta ) ) {
521  return $joins;
522  }
523 
524  foreach ( $joins_meta as $meta ) {
525  if ( ! is_array( $meta ) || count( $meta ) != 4 ) {
526  continue;
527  }
528 
529  list( $join, $join_column, $join_on, $join_on_column ) = $meta;
530 
531  $join = GF_Form::by_id( $join );
532  $join_on = GF_Form::by_id( $join_on );
533 
534  $join_column = is_numeric( $join_column ) ? GF_Field::by_id( $join, $join_column ) : Internal_Field::by_id( $join_column );
535  $join_on_column = is_numeric( $join_on_column ) ? GF_Field::by_id( $join_on, $join_on_column ) : Internal_Field::by_id( $join_on_column );
536 
537  $joins [] = new Join( $join, $join_column, $join_on, $join_on_column );
538  }
539 
540  return $joins;
541  }
542 
543  /**
544  * Get joined forms associated with a view
545  * In no particular order.
546  *
547  * @since 2.0.11
548  *
549  * @api
550  * @since 2.0
551  * @param int $post_id ID of the View
552  *
553  * @return \GV\GF_Form[] Array of \GV\GF_Form instances
554  */
555  public static function get_joined_forms( $post_id ) {
556  $forms = array();
557 
558  if ( ! gravityview()->plugin->supports( Plugin::FEATURE_JOINS ) ) {
559  gravityview()->log->error( 'Cannot get joined forms; joins feature not supported.' );
560  return $forms;
561  }
562 
563  if ( ! $post_id || ! gravityview()->plugin->supports( Plugin::FEATURE_JOINS ) ) {
564  return $forms;
565  }
566 
567  if ( empty( $post_id ) ) {
568  gravityview()->log->error( 'Cannot get joined forms; $post_id was empty' );
569  return $forms;
570  }
571 
572  $joins_meta = get_post_meta( $post_id, '_gravityview_form_joins', true );
573 
574  if ( empty( $joins_meta ) ) {
575  return $forms;
576  }
577 
578  foreach ( $joins_meta as $meta ) {
579  if ( ! is_array( $meta ) || count( $meta ) != 4 ) {
580  continue;
581  }
582 
583  list( $join, $join_column, $join_on, $join_on_column ) = $meta;
584 
585  if ( $form = GF_Form::by_id( $join_on ) ) {
586  $forms[ $join_on ] = $form;
587  }
588 
589  if ( $form = GF_Form::by_id( $join ) ) {
590  $forms[ $join ] = $form;
591  }
592  }
593 
594  return $forms;
595  }
596 
597  /**
598  * Get unions associated with a view
599  *
600  * @param \WP_Post $post GravityView CPT to get unions for
601  *
602  * @api
603  * @since 2.2.2
604  *
605  * @return \GV\Field[][] Array of unions (see self::$unions)
606  */
607  public static function get_unions( $post ) {
608  $unions = array();
609 
610  if ( ! gravityview()->plugin->supports( Plugin::FEATURE_UNIONS ) ) {
611  gravityview()->log->error( 'Cannot get unions; unions feature not supported.' );
612  return $unions;
613  }
614 
615  if ( ! $post || 'gravityview' !== get_post_type( $post ) ) {
616  gravityview()->log->error( 'Only "gravityview" post types can be \GV\View instances.' );
617  return $unions;
618  }
619 
620  $fields = get_post_meta( $post->ID, '_gravityview_directory_fields', true );
621 
622  if ( empty( $fields ) ) {
623  return $unions;
624  }
625 
626  foreach ( $fields as $location => $_fields ) {
627  if ( strpos( $location, 'directory_' ) !== 0 ) {
628  continue;
629  }
630 
631  foreach ( $_fields as $field ) {
632  if ( ! empty( $field['unions'] ) ) {
633  foreach ( $field['unions'] as $form_id => $field_id ) {
634  if ( ! isset( $unions[ $form_id ] ) ) {
635  $unions[ $form_id ] = array();
636  }
637 
638  $unions[ $form_id ][ $field['id'] ] =
639  is_numeric( $field_id ) ? \GV\GF_Field::by_id( \GV\GF_Form::by_id( $form_id ), $field_id ) : \GV\Internal_Field::by_id( $field_id );
640  }
641  }
642  }
643 
644  break;
645  }
646 
647  // @todo We'll probably need to backfill null unions
648 
649  return $unions;
650  }
651 
652  /**
653  * Construct a \GV\View instance from a \WP_Post.
654  *
655  * @param \WP_Post $post The \WP_Post instance to wrap.
656  *
657  * @api
658  * @since 2.0
659  * @return \GV\View|null An instance around this \WP_Post if valid, null otherwise.
660  */
661  public static function from_post( $post ) {
662 
663  if ( ! $post || 'gravityview' !== get_post_type( $post ) ) {
664  gravityview()->log->error( 'Only gravityview post types can be \GV\View instances.' );
665  return null;
666  }
667 
668  if ( $view = Utils::get( self::$cache, "View::from_post:{$post->ID}" ) ) {
669  /**
670  * @filter `gravityview/view/get` Override View.
671  * @param \GV\View $view The View instance pointer.
672  * @since 2.1
673  */
674  do_action_ref_array( 'gravityview/view/get', array( &$view ) );
675 
676  return $view;
677  }
678 
679  $view = new self();
680  $view->post = $post;
681 
682  /** Get connected form. */
683  $view->form = GF_Form::by_id( $view->_gravityview_form_id );
684  if ( ! $view->form ) {
685  gravityview()->log->error( 'View #{view_id} tried attaching non-existent Form #{form_id} to it.', array(
686  'view_id' => $view->ID,
687  'form_id' => $view->_gravityview_form_id ? : 0,
688  ) );
689  }
690 
691  $view->joins = $view::get_joins( $post );
692 
693  $view->unions = $view::get_unions( $post );
694 
695  /**
696  * @filter `gravityview/configuration/fields` Filter the View fields' configuration array.
697  * @since 1.6.5
698  *
699  * @deprecated Use `gravityview/view/configuration/fields` or `gravityview/view/fields` filters.
700  *
701  * @param $fields array Multi-array of fields with first level being the field zones.
702  * @param $view_id int The View the fields are being pulled for.
703  */
704  $configuration = apply_filters( 'gravityview/configuration/fields', (array)$view->_gravityview_directory_fields, $view->ID );
705 
706  /**
707  * @filter `gravityview/view/configuration/fields` Filter the View fields' configuration array.
708  * @since 2.0
709  *
710  * @param array $fields Multi-array of fields with first level being the field zones.
711  * @param \GV\View $view The View the fields are being pulled for.
712  */
713  $configuration = apply_filters( 'gravityview/view/configuration/fields', $configuration, $view );
714 
715  /**
716  * @filter `gravityview/view/fields` Filter the Field Collection for this View.
717  * @since 2.0
718  *
719  * @param \GV\Field_Collection $fields A collection of fields.
720  * @param \GV\View $view The View the fields are being pulled for.
721  */
722  $view->fields = apply_filters( 'gravityview/view/fields', Field_Collection::from_configuration( $configuration ), $view );
723 
724  /**
725  * @filter `gravityview/view/configuration/widgets` Filter the View widgets' configuration array.
726  * @since 2.0
727  *
728  * @param array $fields Multi-array of widgets with first level being the field zones.
729  * @param \GV\View $view The View the widgets are being pulled for.
730  */
731  $configuration = apply_filters( 'gravityview/view/configuration/widgets', (array)$view->_gravityview_directory_widgets, $view );
732 
733  /**
734  * @filter `gravityview/view/widgets` Filter the Widget Collection for this View.
735  * @since 2.0
736  *
737  * @param \GV\Widget_Collection $widgets A collection of widgets.
738  * @param \GV\View $view The View the widgets are being pulled for.
739  */
740  $view->widgets = apply_filters( 'gravityview/view/widgets', Widget_Collection::from_configuration( $configuration ), $view );
741 
742  /** View configuration. */
743  $view->settings->update( gravityview_get_template_settings( $view->ID ) );
744 
745  /** Add the template name into the settings. */
746  $view->settings->update( array( 'template' => gravityview_get_template_id( $view->ID ) ) );
747 
748  /** View basics. */
749  $view->settings->update( array(
750  'id' => $view->ID,
751  ) );
752 
753  self::$cache[ "View::from_post:{$post->ID}" ] = &$view;
754 
755  /**
756  * @filter `gravityview/view/get` Override View.
757  * @param \GV\View $view The View instance pointer.
758  * @since 2.1
759  */
760  do_action_ref_array( 'gravityview/view/get', array( &$view ) );
761 
762  return $view;
763  }
764 
765  /**
766  * Flush the view cache.
767  *
768  * @param int $view_id The View to reset cache for. Optional. Default: resets everything.
769  *
770  * @internal
771  */
772  public static function _flush_cache( $view_id = null ) {
773  if ( $view_id ) {
774  unset( self::$cache[ "View::from_post:$view_id" ] );
775  return;
776  }
777  self::$cache = array();
778  }
779 
780  /**
781  * Construct a \GV\View instance from a post ID.
782  *
783  * @param int|string $post_id The post ID.
784  *
785  * @api
786  * @since 2.0
787  * @return \GV\View|null An instance around this \WP_Post or null if not found.
788  */
789  public static function by_id( $post_id ) {
790  if ( ! $post_id || ! $post = get_post( $post_id ) ) {
791  return null;
792  }
793  return self::from_post( $post );
794  }
795 
796  /**
797  * Determines if a view exists to begin with.
798  *
799  * @param int|\WP_Post|null $view The WordPress post ID, a \WP_Post object or null for global $post;
800  *
801  * @api
802  * @since 2.0
803  * @return bool Whether the post exists or not.
804  */
805  public static function exists( $view ) {
806  return get_post_type( $view ) == 'gravityview';
807  }
808 
809  /**
810  * ArrayAccess compatibility layer with GravityView_View_Data::$views
811  *
812  * @internal
813  * @deprecated
814  * @since 2.0
815  * @return bool Whether the offset exists or not, limited to GravityView_View_Data::$views element keys.
816  */
817  public function offsetExists( $offset ) {
818  $data_keys = array( 'id', 'view_id', 'form_id', 'template_id', 'atts', 'fields', 'widgets', 'form' );
819  return in_array( $offset, $data_keys );
820  }
821 
822  /**
823  * ArrayAccess compatibility layer with GravityView_View_Data::$views
824  *
825  * Maps the old keys to the new data;
826  *
827  * @internal
828  * @deprecated
829  * @since 2.0
830  *
831  * @return mixed The value of the requested view data key limited to GravityView_View_Data::$views element keys. If offset not found, return null.
832  */
833  public function offsetGet( $offset ) {
834 
835  gravityview()->log->notice( 'This is a \GV\View object should not be accessed as an array.' );
836 
837  if ( ! isset( $this[ $offset ] ) ) {
838  return null;
839  }
840 
841  switch ( $offset ) {
842  case 'id':
843  case 'view_id':
844  return $this->ID;
845  case 'form':
846  return $this->form;
847  case 'form_id':
848  return $this->form ? $this->form->ID : null;
849  case 'atts':
850  return $this->settings->as_atts();
851  case 'template_id':
852  return $this->settings->get( 'template' );
853  case 'widgets':
854  return $this->widgets->as_configuration();
855  }
856 
857  return null;
858  }
859 
860  /**
861  * ArrayAccess compatibility layer with GravityView_View_Data::$views
862  *
863  * @internal
864  * @deprecated
865  * @since 2.0
866  *
867  * @return void
868  */
869  public function offsetSet( $offset, $value ) {
870  gravityview()->log->error( 'The old view data is no longer mutable. This is a \GV\View object should not be accessed as an array.' );
871  }
872 
873  /**
874  * ArrayAccess compatibility layer with GravityView_View_Data::$views
875  *
876  * @internal
877  * @deprecated
878  * @since 2.0
879  * @return void
880  */
881  public function offsetUnset( $offset ) {
882  gravityview()->log->error( 'The old view data is no longer mutable. This is a \GV\View object should not be accessed as an array.' );
883  }
884 
885  /**
886  * Be compatible with the old data object.
887  *
888  * Some external code expects an array (doing things like foreach on this, or array_keys)
889  * so let's return an array in the old format for such cases. Do not use unless using
890  * for back-compatibility.
891  *
892  * @internal
893  * @deprecated
894  * @since 2.0
895  * @return array
896  */
897  public function as_data() {
898  return array(
899  'id' => $this->ID,
900  'view_id' => $this->ID,
901  'form_id' => $this->form ? $this->form->ID : null,
902  'form' => $this->form ? gravityview_get_form( $this->form->ID ) : null,
903  'atts' => $this->settings->as_atts(),
904  'fields' => $this->fields->by_visible( $this )->as_configuration(),
905  'template_id' => $this->settings->get( 'template' ),
906  'widgets' => $this->widgets->as_configuration(),
907  );
908  }
909 
910  /**
911  * Retrieve the entries for the current view and request.
912  *
913  * @param \GV\Request The request. Unused for now.
914  *
915  * @return \GV\Entry_Collection The entries.
916  */
917  public function get_entries( $request = null ) {
918  $entries = new \GV\Entry_Collection();
919  if ( $this->form ) {
920  $parameters = $this->settings->as_atts();
921 
922  /**
923  * Remove multiple sorting before calling legacy filters.
924  * This allows us to fake it till we make it.
925  */
926  if ( ! empty( $parameters['sort_field'] ) && is_array( $parameters['sort_field'] ) ) {
927  $has_multisort = true;
928  $parameters['sort_field'] = reset( $parameters['sort_field'] );
929  if ( ! empty( $parameters['sort_direction'] ) && is_array( $parameters['sort_direction'] ) ) {
930  $parameters['sort_direction'] = reset( $parameters['sort_direction'] );
931  }
932  }
933 
934  /**
935  * @todo: Stop using _frontend and use something like $request->get_search_criteria() instead
936  */
937  $parameters = \GravityView_frontend::get_view_entries_parameters( $parameters, $this->form->ID );
938  $parameters['context_view_id'] = $this->ID;
939  $parameters = \GVCommon::calculate_get_entries_criteria( $parameters, $this->form->ID );
940 
941  if ( $request instanceof REST\Request ) {
942  $atts = $this->settings->as_atts();
943  $paging_parameters = wp_parse_args( $request->get_paging(), array(
944  'paging' => array( 'page_size' => $atts['page_size'] ),
945  ) );
946  $parameters['paging'] = $paging_parameters['paging'];
947  }
948 
949  $page = Utils::get( $parameters['paging'], 'current_page' ) ?
950  : ( ( ( $parameters['paging']['offset'] - $this->settings->get( 'offset' ) ) / $parameters['paging']['page_size'] ) + 1 );
951 
952  /**
953  * Cleanup duplicate field_filter parameters to simplify the query.
954  */
955  $unique_field_filters = array();
956  foreach ( $parameters['search_criteria']['field_filters'] as $key => $filter ) {
957  if ( 'mode' === $key ) {
958  $unique_field_filters['mode'] = $filter;
959  } else if ( ! in_array( $filter, $unique_field_filters ) ) {
960  $unique_field_filters[] = $filter;
961  }
962  }
963  $parameters['search_criteria']['field_filters'] = $unique_field_filters;
964 
965  if ( ! empty( $parameters['search_criteria']['field_filters'] ) ) {
966  gravityview()->log->notice( 'search_criteria/field_filters is not empty, third-party code may be using legacy search_criteria filters.' );
967  }
968 
969  if ( gravityview()->plugin->supports( Plugin::FEATURE_GFQUERY ) ) {
970 
971  $query_class = $this->get_query_class();
972 
973  /** @var \GF_Query $query */
974  $query = new $query_class( $this->form->ID, $parameters['search_criteria'], $parameters['sorting'] );
975 
976  /**
977  * Apply multisort.
978  */
979  if ( ! empty( $has_multisort ) ) {
980  $atts = $this->settings->as_atts();
981 
982  $view_setting_sort_field_ids = \GV\Utils::get( $atts, 'sort_field', array() );
983  $view_setting_sort_directions = \GV\Utils::get( $atts, 'sort_direction', array() );
984 
985  $has_sort_query_param = ! empty( $_GET['sort'] ) && is_array( $_GET['sort'] );
986 
987  if( $has_sort_query_param ) {
988  $has_sort_query_param = array_filter( array_values( $_GET['sort'] ) );
989  }
990 
991  if ( $this->settings->get( 'sort_columns' ) && $has_sort_query_param ) {
992  $sort_field_ids = array_keys( $_GET['sort'] );
993  $sort_directions = array_values( $_GET['sort'] );
994  } else {
995  $sort_field_ids = $view_setting_sort_field_ids;
996  $sort_directions = $view_setting_sort_directions;
997  }
998 
999  $skip_first = false;
1000 
1001  foreach ( (array) $sort_field_ids as $key => $sort_field_id ) {
1002 
1003  if ( ! $skip_first && ! $has_sort_query_param ) {
1004  $skip_first = true; // Skip the first one, it's already in the query
1005  continue;
1006  }
1007 
1008  $sort_field_id = \GravityView_frontend::_override_sorting_id_by_field_type( $sort_field_id, $this->form->ID );
1009  $sort_direction = strtoupper( \GV\Utils::get( $sort_directions, $key, 'ASC' ) );
1010 
1011  if ( ! empty( $sort_field_id ) ) {
1012  $order = new \GF_Query_Column( $sort_field_id, $this->form->ID );
1013  if ( \GVCommon::is_field_numeric( $this->form->ID, $sort_field_id ) ) {
1014  $order = \GF_Query_Call::CAST( $order, defined( 'GF_Query::TYPE_DECIMAL' ) ? \GF_Query::TYPE_DECIMAL : \GF_Query::TYPE_SIGNED );
1015  }
1016 
1017  $query->order( $order, $sort_direction );
1018  }
1019  }
1020  }
1021 
1022  /**
1023  * Merge time subfield sorts.
1024  */
1025  add_filter( 'gform_gf_query_sql', $gf_query_timesort_sql_callback = function( $sql ) use ( &$query ) {
1026  $q = $query->_introspect();
1027  $orders = array();
1028 
1029  $merged_time = false;
1030 
1031  foreach ( $q['order'] as $oid => $order ) {
1032  if ( $order[0] instanceof \GF_Query_Column ) {
1033  $column = $order[0];
1034  } else if ( $order[0] instanceof \GF_Query_Call ) {
1035  if ( count( $order[0]->columns ) != 1 || ! $order[0]->columns[0] instanceof \GF_Query_Column ) {
1036  $orders[ $oid ] = $order;
1037  continue; // Need something that resembles a single sort
1038  }
1039  $column = $order[0]->columns[0];
1040  }
1041 
1042  if ( ( ! $field = \GFAPI::get_field( $column->source, $column->field_id ) ) || $field->type !== 'time' ) {
1043  $orders[ $oid ] = $order;
1044  continue; // Not a time field
1045  }
1046 
1047  if ( ! class_exists( '\GV\Mocks\GF_Query_Call_TIMESORT' ) ) {
1048  require_once gravityview()->plugin->dir( 'future/_mocks.timesort.php' );
1049  }
1050 
1051  $orders[ $oid ] = array(
1052  new \GV\Mocks\GF_Query_Call_TIMESORT( 'timesort', array( $column, $sql ) ),
1053  $order[1] // Mock it!
1054  );
1055 
1056  $merged_time = true;
1057  }
1058 
1059  if ( $merged_time ) {
1060  /**
1061  * ORDER again.
1062  */
1063  if ( ! empty( $orders ) && $_orders = $query->_order_generate( $orders ) ) {
1064  $sql['order'] = 'ORDER BY ' . implode( ', ', $_orders );
1065  }
1066  }
1067 
1068  return $sql;
1069  } );
1070 
1071  $query->limit( $parameters['paging']['page_size'] )
1072  ->offset( ( ( $page - 1 ) * $parameters['paging']['page_size'] ) + $this->settings->get( 'offset' ) );
1073 
1074  /**
1075  * Any joins?
1076  */
1077  if ( gravityview()->plugin->supports( Plugin::FEATURE_JOINS ) && count( $this->joins ) ) {
1078 
1079  $is_admin_and_can_view = $this->settings->get( 'admin_show_all_statuses' ) && \GVCommon::has_cap( 'gravityview_moderate_entries', $this->ID );
1080 
1081  foreach ( $this->joins as $join ) {
1082  $query = $join->as_query_join( $query );
1083 
1084  if ( $this->settings->get( 'multiple_forms_disable_null_joins' ) ) {
1085 
1086  // Disable NULL outputs
1087  $condition = new \GF_Query_Condition(
1088  new \GF_Query_Column( $join->join_on_column->ID, $join->join_on->ID ),
1089  \GF_Query_Condition::NEQ,
1090  new \GF_Query_Literal( '' )
1091  );
1092 
1093  $query_parameters = $query->_introspect();
1094 
1095  $query->where( \GF_Query_Condition::_and( $query_parameters['where'], $condition ) );
1096  }
1097 
1098  /**
1099  * This is a temporary stub filter, until GF_Query supports NULL conditions.
1100  * Do not use! This filter will be removed.
1101  */
1102  if ( defined( 'GF_Query_Condition::NULL' ) ) {
1103  $is_null_condition_native = true;
1104  } else {
1105  $is_null_condition_class = apply_filters( 'gravityview/query/is_null_condition', null );
1106  $is_null_condition_native = false;
1107  }
1108 
1109  // Filter to active entries only
1110  $condition = new \GF_Query_Condition(
1111  new \GF_Query_Column( 'status', $join->join_on->ID ),
1112  \GF_Query_Condition::EQ,
1113  new \GF_Query_Literal( 'active' )
1114  );
1115 
1116  if ( $is_null_condition_native ) {
1117  $condition = \GF_Query_Condition::_or( $condition, new \GF_Query_Condition(
1118  new \GF_Query_Column( 'status', $join->join_on->ID ),
1119  \GF_Query_Condition::IS,
1120  \GF_Query_Condition::NULL
1121  ) );
1122  } else if ( ! is_null( $is_null_condition_class ) ) {
1123  $condition = \GF_Query_Condition::_or( $condition, new $is_null_condition_class(
1124  new \GF_Query_Column( 'status', $join->join_on->ID )
1125  ) );
1126  }
1127 
1128  $q = $query->_introspect();
1129  $query->where( \GF_Query_Condition::_and( $q['where'], $condition ) );
1130 
1131  if ( $this->settings->get( 'show_only_approved' ) && ! $is_admin_and_can_view ) {
1132 
1133  // Show only approved joined entries
1134  $condition = new \GF_Query_Condition(
1135  new \GF_Query_Column( \GravityView_Entry_Approval::meta_key, $join->join_on->ID ),
1136  \GF_Query_Condition::EQ,
1137  new \GF_Query_Literal( \GravityView_Entry_Approval_Status::APPROVED )
1138  );
1139 
1140  if ( $is_null_condition_native ) {
1141  $condition = \GF_Query_Condition::_or( $condition, new \GF_Query_Condition(
1142  new \GF_Query_Column( \GravityView_Entry_Approval::meta_key, $join->join_on->ID ),
1143  \GF_Query_Condition::IS,
1144  \GF_Query_Condition::NULL
1145  ) );
1146  } else if ( ! is_null( $is_null_condition_class ) ) {
1147  $condition = \GF_Query_Condition::_or( $condition, new $is_null_condition_class(
1148  new \GF_Query_Column( \GravityView_Entry_Approval::meta_key, $join->join_on->ID )
1149  ) );
1150  }
1151 
1152  $query_parameters = $query->_introspect();
1153 
1154  $query->where( \GF_Query_Condition::_and( $query_parameters['where'], $condition ) );
1155  }
1156  }
1157 
1158  /**
1159  * Unions?
1160  */
1161  } else if ( gravityview()->plugin->supports( Plugin::FEATURE_UNIONS ) && count( $this->unions ) ) {
1162  $query_parameters = $query->_introspect();
1163 
1164  $unions_sql = array();
1165 
1166  /**
1167  * @param \GF_Query_Condition $condition
1168  * @param array $fields
1169  * @param $recurse
1170  *
1171  * @return \GF_Query_Condition
1172  */
1173  $where_union_substitute = function( $condition, $fields, $recurse ) {
1174  if ( $condition->expressions ) {
1175  $conditions = array();
1176 
1177  foreach ( $condition->expressions as $_condition ) {
1178  $conditions[] = $recurse( $_condition, $fields, $recurse );
1179  }
1180 
1181  return call_user_func_array(
1182  array( '\GF_Query_Condition', $condition->operator == 'AND' ? '_and' : '_or' ),
1183  $conditions
1184  );
1185  }
1186 
1187  if ( ! ( $condition->left && $condition->left instanceof \GF_Query_Column ) || ( ! $condition->left->is_entry_column() && ! $condition->left->is_meta_column() ) ) {
1188  return new \GF_Query_Condition(
1189  new \GF_Query_Column( $fields[ $condition->left->field_id ]->ID ),
1190  $condition->operator,
1191  $condition->right
1192  );
1193  }
1194 
1195  return $condition;
1196  };
1197 
1198  foreach ( $this->unions as $form_id => $fields ) {
1199 
1200  // Build a new query for every unioned form
1201  $query_class = $this->get_query_class();
1202 
1203  /** @var \GF_Query|\GF_Patched_Query $q */
1204  $q = new $query_class( $form_id );
1205 
1206  // Copy the WHERE clauses but substitute the field_ids to the respective ones
1207  $q->where( $where_union_substitute( $query_parameters['where'], $fields, $where_union_substitute ) );
1208 
1209  // Copy the ORDER clause and substitute the field_ids to the respective ones
1210  foreach ( $query_parameters['order'] as $order ) {
1211  list( $column, $_order ) = $order;
1212 
1213  if ( $column && $column instanceof \GF_Query_Column ) {
1214  if ( ! $column->is_entry_column() && ! $column->is_meta_column() ) {
1215  $column = new \GF_Query_Column( $fields[ $column->field_id ]->ID );
1216  }
1217 
1218  $q->order( $column, $_order );
1219  }
1220  }
1221 
1222  add_filter( 'gform_gf_query_sql', $gf_query_sql_callback = function( $sql ) use ( &$unions_sql ) {
1223  // Remove SQL_CALC_FOUND_ROWS as it's not needed in UNION clauses
1224  $select = 'UNION ALL ' . str_replace( 'SQL_CALC_FOUND_ROWS ', '', $sql['select'] );
1225 
1226  // Record the SQL
1227  $unions_sql[] = array(
1228  // Remove columns, we'll rebuild them
1229  'select' => preg_replace( '#DISTINCT (.*)#', 'DISTINCT ', $select ),
1230  'from' => $sql['from'],
1231  'join' => $sql['join'],
1232  'where' => $sql['where'],
1233  // Remove order and limit
1234  );
1235 
1236  // Return empty query, no need to call the database
1237  return array();
1238  } );
1239 
1240  do_action_ref_array( 'gravityview/view/query', array( &$q, $this, $request ) );
1241 
1242  $q->get(); // Launch
1243 
1244  remove_filter( 'gform_gf_query_sql', $gf_query_sql_callback );
1245  }
1246 
1247  add_filter( 'gform_gf_query_sql', $gf_query_sql_callback = function( $sql ) use ( $unions_sql ) {
1248  // Remove SQL_CALC_FOUND_ROWS as it's not needed in UNION clauses
1249  $sql['select'] = str_replace( 'SQL_CALC_FOUND_ROWS ', '', $sql['select'] );
1250 
1251  // Remove columns, we'll rebuild them
1252  preg_match( '#DISTINCT (`[motc]\d+`.`.*?`)#', $sql['select'], $select_match );
1253  $sql['select'] = preg_replace( '#DISTINCT (.*)#', 'DISTINCT ', $sql['select'] );
1254 
1255  $unions = array();
1256 
1257  // Transform selected columns to shared alias names
1258  $column_to_alias = function( $column ) {
1259  $column = str_replace( '`', '', $column );
1260  return '`' . str_replace( '.', '_', $column ) . '`';
1261  };
1262 
1263  // Add all the order columns into the selects, so we can order by the whole union group
1264  preg_match_all( '#(`[motc]\d+`.`.*?`)#', $sql['order'], $order_matches );
1265 
1266  $columns = array(
1267  sprintf( '%s AS %s', $select_match[1], $column_to_alias( $select_match[1] ) )
1268  );
1269 
1270  foreach ( array_slice( $order_matches, 1 ) as $match ) {
1271  $columns[] = sprintf( '%s AS %s', $match[0], $column_to_alias( $match[0] ) );
1272 
1273  // Rewrite the order columns to the shared aliases
1274  $sql['order'] = str_replace( $match[0], $column_to_alias( $match[0] ), $sql['order'] );
1275  }
1276 
1277  $columns = array_unique( $columns );
1278 
1279  // Add the columns to every UNION
1280  foreach ( $unions_sql as $union_sql ) {
1281  $union_sql['select'] .= implode( ', ', $columns );
1282  $unions []= implode( ' ', $union_sql );
1283  }
1284 
1285  // Add the columns to the main SELECT, but only grab the entry id column
1286  $sql['select'] = 'SELECT SQL_CALC_FOUND_ROWS t1_id FROM (' . $sql['select'] . implode( ', ', $columns );
1287  $sql['order'] = implode( ' ', $unions ) . ') AS u ' . $sql['order'];
1288 
1289  return $sql;
1290  } );
1291  }
1292 
1293  /**
1294  * @action `gravityview/view/query` Override the \GF_Query before the get() call.
1295  * @param \GF_Query $query The current query object reference
1296  * @param \GV\View $this The current view object
1297  * @param \GV\Request $request The request object
1298  */
1299  do_action_ref_array( 'gravityview/view/query', array( &$query, $this, $request ) );
1300 
1301  gravityview()->log->debug( 'GF_Query parameters: ', array( 'data' => Utils::gf_query_debug( $query ) ) );
1302 
1303  /**
1304  * Map from Gravity Forms entries arrays to an Entry_Collection.
1305  */
1306  if ( count( $this->joins ) ) {
1307  foreach ( $query->get() as $entry ) {
1308  $entries->add(
1309  Multi_Entry::from_entries( array_map( '\GV\GF_Entry::from_entry', $entry ) )
1310  );
1311  }
1312  } else {
1313  array_map( array( $entries, 'add' ), array_map( '\GV\GF_Entry::from_entry', $query->get() ) );
1314  }
1315 
1316  if ( isset( $gf_query_sql_callback ) ) {
1317  remove_action( 'gform_gf_query_sql', $gf_query_sql_callback );
1318  }
1319 
1320  if ( isset( $gf_query_timesort_sql_callback ) ) {
1321  remove_action( 'gform_gf_query_sql', $gf_query_timesort_sql_callback );
1322  }
1323 
1324  /**
1325  * Add total count callback.
1326  */
1327  $entries->add_count_callback( function() use ( $query ) {
1328  return $query->total_found;
1329  } );
1330  } else {
1331  $entries = $this->form->entries
1332  ->filter( \GV\GF_Entry_Filter::from_search_criteria( $parameters['search_criteria'] ) )
1333  ->offset( $this->settings->get( 'offset' ) )
1334  ->limit( $parameters['paging']['page_size'] )
1335  ->page( $page );
1336 
1337  if ( ! empty( $parameters['sorting'] ) && is_array( $parameters['sorting'] && ! isset( $parameters['sorting']['key'] ) ) ) {
1338  // Pluck off multisort arrays
1339  $parameters['sorting'] = $parameters['sorting'][0];
1340  }
1341 
1342  if ( ! empty( $parameters['sorting'] ) && ! empty( $parameters['sorting']['key'] ) ) {
1343  $field = new \GV\Field();
1344  $field->ID = $parameters['sorting']['key'];
1345  $direction = strtolower( $parameters['sorting']['direction'] ) == 'asc' ? \GV\Entry_Sort::ASC : \GV\Entry_Sort::DESC;
1346  $entries = $entries->sort( new \GV\Entry_Sort( $field, $direction ) );
1347  }
1348  }
1349  }
1350 
1351  /**
1352  * @filter `gravityview/view/entries` Modify the entry fetching filters, sorts, offsets, limits.
1353  * @param \GV\Entry_Collection $entries The entries for this view.
1354  * @param \GV\View $view The view.
1355  * @param \GV\Request $request The request.
1356  */
1357  return apply_filters( 'gravityview/view/entries', $entries, $this, $request );
1358  }
1359 
1360  /**
1361  * Last chance to configure the output.
1362  *
1363  * Used for CSV output, for example.
1364  *
1365  * @return void
1366  */
1367  public static function template_redirect() {
1368  /**
1369  * CSV output.
1370  */
1371  if ( ! get_query_var( 'csv' ) ) {
1372  return;
1373  }
1374 
1375  if ( ! $view = gravityview()->request->is_view() ) {
1376  return;
1377  }
1378 
1379  if ( is_wp_error( $error = $view->can_render( array( 'csv' ) ) ) ) {
1380  gravityview()->log->error( 'Not rendering CSV: ' . $error->get_error_message() );
1381  return;
1382  }
1383 
1384  /**
1385  * Modify the name of the generated CSV file. Name will be sanitized using sanitize_file_name() before output.
1386  * @see sanitize_file_name()
1387  * @since 2.1
1388  * @param string $filename File name used when downloading a CSV. Default is "{View title}.csv"
1389  * @param \GV\View $view Current View being rendered
1390  */
1391  $filename = apply_filters( 'gravityview/output/csv/filename', get_the_title( $view->post ), $view );
1392 
1393  if ( ! defined( 'DOING_GRAVITYVIEW_TESTS' ) ) {
1394  header( sprintf( 'Content-Disposition: attachment;filename="%s.csv"', sanitize_file_name( $filename ) ) );
1395  header( 'Content-Transfer-Encoding: binary' );
1396  header( 'Content-Type: text/csv' );
1397  }
1398 
1399  ob_start();
1400  $csv = fopen( 'php://output', 'w' );
1401 
1402  /**
1403  * Add da' BOM if GF uses it
1404  * @see GFExport::start_export()
1405  */
1406  if ( apply_filters( 'gform_include_bom_export_entries', true, $view->form ? $view->form->form : null ) ) {
1407  fputs( $csv, "\xef\xbb\xbf" );
1408  }
1409 
1410  if ( $view->settings->get( 'csv_nolimit' ) ) {
1411  $view->settings->update( array( 'page_size' => -1 ) );
1412  }
1413 
1414  $entries = $view->get_entries();
1415 
1416  $headers_done = false;
1417  $allowed = $headers = array();
1418 
1419  foreach ( $view->fields->by_position( "directory_*" )->by_visible( $view )->all() as $id => $field ) {
1420  $allowed[] = $field;
1421  }
1422 
1423  $renderer = new Field_Renderer();
1424 
1425  foreach ( $entries->all() as $entry ) {
1426 
1427  $return = array();
1428 
1429  /**
1430  * @filter `gravityview/csv/entry/fields` Whitelist more entry fields by ID that are output in CSV requests.
1431  * @param[in,out] array $allowed The allowed ones, default by_visible, by_position( "context_*" ), i.e. as set in the View.
1432  * @param \GV\View $view The view.
1433  * @param \GV\Entry $entry WordPress representation of the item.
1434  */
1435  $allowed_field_ids = apply_filters( 'gravityview/csv/entry/fields', wp_list_pluck( $allowed, 'ID' ), $view, $entry );
1436 
1437  $allowed = array_filter( $allowed, function( $field ) use ( $allowed_field_ids ) {
1438  return in_array( $field->ID, $allowed_field_ids, true );
1439  } );
1440 
1441  foreach ( array_diff( $allowed_field_ids, wp_list_pluck( $allowed, 'ID' ) ) as $field_id ) {
1442  $allowed[] = is_numeric( $field_id ) ? \GV\GF_Field::by_id( $view->form, $field_id ) : \GV\Internal_Field::by_id( $field_id );
1443  }
1444 
1445  foreach ( $allowed as $field ) {
1446  $source = is_numeric( $field->ID ) ? $view->form : new \GV\Internal_Source();
1447 
1448  $return[] = $renderer->render( $field, $view, $source, $entry, gravityview()->request, '\GV\Field_CSV_Template' );
1449 
1450  if ( ! $headers_done ) {
1451  $label = $field->get_label( $view, $source, $entry );
1452  $headers[] = $label ? $label : $field->ID;
1453  }
1454  }
1455 
1456  if ( ! $headers_done ) {
1457  $headers_done = fputcsv( $csv, array_map( array( '\GV\Utils', 'strip_excel_formulas' ), array_values( $headers ) ) );
1458  }
1459 
1460  fputcsv( $csv, array_map( array( '\GV\Utils', 'strip_excel_formulas' ), $return ) );
1461  }
1462 
1463  fflush( $csv );
1464 
1465  echo rtrim( ob_get_clean() );
1466 
1467  if ( ! defined( 'DOING_GRAVITYVIEW_TESTS' ) ) {
1468  exit;
1469  }
1470  }
1471 
1472  /**
1473  * Return the query class for this View.
1474  *
1475  * @return string The class name.
1476  */
1477  public function get_query_class() {
1478  /**
1479  * @filter `gravityview/query/class`
1480  * @param[in,out] string The query class. Default: GF_Query.
1481  * @param \GV\View $this The View.
1482  */
1483  $query_class = apply_filters( 'gravityview/query/class', '\GF_Query', $this );
1484  return $query_class;
1485  }
1486 
1487  /**
1488  * Restrict View access to specific capabilities.
1489  *
1490  * Hooked into `map_meta_cap` WordPress filter.
1491  *
1492  * @since develop
1493  *
1494  * @param $caps array The output capabilities.
1495  * @param $cap string The cap that is being checked.
1496  * @param $user_id int The User ID.
1497  * @param $args array Additional arguments to the capability.
1498  *
1499  * @return array The resulting capabilities.
1500  */
1501  public static function restrict( $caps, $cap, $user_id, $args ) {
1502  /**
1503  * @filter `gravityview/security/require_unfiltered_html` Bypass restrictions on Views that require `unfiltered_html`.
1504  * @param[in,out] boolean
1505  *
1506  * @since develop
1507  * @param string $cap The capability requested.
1508  * @param int $user_id The user ID.
1509  * @param array $args Any additional args to map_meta_cap
1510  */
1511  if ( ! apply_filters( 'gravityview/security/require_unfiltered_html', true, $cap, $user_id ) ) {
1512  return $caps;
1513  }
1514 
1515  switch ( $cap ):
1516  case 'edit_gravityview':
1517  case 'edit_gravityviews':
1518  case 'edit_others_gravityviews':
1519  case 'edit_private_gravityviews':
1520  case 'edit_published_gravityviews':
1521  if ( ! user_can( $user_id, 'unfiltered_html' ) ) {
1522  if ( ! user_can( $user_id, 'gravityview_full_access' ) ) {
1523  return array( 'do_not_allow' );
1524  }
1525  }
1526 
1527  return $caps;
1528  case 'edit_post':
1529  if ( get_post_type( array_pop( $args ) ) == 'gravityview' ) {
1530  return self::restrict( $caps, 'edit_gravityview', $user_id, $args );
1531  }
1532  endswitch;
1533 
1534  return $caps;
1535  }
1536 
1537  public function __get( $key ) {
1538  if ( $this->post ) {
1539  $raw_post = $this->post->filter( 'raw' );
1540  return $raw_post->{$key};
1541  }
1542  return isset( $this->{$key} ) ? $this->{$key} : null;
1543  }
1544 }
If this file is called directly, abort.
$labels
static get_joins( $post)
Get joins associated with a view.
If this file is called directly, abort.
__construct()
The constructor.
static content( $content)
A renderer filter for the View post type content.
$forms
Definition: data-source.php:19
__get( $key)
static calculate_get_entries_criteria( $passed_criteria=array(), $form_ids=array())
Calculates the Search Criteria used on the self::get_entries / self::get_entry methods.
static _override_sorting_id_by_field_type( $sort_field_id, $form_id)
Override sorting per field.
static restrict( $caps, $cap, $user_id, $args)
Restrict View access to specific capabilities.
as_data()
Be compatible with the old data object.
offsetUnset( $offset)
ArrayAccess compatibility layer with GravityView_View_Data::$views.
offsetSet( $offset, $value)
ArrayAccess compatibility layer with GravityView_View_Data::$views.
can_render( $context=null, $request=null)
Checks whether this view can be accessed or not.
gravityview_get_form( $form_id)
Returns the form object for a given Form ID.
gravityview_get_template_settings( $post_id)
Get all the settings for a View.
If this file is called directly, abort.
$entries
offsetGet( $offset)
ArrayAccess compatibility layer with GravityView_View_Data::$views.
gravityview()
Definition: _stubs.php:26
get( $key, $default=null)
Retrieve a setting.
static by_id( $form, $field_id)
Get a by and Field ID.
const DESC
if(empty( $field_settings['content'])) $content
Definition: custom.php:37
static _flush_cache( $view_id=null)
Flush the view cache.
static no_views_text()
Get text for no views found.
Definition: class-admin.php:65
const APPROVED
If this file is called directly, abort.
const ASC
static exists( $view)
Determines if a view exists to begin with.
static is_field_numeric( $form=null, $field='')
Checks if the field type is a &#39;numeric&#39; field type (e.g.
static by_id( $post_id)
Construct a instance from a post ID.
If this file is called directly, abort.
static add_rewrite_endpoint()
Add extra rewrite endpoints.
$field_id
Definition: time.php:17
offsetExists( $offset)
ArrayAccess compatibility layer with GravityView_View_Data::$views.
static template_redirect()
Last chance to configure the output.
static by_id( $field_id)
Get a from an internal Gravity Forms field ID.
if(empty( $created_by)) $form_id
If this file is called directly, abort.
static get( $array, $key, $default=null)
Grab a value from an array or an object or default.
global $post
If this file is called directly, abort.
If this file is called directly, abort.
gravityview_get_template_id( $post_id)
Get the template ID (list, table, datatables, map) for a View.
static get_joined_forms( $post_id)
Get joined forms associated with a view In no particular order.
static is_approved( $status)
If this file is called directly, abort.
static has_cap( $caps='', $object_id=null, $user_id=null)
Alias of GravityView_Roles_Capabilities::has_cap()
$entry
Definition: notes.php:27
static register_post_type()
Register the gravityview WordPress Custom Post Type.
static get_unions( $post)
Get unions associated with a view.
static from_post( $post)
Construct a instance from a .
const meta_key
If this file is called directly, abort.
$field
Definition: gquiz_grade.php:11
get_query_class()
Return the query class for this View.
If this file is called directly, abort.