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