Completed
Push — master ( 589d3f...c5163f )
by Zack
12:27 queued 08:09
created

includes/class-frontend-views.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * GravityView Frontend functions
4
 *
5
 * @package   GravityView
6
 * @license   GPL2+
7
 * @author    Katz Web Services, Inc.
8
 * @link      http://gravityview.co
9
 * @copyright Copyright 2014, Katz Web Services, Inc.
10
 *
11
 * @since 1.0.0
12
 */
13
14
15
class GravityView_frontend {
16
17
	/**
18
	 * Regex strings that are used to determine whether the current request is a GravityView search or not.
19
	 * @see GravityView_frontend::is_searching()
20
	 * @since 1.7.4.1
21
	 * @var array
22
	 */
23
	private static $search_parameters = array( 'gv_search', 'gv_start', 'gv_end', 'gv_id', 'gv_by', 'filter_*' );
24
25
	/**
26
	 * Is the currently viewed post a `gravityview` post type?
27
	 * @var boolean
28
	 */
29
	var $is_gravityview_post_type = false;
30
31
	/**
32
	 * Does the current post have a `[gravityview]` shortcode?
33
	 * @var boolean
34
	 */
35
	var $post_has_shortcode = false;
36
37
	/**
38
	 * The Post ID of the currently viewed post. Not necessarily GV
39
	 * @var int
40
	 */
41
	var $post_id = null;
42
43
	/**
44
	 * Are we currently viewing a single entry?
45
	 * If so, the int value of the entry ID. Otherwise, false.
46
	 * @var int|boolean
47
	 */
48
	var $single_entry = false;
49
50
	/**
51
	 * If we are viewing a single entry, the entry data
52
	 * @var array|false
53
	 */
54
	var $entry = false;
55
56
	/**
57
	 * When displaying the single entry we should always know to which View it belongs (the context is everything!)
58
	 * @var null
59
	 */
60
	var $context_view_id = null;
61
62
	/**
63
	 * The View is showing search results
64
	 * @since 1.5.4
65
	 * @var boolean
66
	 */
67
	var $is_search = false;
68
69
	/**
70
	 * The view data parsed from the $post
71
	 *
72
	 * @see  GravityView_View_Data::__construct()
73
	 * @var GravityView_View_Data
74
	 */
75
	var $gv_output_data = null;
76
77
	/**
78
	 * @var GravityView_frontend
79
	 */
80
	static $instance;
81
82
	/**
83
	 * Class constructor, enforce Singleton pattern
84
	 */
85
	private function __construct() {}
86
87 3
	private function initialize() {
88 3
		add_action( 'wp', array( $this, 'parse_content'), 11 );
89 3
		add_filter( 'parse_query', array( $this, 'parse_query_fix_frontpage' ), 10 );
90 3
		add_action( 'template_redirect', array( $this, 'set_entry_data'), 1 );
91
92
		// Enqueue scripts and styles after GravityView_Template::register_styles()
93 3
		add_action( 'wp_enqueue_scripts', array( $this, 'add_scripts_and_styles' ), 20 );
94
95
		// Enqueue and print styles in the footer. Added 1 priorty so stuff gets printed at 10 priority.
96 3
		add_action( 'wp_print_footer_scripts', array( $this, 'add_scripts_and_styles' ), 1 );
97
98 3
		add_filter( 'the_title', array( $this, 'single_entry_title' ), 1, 2 );
99 3
		add_filter( 'the_content', array( $this, 'insert_view_in_content' ) );
100 3
		add_filter( 'comments_open', array( $this, 'comments_open' ), 10, 2 );
101
102 3
		add_action( 'gravityview_after', array( $this, 'context_not_configured_warning' ) );
103 3
	}
104
105
	/**
106
	 * Get the one true instantiated self
107
	 * @return GravityView_frontend
108
	 */
109 3
	public static function getInstance() {
110
111 3
		if ( empty( self::$instance ) ) {
112 3
			self::$instance = new self;
113 3
			self::$instance->initialize();
114
		}
115
116 3
		return self::$instance;
117
	}
118
119
	/**
120
	 * @return GravityView_View_Data
121
	 */
122
	public function getGvOutputData() {
123
		return $this->gv_output_data;
124
	}
125
126
	/**
127
	 * @param GravityView_View_Data $gv_output_data
128
	 */
129
	public function setGvOutputData( $gv_output_data ) {
130
		$this->gv_output_data = $gv_output_data;
131
	}
132
133
	/**
134
	 * @return boolean
135
	 */
136
	public function isSearch() {
137
		return $this->is_search;
138
	}
139
140
	/**
141
	 * @param boolean $is_search
142
	 */
143
	public function setIsSearch( $is_search ) {
144
		$this->is_search = $is_search;
145
	}
146
147
	/**
148
	 * @return bool|int
149
	 */
150 1
	public function getSingleEntry() {
151 1
		return $this->single_entry;
152
	}
153
154
	/**
155
	 * Sets the single entry ID and also the entry
156
	 * @param bool|int|string $single_entry
157
	 */
158
	public function setSingleEntry( $single_entry ) {
159
160
		$this->single_entry = $single_entry;
161
162
	}
163
164
	/**
165
	 * @return array
166
	 */
167
	public function getEntry() {
168
		return $this->entry;
169
	}
170
171
	/**
172
	 * Set the current entry
173
	 * @param array|int $entry Entry array or entry slug or ID
174
	 */
175
	public function setEntry( $entry ) {
176
177
		if ( ! is_array( $entry ) ) {
178
			$entry = GVCommon::get_entry( $entry );
179
		}
180
181
		$this->entry = $entry;
182
	}
183
184
	/**
185
	 * @return int
186
	 */
187
	public function getPostId() {
188
		return $this->post_id;
189
	}
190
191
	/**
192
	 * @param int $post_id
193
	 */
194
	public function setPostId( $post_id ) {
195
		$this->post_id = $post_id;
196
	}
197
198
	/**
199
	 * @return boolean
200
	 */
201 3
	public function isPostHasShortcode() {
202 3
		return $this->post_has_shortcode;
203
	}
204
205
	/**
206
	 * @param boolean $post_has_shortcode
207
	 */
208
	public function setPostHasShortcode( $post_has_shortcode ) {
209
		$this->post_has_shortcode = $post_has_shortcode;
210
	}
211
212
	/**
213
	 * @return boolean
214
	 */
215 3
	public function isGravityviewPostType() {
216 3
		return $this->is_gravityview_post_type;
217
	}
218
219
	/**
220
	 * @param boolean $is_gravityview_post_type
221
	 */
222
	public function setIsGravityviewPostType( $is_gravityview_post_type ) {
223
		$this->is_gravityview_post_type = $is_gravityview_post_type;
224
	}
225
226
	/**
227
	 * Set the context view ID used when page contains multiple embedded views or displaying the single entry view
228
	 *
229
	 *
230
	 *
231
	 * @param null $view_id
232
	 */
233 2
	public function set_context_view_id( $view_id = null ) {
234 2
		$multiple_views = defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ? gravityview()->views->count() > 1 : ( $this->getGvOutputData() && $this->getGvOutputData()->has_multiple_views() );
235
236 2
		if ( ! empty( $view_id ) ) {
237
238 2
			$this->context_view_id = $view_id;
239
240 2
		} elseif ( isset( $_GET['gvid'] ) && $multiple_views ) {
241
			/**
242
			 * used on a has_multiple_views context
243
			 * @see GravityView_API::entry_link
244
			 */
245 1
			$this->context_view_id = $_GET['gvid'];
246
247 2
		} elseif ( ! $multiple_views ) {
248 2
			if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
249 2
				$view = gravityview()->views->last();
250 2
				$this->context_view_id = $view ? $view->ID : null;
251
			} else {
252
				/** GravityView_View_Data::get_views is deprecated. */
253
				$array_keys = array_keys( $this->getGvOutputData()->get_views() );
254
				$this->context_view_id = array_pop( $array_keys );
255
				unset( $array_keys );
256
			}
257
		}
258
259 2
	}
260
261
	/**
262
	 * Returns the the view_id context when page contains multiple embedded views or displaying single entry view
263
	 *
264
	 * @since 1.5.4
265
	 *
266
	 * @return string
267
	 */
268
	public function get_context_view_id() {
269
		return $this->context_view_id;
270
	}
271
272
	/**
273
	 * Allow GravityView entry endpoints on the front page of a site
274
	 *
275
	 * @link  https://core.trac.wordpress.org/ticket/23867 Fixes this core issue
276
	 * @link https://wordpress.org/plugins/cpt-on-front-page/ Code is based on this
277
	 *
278
	 * @since 1.17.3
279
	 *
280
	 * @param WP_Query &$query (passed by reference)
281
	 *
282
	 * @return void
283
	 */
284 5
	public function parse_query_fix_frontpage( &$query ) {
285 5
		global $wp_rewrite;
286
287 5
		$is_front_page = ( $query->is_home || $query->is_page );
288 5
		$show_on_front = ( 'page' === get_option('show_on_front') );
289 5
		$front_page_id = get_option('page_on_front');
290
291 5
		if (  $is_front_page && $show_on_front && $front_page_id ) {
292
293
			// Force to be an array, potentially a query string ( entry=16 )
294
			$_query = wp_parse_args( $query->query );
295
296
			// pagename can be set and empty depending on matched rewrite rules. Ignore an empty pagename.
297
			if ( isset( $_query['pagename'] ) && '' === $_query['pagename'] ) {
298
				unset( $_query['pagename'] );
299
			}
300
301
			// this is where will break from core wordpress
302
			/** @internal Don't use this filter; it will be unnecessary soon - it's just a patch for specific use case */
303
			$ignore = apply_filters( 'gravityview/internal/ignored_endpoints', array( 'preview', 'page', 'paged', 'cpage' ), $query );
304
			$endpoints = rgobj( $wp_rewrite, 'endpoints' );
305
			foreach ( (array) $endpoints as $endpoint ) {
306
				$ignore[] = $endpoint[1];
307
			}
308
			unset( $endpoints );
309
310
			// Modify the query if:
311
			// - We're on the "Page on front" page (which we are), and:
312
			// - The query is empty OR
313
			// - The query includes keys that are associated with registered endpoints. `entry`, for example.
314
			if ( empty( $_query ) || ! array_diff( array_keys( $_query ), $ignore ) ) {
315
316
				$qv =& $query->query_vars;
317
318
				// Prevent redirect when on the single entry endpoint
319
				if( self::is_single_entry() ) {
320
					add_filter( 'redirect_canonical', '__return_false' );
321
				}
322
323
				$query->is_page = true;
324
				$query->is_home = false;
325
				$qv['page_id']  = $front_page_id;
326
327
				// Correct <!--nextpage--> for page_on_front
328
				if ( ! empty( $qv['paged'] ) ) {
329
					$qv['page'] = $qv['paged'];
330
					unset( $qv['paged'] );
331
				}
332
			}
333
334
			// reset the is_singular flag after our updated code above
335
			$query->is_singular = $query->is_single || $query->is_page || $query->is_attachment;
336
		}
337 5
	}
338
339
	/**
340
	 * Read the $post and process the View data inside
341
	 * @param  array  $wp Passed in the `wp` hook. Not used.
342
	 * @return void
343
	 */
344
	public function parse_content( $wp = array() ) {
345
		global $post;
346
347
		// If in admin and NOT AJAX request, get outta here.
348
		if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) && gravityview()->request->is_admin() ) {
349
			return;
350
			/** Deprecated in favor of gravityview()->request->is_admin(). */
351
		} else if ( GravityView_Plugin::is_admin() ) {
352
			return;
353
		}
354
355
		// Calculate requested Views
356
		$this->setGvOutputData( GravityView_View_Data::getInstance( $post ) );
357
358
		// !important: we need to run this before getting single entry (to kick the advanced filter)
359
		$this->set_context_view_id();
360
361
		$this->setIsGravityviewPostType( get_post_type( $post ) === 'gravityview' );
362
363
		$post_id = $this->getPostId() ? $this->getPostId() : (isset( $post ) ? $post->ID : null );
364
		$this->setPostId( $post_id );
365
		$post_has_shortcode = ! empty( $post->post_content ) ? gravityview_has_shortcode_r( $post->post_content, 'gravityview' ) : false;
366
		$this->setPostHasShortcode( $this->isGravityviewPostType() ? null : ! empty( $post_has_shortcode ) );
367
368
		// check if the View is showing search results (only for multiple entries View)
369
		$this->setIsSearch( $this->is_searching() );
370
371
		unset( $entry, $post_id, $post_has_shortcode );
372
	}
373
374
	/**
375
	 * Set the entry
376
	 */
377
	function set_entry_data() {
378
		$entry_id = self::is_single_entry();
379
		$this->setSingleEntry( $entry_id );
380
		$this->setEntry( $entry_id );
381
	}
382
383
	/**
384
	 * Checks if the current View is presenting search results
385
	 *
386
	 * @since 1.5.4
387
	 *
388
	 * @return boolean True: Yes, it's a search; False: No, not a search.
389
	 */
390
	function is_searching() {
391
392
		// It's a single entry, not search
393
		if ( $this->getSingleEntry() ) {
394
			return false;
395
		}
396
397
		$search_method = GravityView_Widget_Search::getInstance()->get_search_method();
398
399
		if( 'post' === $search_method ) {
400
			$get = $_POST;
401
		} else {
402
			$get = $_GET;
403
		}
404
405
		// No $_GET parameters
406
		if ( empty( $get ) || ! is_array( $get ) ) {
407
			return false;
408
		}
409
410
		// Remove empty values
411
		$get = array_filter( $get );
412
413
		// If the $_GET parameters are empty, it's no search.
414
		if ( empty( $get ) ) {
415
			return false;
416
		}
417
418
		$search_keys = array_keys( $get );
419
420
		$search_match = implode( '|', self::$search_parameters );
421
422
		foreach ( $search_keys as $search_key ) {
423
424
			// Analyze the search key $_GET parameter and see if it matches known GV args
425
			if ( preg_match( '/(' . $search_match . ')/i', $search_key ) ) {
426
				return true;
427
			}
428
		}
429
430
		return false;
431
	}
432
433
	/**
434
	 * Filter the title for the single entry view
435
	 *
436
	 * @param  string $title   current title
437
	 * @param  int $passed_post_id Post ID
438
	 * @return string          (modified) title
439
	 */
440 1
	public function single_entry_title( $title, $passed_post_id = null ) {
441 1
		global $post;
442
443
		// If this is the directory view, return.
444 1
		if ( ! $this->getSingleEntry() ) {
445
			return $title;
446
		}
447
448 1
		$entry = $this->getEntry();
449
450
		/**
451
		 * @filter `gravityview/single/title/out_loop` Apply the Single Entry Title filter outside the WordPress loop?
452
		 * @param boolean $in_the_loop Whether to apply the filter to the menu title and the meta tag <title> - outside the loop
453
		 * @param array $entry Current entry
454
		 */
455 1
		$apply_outside_loop = apply_filters( 'gravityview/single/title/out_loop' , in_the_loop(), $entry );
456
457 1
		if ( ! $apply_outside_loop ) {
458
			return $title;
459
		}
460
461
		// User reported WooCommerce doesn't pass two args.
462 1
		if ( empty( $passed_post_id ) )  {
463
			return $title;
464
		}
465
466
		// Don't modify the title for anything other than the current view/post.
467
		// This is true for embedded shortcodes and Views.
468 1
		if ( is_object( $post ) && (int) $post->ID !== (int) $passed_post_id ) {
469
			return $title;
470
		}
471
472 1
		$context_view_id = $this->get_context_view_id();
473
474 1
		$multiple_views = defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ? gravityview()->views->count() > 1 : $this->getGvOutputData()->has_multiple_views();
475
476 1
		if ( $multiple_views && ! empty( $context_view_id ) ) {
477 1
			if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
478 1
				$view = gravityview()->views->get( $context_view_id );
479 1
				if ( ! $view ) {
480
					/** Emulate the weird behavior of \GravityView_View_Data::get_view adding a view which wasn't there to begin with. */
481
					gravityview()->views->add( \GV\View::by_id( $context_view_id ) );
482 1
					$view = gravityview()->views->get( $context_view_id );
483
				}
484
			} else {
485
				/** Deprecated. Use gravityview()->views->get() or gravityview()->request->get() */
486 1
				$view_meta = $this->getGvOutputData()->get_view( $context_view_id );
487
			}
488
		} else {
489 1
			if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
490 1
				foreach ( gravityview()->views->all() as $_view ) {
491 1
					if ( intval( $_view->form->ID ) === intval( $entry['form_id'] ) ) {
492 1
						$view = $_view;
493 1
						break;
494
					}
495
				}
496
497
				/** No matching form sources were found, happens when requesting an entry from a different form . */
498 1
				if ( ! isset( $view ) )
499 1
					return $title;
500
			} else {
501
				/** Deprecated. Use gravityview()->views->all() or gravityview()->request->all() */
502
				foreach ( $this->getGvOutputData()->get_views() as $view_id => $view_data ) {
503
					if ( intval( $view_data['form_id'] ) === intval( $entry['form_id'] ) ) {
504
						$view_meta = $view_data;
505
						break;
506
					}
507
				}
508
			}
509
		}
510
511 1
		if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
512 1
			if ( $title = $view->settings->get( 'single_title' ) ) {
513 1
				$title = GravityView_API::replace_variables( $title, $view->form->form, $entry );
514 1
				$title = do_shortcode( $title );
515
			}
516
		} else {
517
			/** Deprecated stuff in the future. See the branch above. */
518
			if ( ! empty( $view_meta['atts']['single_title'] ) ) {
519
520
				$title = $view_meta['atts']['single_title'];
521
522
				// We are allowing HTML in the fields, so no escaping the output
523
				$title = GravityView_API::replace_variables( $title, $view_meta['form'], $entry );
524
525
				$title = do_shortcode( $title );
526
			}
527
		}
528
529
530 1
		return $title;
531
	}
532
533
534
	/**
535
	 * In case View post is called directly, insert the view in the post content
536
	 *
537
	 * @access public
538
	 * @static
539
	 * @param mixed $content
540
	 * @return string Add the View output into View CPT content
541
	 */
542 1
	public function insert_view_in_content( $content ) {
543
544
		// Plugins may run through the content in the header. WP SEO does this for its OpenGraph functionality.
545 1
		if ( ! did_action( 'loop_start' ) ) {
546
547
			do_action( 'gravityview_log_debug', '[insert_view_in_content] Not processing yet: loop_start hasn\'t run yet. Current action:', current_filter() );
548
549
			return $content;
550
		}
551
552
		//	We don't want this filter to run infinite loop on any post content fields
553 1
		remove_filter( 'the_content', array( $this, 'insert_view_in_content' ) );
554
555
		// Otherwise, this is called on the Views page when in Excerpt mode.
556 1
		if ( is_admin() ) {
557
			return $content;
558
		}
559
560
		// Only render in the loop. Fixes issues with the_content filter being applied in places like the sidebar
561 1
		if( ! in_the_loop() ) {
562
			return $content;
563
		}
564
565 1
		if ( $this->isGravityviewPostType() ) {
566
567
			/** @since 1.7.4 */
568 1
			if ( is_preview() && ! gravityview_get_form_id( $this->post_id ) ) {
569
				$content .= __( 'When using a preset template, you must save the View before a Preview is available.', 'gravityview' );
570
			} else {
571 1
				if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
572 1
					foreach ( gravityview()->views->all() as $view ) {
573 1
						$content .= $this->render_view( array( 'id' => $view->ID ) );
574
					}
575
				} else {
576
					/** The \GravityView_View_Data::get_views method is depreacted. */
577
					foreach ( $this->getGvOutputData()->get_views() as $view_id => $data ) {
578
						$content .= $this->render_view( array( 'id' => $view_id ) );
579
					}
580
				}
581
			}
582
		}
583
584
		//	Add the filter back in
585 1
		add_filter( 'the_content', array( $this, 'insert_view_in_content' ) );
586
587 1
		return $content;
588
	}
589
590
	/**
591
	 * Disable comments on GravityView post types
592
	 * @param  boolean $open    existing status
593
	 * @param  int $post_id Post ID
594
	 * @return boolean
595
	 */
596
	public function comments_open( $open, $post_id ) {
597
598
		if ( $this->isGravityviewPostType() ) {
599
			$open = false;
600
		}
601
602
		/**
603
		 * @filter `gravityview/comments_open` Whether to set comments to open or closed.
604
		 * @since  1.5.4
605
		 * @param  boolean $open Open or closed status
606
		 * @param  int $post_id Post ID to set comment status for
607
		 */
608
		$open = apply_filters( 'gravityview/comments_open', $open, $post_id );
609
610
		return $open;
611
	}
612
613
	/**
614
	 * Display a warning when a View has not been configured
615
	 *
616
	 * @since 1.19.2
617
	 *
618
	 * @param int $view_id The ID of the View currently being displayed
619
	 *
620
	 * @return void
621
	 */
622
	public function context_not_configured_warning( $view_id = 0 ) {
623
624
		if ( ! class_exists( 'GravityView_View' ) ) {
625
			return;
626
		}
627
628
		$fields = GravityView_View::getInstance()->getContextFields();
629
630
		if ( ! empty( $fields ) ) {
631
			return;
632
		}
633
634
		$context = GravityView_View::getInstance()->getContext();
635
636
		switch( $context ) {
637
			case 'directory':
638
				$tab = __( 'Multiple Entries', 'gravityview' );
639
				break;
640
			case 'edit':
641
				$tab = __( 'Edit Entry', 'gravityview' );
642
				break;
643
			case 'single':
644
			default:
645
				$tab = __( 'Single Entry', 'gravityview' );
646
				break;
647
		}
648
0 ignored issues
show
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
649
650
		$title = sprintf( esc_html_x('The %s layout has not been configured.', 'Displayed when a View is not configured. %s is replaced by the tab label', 'gravityview' ), $tab );
651
		$edit_link = admin_url( sprintf( 'post.php?post=%d&action=edit#%s-view', $view_id, $context ) );
652
		$action_text = sprintf( esc_html__('Add fields to %s', 'gravityview' ), $tab );
653
		$message = esc_html__( 'You can only see this message because you are able to edit this View.', 'gravityview' );
654
655
		$image =  sprintf( '<img alt="%s" src="%s" style="margin-top: 10px;" />', $tab, esc_url(plugins_url( sprintf( 'assets/images/tab-%s.png', $context ), GRAVITYVIEW_FILE ) ) );
656
		$output = sprintf( '<h3>%s <strong><a href="%s">%s</a></strong></h3><p>%s</p>', $title, esc_url( $edit_link ), $action_text, $message );
657
658
		echo GVCommon::generate_notice( $output . $image, 'gv-error error', 'edit_gravityview', $view_id );
659
	}
660
661
662
	/**
663
	 * Core function to render a View based on a set of arguments
664
	 *
665
	 * @access public
666
	 * @static
667
	 * @param array $passed_args {
668
	 *
669
	 *      Settings for rendering the View
670
	 *
671
	 *      @type int $id View id
672
	 *      @type int $page_size Number of entries to show per page
673
	 *      @type string $sort_field Form field id to sort
674
	 *      @type string $sort_direction Sorting direction ('ASC' or 'DESC')
675
	 *      @type string $start_date - Ymd
676
	 *      @type string $end_date - Ymd
677
	 *      @type string $class - assign a html class to the view
678
	 *      @type string $offset (optional) - This is the start point in the current data set (0 index based).
679
	 * }
680
	 *
681
	 * @return string|null HTML output of a View, NULL if View isn't found
682
	 */
683 1
	public function render_view( $passed_args ) {
684
685
		// validate attributes
686 1
		if ( empty( $passed_args['id'] ) ) {
687
			do_action( 'gravityview_log_error', '[render_view] Returning; no ID defined.', $passed_args );
688
			return null;
689
		}
690
691
		// Solve problem when loading content via admin-ajax.php
692
		// @hack
693 1
		if ( ! $this->getGvOutputData() ) {
694
695 1
			do_action( 'gravityview_log_error', '[render_view] gv_output_data not defined; parsing content.', $passed_args );
696
697 1
			$this->parse_content();
698
		}
699
700
		// Make 100% sure that we're dealing with a properly called situation
701 1
		if ( ! is_object( $this->getGvOutputData() ) || ! is_callable( array( $this->getGvOutputData(), 'get_view' ) ) ) {
702
703
			do_action( 'gravityview_log_error', '[render_view] gv_output_data not an object or get_view not callable.', $this->getGvOutputData() );
704
705
			return null;
706
		}
707
708 1
		$view_id = $passed_args['id'];
709
710 1
		if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
711 1
			$view = gravityview()->views->get( $view_id );
712 1
			if ( ! $view ) {
713
				/** Emulate the weird behavior of \GravityView_View_Data::get_view adding a view which wasn't there to begin with. */
714 1
				gravityview()->views->add( \GV\View::by_id( $view_id ) );
715 1
				$view = gravityview()->views->get( $view_id );
716
717 1
				if ( ! $view ) {
718
					do_action( 'gravityview_log_debug', sprintf( 'GravityView_View_Data[add_view] Returning; View #%s does not exist.', $view_id ) );
719
					return null;
720
				}
721
			}
722
723
			/** Update the view settings with the requested arguments. */
724 1
			$view->settings->update( $passed_args );
725
		} else {
726
			/** \GravityView_View_Data::get_view is deprecated. */
727
			$view_data = $this->getGvOutputData()->get_view( $view_id, $passed_args );
728
729
			do_action( 'gravityview_log_debug', '[render_view] View Data: ', $view_data );
730
731
			do_action( 'gravityview_log_debug', '[render_view] Init View. Arguments: ', $passed_args );
732
733
			// The passed args were always winning, even if they were NULL.
734
			// This prevents that. Filters NULL, FALSE, and empty strings.
735
			$passed_args = array_filter( $passed_args, 'strlen' );
736
737
			//Override shortcode args over View template settings
738
			$atts = wp_parse_args( $passed_args, $view_data['atts'] );
739
740
			do_action( 'gravityview_log_debug', '[render_view] Arguments after merging with View settings: ', $atts );
741
		}
742
743
		// It's password protected and you need to log in.
744 1
		if ( post_password_required( $view_id ) ) {
745
746
			do_action( 'gravityview_log_error', sprintf( '[render_view] Returning: View %d is password protected.', $view_id ) );
747
748
			// If we're in an embed or on an archive page, show the password form
749
			if ( get_the_ID() !== $view_id ) {
750
				return get_the_password_form();
751
			}
752
753
			// Otherwise, just get outta here
754
			return null;
755
		}
756
757
		/**
758
		 * Don't render View if user isn't allowed to see it
759
		 * @since 1.15
760
		 * @since 1.17.2 Added check for if a user has no caps but is logged in (member of multisite, but not any site). Treat as if logged-out.
761
		 */
762 1
		if( is_user_logged_in() && ! ( empty( wp_get_current_user()->caps ) && empty( wp_get_current_user()->roles ) ) && false === GVCommon::has_cap( 'read_gravityview', $view_id ) ) {
763
764
			do_action( 'gravityview_log_debug', sprintf( '%s Returning: View %d is not visible by current user.', __METHOD__, $view_id ) );
765
766
			return null;
767
		}
768
769 1
		if( $this->isGravityviewPostType() ) {
770
771
			/**
772
			 * @filter `gravityview_direct_access` Should Views be directly accessible, or only visible using the shortcode?
773
			 * @see https://codex.wordpress.org/Function_Reference/register_post_type#public
774
			 * @see \GV\Entry::get_endpoint_name
775
			 * @since 1.15.2
776
			 * @param[in,out] boolean `true`: allow Views to be accessible directly. `false`: Only allow Views to be embedded via shortcode. Default: `true`
777
			 * @param int $view_id The ID of the View currently being requested. `0` for general setting
778
			 */
779 1
			$direct_access = apply_filters( 'gravityview_direct_access', true, $view_id );
780
781 1
			if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
782 1
				$embed_only = $view->settings->get( 'embed_only' );
783
			} else {
784
				/** Deprecated. View attributes moved to \GV\View::$settings. */
785
				$embed_only = ! empty( $atts['embed_only'] );
786
			}
787
788 1
			if( ! $direct_access || ( $embed_only && ! GVCommon::has_cap( 'read_private_gravityviews' ) ) ) {
789 1
				return __( 'You are not allowed to view this content.', 'gravityview' );
790
			}
791
		}
792
793 1
		ob_start();
794
795
		/**
796
		 * Set globals for templating
797
		 * @deprecated 1.6.2
798
		 */
799 1
		global $gravityview_view;
800
801 1
		if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
802 1
			$view_data = $view->as_data();
803 1
			$gravityview_view = new GravityView_View( $view_data );
804 1
			$post_id = intval( $view->settings->get( 'post_id' ) ? : get_the_ID() );
805 1
			$template_id = $view->template ? $view->template->ID : null;
806
		} else {
807
			/** These constructs are deprecated. Use the new gravityview() wrapper. */
808
			$gravityview_view = new GravityView_View( $view_data );
809
			$post_id = ! empty( $atts['post_id'] ) ? intval( $atts['post_id'] ) : get_the_ID();
810
			$template_id = $view_data['template_id'];
811
		}
812
813 1
		$gravityview_view->setPostId( $post_id );
814
815 1
		if ( ! $this->getSingleEntry() ) {
816
817
			// user requested Directory View
818 1
			do_action( 'gravityview_log_debug', '[render_view] Executing Directory View' );
819
820
			//fetch template and slug
821 1
			$view_slug = apply_filters( 'gravityview_template_slug_'. $template_id, 'table', 'directory' );
822
823 1
			do_action( 'gravityview_log_debug', '[render_view] View template slug: ', $view_slug );
824
825
			/**
826
			 * Disable fetching initial entries for views that don't need it (DataTables)
827
			 */
828 1
			$get_entries = apply_filters( 'gravityview_get_view_entries_'.$view_slug, true );
829
830 1
			if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
831 1
				$hide_until_searched = $view->settings->get( 'hide_until_searched' );
832
			} else {
833
				/** $atts is deprecated, use \GV\View:$settings */
834
				$hide_until_searched = ! empty( $atts['hide_until_searched'] );
835
			}
836
837
			/**
838
			 * Hide View data until search is performed
839
			 * @since 1.5.4
840
			 */
841 1
			if ( $hide_until_searched && ! $this->isSearch() ) {
842
				$gravityview_view->setHideUntilSearched( true );
843
				$get_entries = false;
844
			}
845
846 1
			if ( $get_entries ) {
847
848 1
				if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
849 1
					$sort_columns = $view->settings->get( 'sort_columns' );
850
				} else {
851
					/** $atts is deprecated, use \GV\View:$settings */
852
					$sort_columns = ! empty( $atts['sort_columns'] );
853
				}
854
855 1
				if ( $sort_columns ) {
856
					// add filter to enable column sorting
857
					add_filter( 'gravityview/template/field_label', array( $this, 'add_columns_sort_links' ) , 100, 3 );
858
				}
859
860 1
				if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
861 1
					$view_entries = self::get_view_entries( $view->settings->as_atts(), $view->form->ID );
862
				} else {
863
					/** $atts is deprecated, use \GV\View:$settings */
864
					/** $view_data is depreacted, use \GV\View properties */
865
					$view_entries = self::get_view_entries( $atts, $view_data['form_id'] );
866
				}
867
868 1
				do_action( 'gravityview_log_debug', sprintf( '[render_view] Get Entries. Found %s entries total, showing %d entries', $view_entries['count'], sizeof( $view_entries['entries'] ) ) );
869
870
			} else {
871
872
				$view_entries = array( 'count' => null, 'entries' => null, 'paging' => null );
873
874
				do_action( 'gravityview_log_debug', '[render_view] Not fetching entries because `gravityview_get_view_entries_'.$view_slug.'` is false' );
875
			}
876
877 1
			$gravityview_view->setPaging( $view_entries['paging'] );
878 1
			$gravityview_view->setContext( 'directory' );
879 1
			$sections = array( 'header', 'body', 'footer' );
880
881
		} else {
882
883
			// user requested Single Entry View
884 1
			do_action( 'gravityview_log_debug', '[render_view] Executing Single View' );
885
886
			/**
887
			 * @action `gravityview_render_entry_{View ID}` Before rendering a single entry for a specific View ID
888
			 * @since 1.17
889
			 */
890 1
			if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
891 1
				do_action( 'gravityview_render_entry_' . $view->ID );
892
			} else {
893
				/** $view_data is depreacted, use \GV\View properties */
894
				do_action( 'gravityview_render_entry_'.$view_data['id'] );
895
			}
896
897 1
			$entry = $this->getEntry();
898
899
			// You are not permitted to view this entry.
900 1
			if ( empty( $entry ) || ! self::is_entry_approved( $entry, defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ? $view->settings->as_atts() : $atts ) ) {
901
902
				do_action( 'gravityview_log_debug', '[render_view] Entry does not exist. This may be because of View filters limiting access.' );
903
904
				// Only display warning once when multiple Views are embedded
905
				if( $view_id !== (int) GravityView_frontend::get_context_view_id() ) {
906
					ob_end_clean();
907
					return null;
908
				}
909
910
				/**
911
				 * @filter `gravityview/render/entry/not_visible` Modify the message shown to users when the entry doesn't exist or they aren't allowed to view it.
912
				 * @since 1.6
913
				 * @param string $message Default: "You have attempted to view an entry that is not visible or may not exist."
914
				 */
915
				$message = apply_filters( 'gravityview/render/entry/not_visible', __( 'You have attempted to view an entry that is not visible or may not exist.', 'gravityview' ) );
916
917
				/**
918
				 * @since 1.6
919
				 */
920
				echo esc_attr( $message );
921
922
				ob_end_clean();
923
				return null;
924
			}
925
926
			// We're in single view, but the view being processed is not the same view the single entry belongs to.
927
			// important: do not remove this as it prevents fake attempts of displaying entries from other views/forms
928 1
			$multiple_views = defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ? gravityview()->views->count() > 1 : $this->getGvOutputData()->has_multiple_views();
929 1
			if ( $multiple_views && $view_id != $this->get_context_view_id() ) {
930
				do_action( 'gravityview_log_debug', '[render_view] In single entry view, but the entry does not belong to this View. Perhaps there are multiple views on the page. View ID: '. $view_id );
931
				ob_end_clean();
932
				return null;
933
			}
934
935
			//fetch template and slug
936 1
			$view_slug = apply_filters( 'gravityview_template_slug_' . $template_id, 'table', 'single' );
937 1
			do_action( 'gravityview_log_debug', '[render_view] View single template slug: ', $view_slug );
938
939
			//fetch entry detail
940 1
			$view_entries['count'] = 1;
941 1
			$view_entries['entries'][] = $entry;
942 1
			do_action( 'gravityview_log_debug', '[render_view] Get single entry: ', $view_entries['entries'] );
943
944 1
			if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
945 1
				$back_link_label = $view->settings->get( 'back_link_label', null );
946
			} else {
947
				$back_link_label = isset( $atts['back_link_label'] ) ? $atts['back_link_label'] : null;
948
			}
949
950
			// set back link label
951 1
			$gravityview_view->setBackLinkLabel( $back_link_label );
952 1
			$gravityview_view->setContext( 'single' );
953 1
			$sections = array( 'single' );
954
955
		}
956
957
		// add template style
958 1
		self::add_style( $template_id );
959
960
		// Prepare to render view and set vars
961 1
		$gravityview_view->setEntries( $view_entries['entries'] );
962 1
		$gravityview_view->setTotalEntries( $view_entries['count'] );
963
964
		// If Edit
965 1
		if ( 'edit' === gravityview_get_context() ) {
966
967
			do_action( 'gravityview_log_debug', '[render_view] Edit Entry ' );
968
969
			do_action( 'gravityview_edit_entry', $this->getGvOutputData() );
970
971
			return ob_get_clean();
972
973
		} else {
974
			// finaly we'll render some html
975 1
			$sections = apply_filters( 'gravityview_render_view_sections', $sections, $template_id );
976
977 1
			do_action( 'gravityview_log_debug', '[render_view] Sections to render: ', $sections );
978 1
			foreach ( $sections as $section ) {
979 1
				do_action( 'gravityview_log_debug', '[render_view] Rendering '. $section . ' section.' );
980 1
				$gravityview_view->render( $view_slug, $section, false );
981
			}
982
		}
983
984
		//@todo: check why we need the IF statement vs. print the view id always.
985 1
		if ( $this->isGravityviewPostType() || $this->isPostHasShortcode() ) {
986
			// Print the View ID to enable proper cookie pagination
987 1
			echo '<input type="hidden" class="gravityview-view-id" value="' . esc_attr( $view_id ) . '">';
988
		}
989 1
		$output = ob_get_clean();
990
991 1
		return $output;
992
	}
993
994
	/**
995
	 * Process the start and end dates for a view - overrides values defined in shortcode (if needed)
996
	 *
997
	 * The `start_date` and `end_date` keys need to be in a format processable by GFFormsModel::get_date_range_where(),
998
	 * which uses \DateTime() format.
999
	 *
1000
	 * You can set the `start_date` or `end_date` to any value allowed by {@link http://www.php.net//manual/en/function.strtotime.php strtotime()},
1001
	 * including strings like "now" or "-1 year" or "-3 days".
1002
	 *
1003
	 * @see GFFormsModel::get_date_range_where
1004
	 *
1005
	 * @param  array      $args            View settings
1006
	 * @param  array      $search_criteria Search being performed, if any
1007
	 * @return array                       Modified `$search_criteria` array
1008
	 */
1009 1
	public static function process_search_dates( $args, $search_criteria = array() ) {
1010
1011 1
		$return_search_criteria = $search_criteria;
1012
1013 1
		foreach ( array( 'start_date', 'end_date' ) as $key ) {
1014
1015
1016
			// Is the start date or end date set in the view or shortcode?
1017
			// If so, we want to make sure that the search doesn't go outside the bounds defined.
1018 1
			if ( ! empty( $args[ $key ] ) ) {
1019
1020
				// Get a timestamp and see if it's a valid date format
1021 1
				$date = strtotime( $args[ $key ] );
1022
1023
				// The date was invalid
1024 1
				if ( empty( $date ) ) {
1025
					do_action( 'gravityview_log_error', __METHOD__ . ' Invalid ' . $key . ' date format: ' . $args[ $key ] );
1026
					continue;
1027
				}
1028
1029
				// The format that Gravity Forms expects for start_date and day-specific (not hour/second-specific) end_date
1030 1
				$datetime_format = 'Y-m-d H:i:s';
1031 1
				$search_is_outside_view_bounds = false;
1032
1033 1
				if( ! empty( $search_criteria[ $key ] ) ) {
1034
1035 1
					$search_date = strtotime( $search_criteria[ $key ] );
1036
1037
					// The search is for entries before the start date defined by the settings
1038
					switch ( $key ) {
1039 1
						case 'end_date':
1040
							/**
1041
							 * If the end date is formatted as 'Y-m-d', it should be formatted without hours and seconds
1042
							 * so that Gravity Forms can convert the day to 23:59:59 the previous day.
1043
							 *
1044
							 * If it's a relative date ("now" or "-1 day"), then it should use the precise date format
1045
							 *
1046
							 * @see GFFormsModel::get_date_range_where
1047
							 */
1048 1
							$datetime_format               = gravityview_is_valid_datetime( $args[ $key ] ) ? 'Y-m-d' : 'Y-m-d H:i:s';
1049 1
							$search_is_outside_view_bounds = ( $search_date > $date );
1050 1
							break;
1051 1
						case 'start_date':
1052 1
							$search_is_outside_view_bounds = ( $search_date < $date );
1053 1
							break;
1054
					}
1055
				}
1056
1057
				// If there is no search being performed, or if there is a search being performed that's outside the bounds
1058 1
				if ( empty( $search_criteria[ $key ] ) || $search_is_outside_view_bounds ) {
1059
1060
					// Then we override the search and re-set the start date
1061 1
					$return_search_criteria[ $key ] = date_i18n( $datetime_format , $date, true );
1062
				}
1063
			}
1064
		}
1065
1066 1
		if( isset( $return_search_criteria['start_date'] ) && isset( $return_search_criteria['end_date'] ) ) {
1067
			// The start date is AFTER the end date. This will result in no results, but let's not force the issue.
1068 1
			if ( strtotime( $return_search_criteria['start_date'] ) > strtotime( $return_search_criteria['end_date'] ) ) {
1069 1
				do_action( 'gravityview_log_error', __METHOD__ . ' Invalid search: the start date is after the end date.', $return_search_criteria );
1070
			}
1071
		}
1072
1073 1
		return $return_search_criteria;
1074
	}
1075
1076
1077
	/**
1078
	 * Process the approved only search criteria according to the View settings
1079
	 *
1080
	 * @param  array      $args            View settings
1081
	 * @param  array      $search_criteria Search being performed, if any
1082
	 * @return array                       Modified `$search_criteria` array
1083
	 */
1084
	public static function process_search_only_approved( $args, $search_criteria ) {
1085
1086
		/** @since 1.19 */
1087
		if( ! empty( $args['admin_show_all_statuses'] ) && GVCommon::has_cap('gravityview_moderate_entries') ) {
1088
			do_action( 'gravityview_log_debug', __METHOD__ . ': User can moderate entries; showing all approval statuses' );
1089
			return $search_criteria;
1090
		}
1091
1092
		if ( ! empty( $args['show_only_approved'] ) ) {
1093
1094
			$search_criteria['field_filters'][] = array(
1095
				'key' => GravityView_Entry_Approval::meta_key,
1096
				'value' => GravityView_Entry_Approval_Status::APPROVED
1097
			);
1098
1099
			$search_criteria['field_filters']['mode'] = 'all'; // force all the criterias to be met
1100
1101
			do_action( 'gravityview_log_debug', '[process_search_only_approved] Search Criteria if show only approved: ', $search_criteria );
1102
		}
1103
1104
		return $search_criteria;
1105
	}
1106
1107
1108
	/**
1109
	 * Check if a certain entry is approved.
1110
	 *
1111
	 * If we pass the View settings ($args) it will check the 'show_only_approved' setting before
1112
	 *   checking the entry approved field, returning true if show_only_approved = false.
1113
	 *
1114
	 * @since 1.7
1115
	 * @since 1.18 Converted check to use GravityView_Entry_Approval_Status::is_approved
1116
	 *
1117
	 * @uses GravityView_Entry_Approval_Status::is_approved
1118
	 *
1119
	 * @param array $entry  Entry object
1120
	 * @param array $args   View settings (optional)
1121
	 *
1122
	 * @return bool
1123
	 */
1124
	public static function is_entry_approved( $entry, $args = array() ) {
1125
1126
		if ( empty( $entry['id'] ) || ( array_key_exists( 'show_only_approved', $args ) && ! $args['show_only_approved'] ) ) {
1127
			// is implicitly approved if entry is null or View settings doesn't require to check for approval
1128
			return true;
1129
		}
1130
1131
		/** @since 1.19 */
1132
		if( ! empty( $args['admin_show_all_statuses'] ) && GVCommon::has_cap('gravityview_moderate_entries') ) {
1133
			do_action( 'gravityview_log_debug', __METHOD__ . ': User can moderate entries, so entry is approved for viewing' );
1134
			return true;
1135
		}
1136
1137
		$is_approved = gform_get_meta( $entry['id'], GravityView_Entry_Approval::meta_key );
1138
1139
		return GravityView_Entry_Approval_Status::is_approved( $is_approved );
1140
	}
1141
1142
	/**
1143
	 * Parse search criteria for a entries search.
1144
	 *
1145
	 * array(
1146
	 * 	'search_field' => 1, // ID of the field
1147
	 *  'search_value' => '', // Value of the field to search
1148
	 *  'search_operator' => 'contains', // 'is', 'isnot', '>', '<', 'contains'
1149
	 *  'show_only_approved' => 0 or 1 // Boolean
1150
	 * )
1151
	 *
1152
	 * @param  array $args    Array of args
1153
	 * @param  int $form_id Gravity Forms form ID
1154
	 * @return array          Array of search parameters, formatted in Gravity Forms mode, using `status` key set to "active" by default, `field_filters` array with `key`, `value` and `operator` keys.
1155
	 */
1156 1
	public static function get_search_criteria( $args, $form_id ) {
1157
1158
		/**
1159
		 * @filter `gravityview_fe_search_criteria` Modify the search criteria
1160
		 * @see GravityView_Widget_Search::filter_entries Adds the default search criteria
1161
		 * @param array $search_criteria Empty `field_filters` key
1162
		 * @param int $form_id ID of the Gravity Forms form that is being searched
1163
		 */
1164 1
		$search_criteria = apply_filters( 'gravityview_fe_search_criteria', array( 'field_filters' => array() ), $form_id );
1165
1166 1
		$original_search_criteria = $search_criteria;
1167
1168 1
		do_action( 'gravityview_log_debug', '[get_search_criteria] Search Criteria after hook gravityview_fe_search_criteria: ', $search_criteria );
1169
1170
		// implicity search
1171 1
		if ( ! empty( $args['search_value'] ) ) {
1172
1173
			// Search operator options. Options: `is` or `contains`
1174 1
			$operator = ! empty( $args['search_operator'] ) && in_array( $args['search_operator'], array( 'is', 'isnot', '>', '<', 'contains' ) ) ? $args['search_operator'] : 'contains';
1175
1176 1
			$search_criteria['field_filters'][] = array(
1177 1
				'key' => rgget( 'search_field', $args ), // The field ID to search
1178 1
				'value' => _wp_specialchars( $args['search_value'] ), // The value to search. Encode ampersands but not quotes.
1179 1
				'operator' => $operator,
1180
			);
1181
1182
			// Lock search mode to "all" with implicit presearch filter.
1183 1
			$search_criteria['field_filters']['mode'] = 'all';
1184
		}
1185
1186 1
		if( $search_criteria !== $original_search_criteria ) {
1187 1
			do_action( 'gravityview_log_debug', '[get_search_criteria] Search Criteria after implicity search: ', $search_criteria );
1188
		}
1189
1190
		// Handle setting date range
1191 1
		$search_criteria = self::process_search_dates( $args, $search_criteria );
1192
1193 1
		if( $search_criteria !== $original_search_criteria ) {
1194 1
			do_action( 'gravityview_log_debug', '[get_search_criteria] Search Criteria after date params: ', $search_criteria );
1195
		}
1196
1197
		// remove not approved entries
1198 1
		$search_criteria = self::process_search_only_approved( $args, $search_criteria );
1199
1200
		/**
1201
		 * @filter `gravityview_status` Modify entry status requirements to be included in search results.
1202
		 * @param string $status Default: `active`. Accepts all Gravity Forms entry statuses, including `spam` and `trash`
1203
		 */
1204 1
		$search_criteria['status'] = apply_filters( 'gravityview_status', 'active', $args );
1205
1206 1
		return $search_criteria;
1207
	}
1208
1209
1210
1211
	/**
1212
	 * Core function to calculate View multi entries (directory) based on a set of arguments ($args):
1213
	 *   $id - View id
1214
	 *   $page_size - Page
1215
	 *   $sort_field - form field id to sort
1216
	 *   $sort_direction - ASC / DESC
1217
	 *   $start_date - Ymd
1218
	 *   $end_date - Ymd
1219
	 *   $class - assign a html class to the view
1220
	 *   $offset (optional) - This is the start point in the current data set (0 index based).
1221
	 *
1222
	 *
1223
	 *
1224
	 * @uses  gravityview_get_entries()
1225
	 * @access public
1226
	 * @param array $args\n
1227
	 *   - $id - View id
1228
	 *   - $page_size - Page
1229
	 *   - $sort_field - form field id to sort
1230
	 *   - $sort_direction - ASC / DESC
1231
	 *   - $start_date - Ymd
1232
	 *   - $end_date - Ymd
1233
	 *   - $class - assign a html class to the view
1234
	 *   - $offset (optional) - This is the start point in the current data set (0 index based).
1235
	 * @param int $form_id Gravity Forms Form ID
1236
	 * @return array Associative array with `count`, `entries`, and `paging` keys. `count` has the total entries count, `entries` is an array with Gravity Forms full entry data, `paging` is an array with `offset` and `page_size` keys
1237
	 */
1238 1
	public static function get_view_entries( $args, $form_id ) {
1239
1240 1
		do_action( 'gravityview_log_debug', '[get_view_entries] init' );
1241
		// start filters and sorting
1242
1243 1
		$parameters = self::get_view_entries_parameters( $args, $form_id );
1244
1245 1
		$count = 0; // Must be defined so that gravityview_get_entries can use by reference
1246
1247
		// fetch entries
1248 1
		if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
1249
			list( $entries, $paging, $count ) =
1250 1
				\GV\Mocks\GravityView_frontend_get_view_entries( $args, $form_id, $parameters, $count );
1251
		} else {
1252
			/** Deprecated, use $form->entries instead. */
1253
			$entries = gravityview_get_entries( $form_id, $parameters, $count );
1254
1255
			/** Set paging. */
1256
			$paging = rgar( $parameters, 'paging' );
1257
1258
			/** Adjust count by defined offset. */
1259
			$count = max( 0, ( $count - rgar( $args, 'offset', 0 ) ) );
1260
		}
1261
1262 1
		do_action( 'gravityview_log_debug', sprintf( '%s: Get Entries. Found: %s entries', __METHOD__, $count ), $entries );
1263
1264
		/**
1265
		 * @filter `gravityview_view_entries` Filter the entries output to the View
1266
		 * @deprecated since 1.5.2
1267
		 * @param array $args View settings associative array
1268
		 * @var array
1269
		 */
1270 1
		$entries = apply_filters( 'gravityview_view_entries', $entries, $args );
1271
1272
		$return = array(
1273 1
			'count' => $count,
1274 1
			'entries' => $entries,
1275 1
			'paging' => $paging,
1276
		);
1277
1278
		/**
1279
		 * @filter `gravityview/view/entries` Filter the entries output to the View
1280
		 * @param array $criteria associative array containing count, entries & paging
1281
		 * @param array $args View settings associative array
1282
		 * @since 1.5.2
1283
		 */
1284 1
		return apply_filters( 'gravityview/view/entries', $return, $args );
1285
	}
1286
1287
	/**
1288
	 * Get an array of search parameters formatted as Gravity Forms requires
1289
	 *
1290
	 * Results are filtered by `gravityview_get_entries` and `gravityview_get_entries_{View ID}` filters
1291
	 *
1292
	 * @uses GravityView_frontend::get_search_criteria
1293
	 * @uses GravityView_frontend::get_search_criteria_paging
1294
	 *
1295
	 * @since 1.20
1296
	 *
1297
	 * @see \GV\View_Settings::defaults For $args options
1298
	 *
1299
	 * @param array $args Array of View settings, as structured in \GV\View_Settings::defaults
1300
	 * @param int $form_id Gravity Forms form ID to search
1301
	 *
1302
	 * @return array With `search_criteria`, `sorting`, `paging`, `cache` keys
1303
	 */
1304
	public static function get_view_entries_parameters( $args = array(), $form_id = 0 ) {
1305
1306
1307
		if ( ! is_array( $args ) || ! is_numeric( $form_id ) ) {
1308
1309
			do_action( 'gravityview_log_error', __METHOD__ . ': Passed args are not an array or the form ID is not numeric' );
1310
1311
			return array();
1312
		}
1313
1314
		$form_id = intval( $form_id );
1315
1316
		/**
1317
		 * Process search parameters
1318
		 * @var array
1319
		 */
1320
		$search_criteria = self::get_search_criteria( $args, $form_id );
1321
1322
		$paging = self::get_search_criteria_paging( $args );
1323
1324
		$parameters = array(
1325
			'search_criteria' => $search_criteria,
1326
			'sorting' => self::updateViewSorting( $args, $form_id ),
1327
			'paging' => $paging,
1328
			'cache' => isset( $args['cache'] ) ? $args['cache'] : true,
1329
		);
1330
1331
		/**
1332
		 * @filter `gravityview_get_entries` Filter get entries criteria
1333
		 * @param array $parameters Array with `search_criteria`, `sorting` and `paging` keys.
1334
		 * @param array $args View configuration args. {
1335
		 *      @type int $id View id
1336
		 *      @type int $page_size Number of entries to show per page
1337
		 *      @type string $sort_field Form field id to sort
1338
		 *      @type string $sort_direction Sorting direction ('ASC' or 'DESC')
1339
		 *      @type string $start_date - Ymd
1340
		 *      @type string $end_date - Ymd
1341
		 *      @type string $class - assign a html class to the view
1342
		 *      @type string $offset (optional) - This is the start point in the current data set (0 index based).
1343
		 * }
1344
		 * @param int $form_id ID of Gravity Forms form
1345
		 */
1346
		$parameters = apply_filters( 'gravityview_get_entries', $parameters, $args, $form_id );
1347
1348
		/**
1349
		 * @filter `gravityview_get_entries_{View ID}` Filter get entries criteria
1350
		 * @param array $parameters Array with `search_criteria`, `sorting` and `paging` keys.
1351
		 * @param array $args View configuration args.
1352
		 */
1353
		$parameters = apply_filters( 'gravityview_get_entries_'.$args['id'], $parameters, $args, $form_id );
1354
1355
		do_action( 'gravityview_log_debug', __METHOD__ . ': $parameters passed to gravityview_get_entries(): ', $parameters );
1356
1357
		return $parameters;
1358
	}
1359
1360
	/**
1361
	 * Get the paging array for the View
1362
	 *
1363
	 * @since 1.19.5
1364
	 *
1365
	 * @param $args
1366
	 * @param int $form_id
1367
	 */
1368
	public static function get_search_criteria_paging( $args ) {
1369
1370
		/**
1371
		 * @filter `gravityview_default_page_size` The default number of entries displayed in a View
1372
		 * @since 1.1.6
1373
		 * @param int $default_page_size Default: 25
1374
		 */
1375
		$default_page_size = apply_filters( 'gravityview_default_page_size', 25 );
1376
1377
		// Paging & offset
1378
		$page_size = ! empty( $args['page_size'] ) ? intval( $args['page_size'] ) : $default_page_size;
1379
1380
		if ( -1 === $page_size ) {
1381
			$page_size = PHP_INT_MAX;
1382
		}
1383
1384
		$curr_page = empty( $_GET['pagenum'] ) ? 1 : intval( $_GET['pagenum'] );
1385
		$offset = ( $curr_page - 1 ) * $page_size;
1386
1387
		if ( ! empty( $args['offset'] ) ) {
1388
			$offset += intval( $args['offset'] );
1389
		}
1390
1391
		$paging = array(
1392
			'offset' => $offset,
1393
			'page_size' => $page_size,
1394
		);
1395
1396
		do_action( 'gravityview_log_debug', __METHOD__ . ': Paging: ', $paging );
1397
1398
		return $paging;
1399
	}
1400
1401
	/**
1402
	 * Updates the View sorting criteria
1403
	 *
1404
	 * @since 1.7
1405
	 *
1406
	 * @param array $args View settings. Required to have `sort_field` and `sort_direction` keys
1407
	 * @param int $form_id The ID of the form used to sort
1408
	 * @return array $sorting Array with `key`, `direction` and `is_numeric` keys
1409
	 */
1410
	public static function updateViewSorting( $args, $form_id ) {
1411
		$sorting = array();
1412
		$sort_field_id = isset( $_GET['sort'] ) ? $_GET['sort'] : rgar( $args, 'sort_field' );
1413
		$sort_direction = isset( $_GET['dir'] ) ? $_GET['dir'] : rgar( $args, 'sort_direction' );
1414
1415
		$sort_field_id = self::_override_sorting_id_by_field_type( $sort_field_id, $form_id );
1416
1417
		if ( ! empty( $sort_field_id ) ) {
1418
			$sorting = array(
1419
				'key' => $sort_field_id,
1420
				'direction' => strtolower( $sort_direction ),
1421
				'is_numeric' => GVCommon::is_field_numeric( $form_id, $sort_field_id )
1422
			);
1423
		}
1424
1425
		GravityView_View::getInstance()->setSorting( $sorting );
1426
1427
		do_action( 'gravityview_log_debug', '[updateViewSorting] Sort Criteria : ', $sorting );
1428
1429
		return $sorting;
1430
1431
	}
1432
1433
	/**
1434
	 * Override sorting per field
1435
	 *
1436
	 * Currently only modifies sorting ID when sorting by the full name. Sorts by first name.
1437
	 * Use the `gravityview/sorting/full-name` filter to override.
1438
	 *
1439
	 * @todo Filter from GravityView_Field
1440
	 * @since 1.7.4
1441
	 *
1442
	 * @param int|string $sort_field_id Field used for sorting (`id` or `1.2`)
1443
	 * @param int $form_id GF Form ID
1444
	 *
1445
	 * @return string Possibly modified sorting ID
1446
	 */
1447
	private static function _override_sorting_id_by_field_type( $sort_field_id, $form_id ) {
1448
1449
		$form = gravityview_get_form( $form_id );
1450
1451
		$sort_field = GFFormsModel::get_field( $form, $sort_field_id );
1452
1453
		if( ! $sort_field ) {
1454
			return $sort_field_id;
1455
		}
1456
1457
		switch ( $sort_field['type'] ) {
1458
1459
			case 'address':
1460
				// Sorting by full address
1461
				if ( floatval( $sort_field_id ) === floor( $sort_field_id ) ) {
1462
1463
					/**
1464
					 * Override how to sort when sorting address
1465
					 *
1466
					 * @since 1.8
1467
					 *
1468
					 * @param string $address_part `street`, `street2`, `city`, `state`, `zip`, or `country` (default: `city`)
1469
					 * @param string $sort_field_id Field used for sorting
1470
					 * @param int $form_id GF Form ID
1471
					 */
1472
					$address_part = apply_filters( 'gravityview/sorting/address', 'city', $sort_field_id, $form_id );
1473
1474
					switch( strtolower( $address_part ) ){
1475
						case 'street':
1476
							$sort_field_id .= '.1';
1477
							break;
1478
						case 'street2':
1479
							$sort_field_id .= '.2';
1480
							break;
1481
						default:
1482
						case 'city':
1483
							$sort_field_id .= '.3';
1484
							break;
1485
						case 'state':
1486
							$sort_field_id .= '.4';
1487
							break;
1488
						case 'zip':
1489
							$sort_field_id .= '.5';
1490
							break;
1491
						case 'country':
1492
							$sort_field_id .= '.6';
1493
							break;
1494
					}
1495
1496
				}
1497
				break;
1498
			case 'name':
1499
				// Sorting by full name, not first, last, etc.
1500
				if ( floatval( $sort_field_id ) === floor( $sort_field_id ) ) {
1501
					/**
1502
					 * @filter `gravityview/sorting/full-name` Override how to sort when sorting full name.
1503
					 * @since 1.7.4
1504
					 * @param[in,out] string $name_part Sort by `first` or `last` (default: `first`)
1505
					 * @param[in] string $sort_field_id Field used for sorting
1506
					 * @param[in] int $form_id GF Form ID
1507
					 */
1508
					$name_part = apply_filters( 'gravityview/sorting/full-name', 'first', $sort_field_id, $form_id );
1509
1510
					if ( 'last' === strtolower( $name_part ) ) {
1511
						$sort_field_id .= '.6';
1512
					} else {
1513
						$sort_field_id .= '.3';
1514
					}
1515
				}
1516
				break;
1517
			case 'list':
1518
				$sort_field_id = false;
1519
				break;
1520
			case 'time':
1521
1522
				/**
1523
				 * @filter `gravityview/sorting/time` Override how to sort when sorting time
1524
				 * @see GravityView_Field_Time
1525
				 * @since 1.14
1526
				 * @param[in,out] string $name_part Field used for sorting
1527
				 * @param[in] int $form_id GF Form ID
1528
				 */
1529
				$sort_field_id = apply_filters( 'gravityview/sorting/time', $sort_field_id, $form_id );
1530
				break;
1531
		}
1532
1533
		return $sort_field_id;
1534
	}
1535
1536
	/**
1537
	 * Verify if user requested a single entry view
1538
	 * @return boolean|string false if not, single entry slug if true
1539
	 */
1540 3
	public static function is_single_entry() {
1541
1542 3
		if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
1543 3
			$var_name = \GV\Entry::get_endpoint_name();
1544
		} else {
1545
			/** Deprecated. Use \GV\Entry::get_endpoint_name instead. */
1546
			$var_name = GravityView_Post_Types::get_entry_var_name();
1547
		}
1548
1549 3
		$single_entry = get_query_var( $var_name );
1550
1551
		/**
1552
		 * Modify the entry that is being displayed.
1553
		 *
1554
		 * @internal Should only be used by things like the oEmbed functionality.
1555
		 * @since 1.6
1556
		 */
1557 3
		$single_entry = apply_filters( 'gravityview/is_single_entry', $single_entry );
1558
1559 3
		if ( empty( $single_entry ) ){
1560
			return false;
1561
		} else {
1562 3
			return $single_entry;
1563
		}
1564
	}
1565
1566
1567
	/**
1568
	 * Register styles and scripts
1569
	 *
1570
	 * @access public
1571
	 * @return void
1572
	 */
1573 1
	public function add_scripts_and_styles() {
1574 1
		global $post, $posts;
1575
		// enqueue template specific styles
1576 1
		if ( $this->getGvOutputData() ) {
1577
1578 1
			if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
1579 1
				$views = gravityview()->views->all();
1580
			} else {
1581
				/** \GravityView_View_Data::get_view is no more... */
1582
				$views = $this->getGvOutputData()->get_views();
1583
			}
1584
1585 1
			foreach ( $views as $view_id => $data ) {
1586 1
				if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
1587 1
					$view = $data;
1588 1
					$view_id = $view->ID;
1589 1
					$template_id = $view->template ? $view->template->ID : null;
1590 1
					$data = $view->as_data();
1591
				} else {
1592
					$template_id = $data['template_id'];
1593
				}
1594
1595
				/**
1596
				 * Don't enqueue the scripts or styles if it's not going to be displayed.
1597
				 * @since 1.15
1598
				 */
1599 1
				if( is_user_logged_in() && false === GVCommon::has_cap( 'read_gravityview', $view_id ) ) {
1600
					continue;
1601
				}
1602
1603
				// By default, no thickbox
1604 1
				$js_dependencies = array( 'jquery', 'gravityview-jquery-cookie' );
1605 1
				$css_dependencies = array();
1606
1607 1
				if ( defined( 'GRAVITYVIEW_FUTURE_CORE_LOADED' ) ) {
1608 1
					$lightbox = $view->settings->get( 'lightbox' );
1609
				} else {
1610
					/** View data attributes are now stored in \GV\View::$settings */
1611
					$lightbox = ! empty( $data['atts']['lightbox'] );
1612
				}
1613
1614
				// If the thickbox is enqueued, add dependencies
1615 1
				if ( $lightbox ) {
1616
1617
					/**
1618
					 * @filter `gravity_view_lightbox_script` Override the lightbox script to enqueue. Default: `thickbox`
1619
					 * @param string $script_slug If you want to use a different lightbox script, return the name of it here.
1620
					 */
1621 1
					$js_dependencies[] = apply_filters( 'gravity_view_lightbox_script', 'thickbox' );
1622
1623
					/**
1624
					 * @filter `gravity_view_lightbox_style` Modify the lightbox CSS slug. Default: `thickbox`
1625
					 * @param string $script_slug If you want to use a different lightbox script, return the name of its CSS file here.
1626
					 */
1627 1
					$css_dependencies[] = apply_filters( 'gravity_view_lightbox_style', 'thickbox' );
1628
				}
1629
1630
				/**
1631
				 * If the form has checkbox fields, enqueue dashicons
1632
				 * @see https://github.com/katzwebservices/GravityView/issues/536
1633
				 * @since 1.15
1634
				 */
1635 1
				if( gravityview_view_has_single_checkbox_or_radio( $data['form'], $data['fields'] ) ) {
1636
					$css_dependencies[] = 'dashicons';
1637
				}
1638
1639 1
				wp_register_script( 'gravityview-jquery-cookie', plugins_url( 'assets/lib/jquery.cookie/jquery.cookie.min.js', GRAVITYVIEW_FILE ), array( 'jquery' ), GravityView_Plugin::version, true );
1640
1641 1
				$script_debug = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
1642
1643 1
				wp_register_script( 'gravityview-fe-view', plugins_url( 'assets/js/fe-views' . $script_debug . '.js', GRAVITYVIEW_FILE ), apply_filters( 'gravityview_js_dependencies', $js_dependencies ) , GravityView_Plugin::version, true );
1644
1645 1
				wp_enqueue_script( 'gravityview-fe-view' );
1646
1647 1
				if ( ! empty( $data['atts']['sort_columns'] ) ) {
1648
					wp_enqueue_style( 'gravityview_font', plugins_url( 'assets/css/font.css', GRAVITYVIEW_FILE ), $css_dependencies, GravityView_Plugin::version, 'all' );
1649
				}
1650
1651 1
				$this->enqueue_default_style( $css_dependencies );
1652
1653 1
				self::add_style( $template_id );
1654
			}
1655
1656 1
			if ( 'wp_print_footer_scripts' === current_filter() ) {
1657
1658
				$js_localization = array(
1659
					'cookiepath' => COOKIEPATH,
1660
					'clear' => _x( 'Clear', 'Clear all data from the form', 'gravityview' ),
1661
					'reset' => _x( 'Reset', 'Reset the search form to the state that existed on page load', 'gravityview' ),
1662
				);
1663
1664
				/**
1665
				 * @filter `gravityview_js_localization` Modify the array passed to wp_localize_script()
1666
				 * @param array $js_localization The data padded to the Javascript file
1667
				 * @param array $views Array of View data arrays with View settings
1668
				 */
1669
				$js_localization = apply_filters( 'gravityview_js_localization', $js_localization, $views );
1670
1671
				wp_localize_script( 'gravityview-fe-view', 'gvGlobals', $js_localization );
1672
			}
1673
		}
1674 1
	}
1675
1676
	/**
1677
	 * Handle enqueuing the `gravityview_default_style` stylesheet
1678
	 *
1679
	 * @since 1.17
1680
	 *
1681
	 * @param array $css_dependencies Dependencies for the `gravityview_default_style` stylesheet
1682
	 *
1683
	 * @return void
1684
	 */
1685
	private function enqueue_default_style( $css_dependencies = array() ) {
1686
1687
		/**
1688
		 * @filter `gravityview_use_legacy_search_css` Should GravityView use the legacy Search Bar stylesheet (from before Version 1.17)?
1689
		 * @since 1.17
1690
		 * @param bool $use_legacy_search_style If true, loads `gv-legacy-search(-rtl).css`. If false, loads `gv-default-styles(-rtl).css`. `-rtl` is added on RTL websites. Default: `false`
1691
		 */
1692
		$use_legacy_search_style = apply_filters( 'gravityview_use_legacy_search_style', false );
1693
1694
		$rtl = is_rtl() ? '-rtl' : '';
1695
1696
		$css_file_base = $use_legacy_search_style ? 'gv-legacy-search' : 'gv-default-styles';
1697
1698
		$path = gravityview_css_url( $css_file_base . $rtl . '.css' );
1699
1700
		wp_enqueue_style( 'gravityview_default_style', $path, $css_dependencies, GravityView_Plugin::version, 'all' );
1701
	}
1702
1703
	/**
1704
	 * Add template extra style if exists
1705
	 * @param string $template_id
1706
	 */
1707
	public static function add_style( $template_id ) {
1708
1709
		if ( ! empty( $template_id ) && wp_style_is( 'gravityview_style_' . $template_id, 'registered' ) ) {
1710
			do_action( 'gravityview_log_debug', sprintf( '[add_style] Adding extra template style for %s', $template_id ) );
1711
			wp_enqueue_style( 'gravityview_style_' . $template_id );
1712
		} elseif ( empty( $template_id ) ) {
1713
			do_action( 'gravityview_log_error', '[add_style] Cannot add template style; template_id is empty' );
1714
		} else {
1715
			do_action( 'gravityview_log_error', sprintf( '[add_style] Cannot add template style; %s is not registered', 'gravityview_style_'.$template_id ) );
1716
		}
1717
1718
	}
1719
1720
1721
	/**
1722
	 * Inject the sorting links on the table columns
1723
	 *
1724
	 * Callback function for hook 'gravityview/template/field_label'
1725
	 * @see GravityView_API::field_label() (in includes/class-api.php)
1726
	 *
1727
	 * @since 1.7
1728
	 *
1729
	 * @param string $label Field label
1730
	 * @param array $field Field settings
1731
	 *
1732
	 * @return string Field Label
1733
	 */
1734
	public function add_columns_sort_links( $label = '', $field, $form ) {
1735
1736
		/**
1737
		 * Not a table-based template; don't add sort icons
1738
		 * @since 1.12
1739
		 */
1740
		if( ! preg_match( '/table/ism', GravityView_View::getInstance()->getTemplatePartSlug() ) ) {
1741
			return $label;
1742
		}
1743
1744
		if ( ! $this->is_field_sortable( $field['id'], $form ) ) {
1745
			return $label;
1746
		}
1747
1748
		$sorting = GravityView_View::getInstance()->getSorting();
1749
1750
		$class = 'gv-sort';
1751
1752
		$sort_field_id = self::_override_sorting_id_by_field_type( $field['id'], $form['id'] );
1753
1754
		$sort_args = array(
1755
			'sort' => $field['id'],
1756
			'dir' => 'asc',
1757
		);
1758
1759
		if ( ! empty( $sorting['key'] ) && (string) $sort_field_id === (string) $sorting['key'] ) {
1760
			//toggle sorting direction.
1761
			if ( 'asc' === $sorting['direction'] ) {
1762
				$sort_args['dir'] = 'desc';
1763
				$class .= ' gv-icon-sort-desc';
1764
			} else {
1765
				$sort_args['dir'] = 'asc';
1766
				$class .= ' gv-icon-sort-asc';
1767
			}
1768
		} else {
1769
			$class .= ' gv-icon-caret-up-down';
1770
		}
1771
1772
		$url = add_query_arg( $sort_args, remove_query_arg( array('pagenum') ) );
1773
1774
		return '<a href="'. esc_url_raw( $url ) .'" class="'. $class .'" ></a>&nbsp;'. $label;
1775
1776
	}
1777
1778
	/**
1779
	 * Checks if field (column) is sortable
1780
	 *
1781
	 * @param string $field Field settings
1782
	 * @param array $form Gravity Forms form array
1783
	 *
1784
	 * @since 1.7
1785
	 *
1786
	 * @return bool True: Yes, field is sortable; False: not sortable
1787
	 */
1788
	public function is_field_sortable( $field_id = '', $form = array() ) {
1789
1790
		$field_type = $field_id;
1791
1792
		if( is_numeric( $field_id ) ) {
1793
			$field = GFFormsModel::get_field( $form, $field_id );
1794
			$field_type = $field->type;
1795
		}
1796
1797
		$not_sortable = array(
1798
			'edit_link',
1799
			'delete_link',
1800
		);
1801
1802
		/**
1803
		 * @filter `gravityview/sortable/field_blacklist` Modify what fields should never be sortable.
1804
		 * @since 1.7
1805
		 * @param[in,out] array $not_sortable Array of field types that aren't sortable
1806
		 * @param string $field_type Field type to check whether the field is sortable
1807
		 * @param array $form Gravity Forms form
1808
		 */
1809
		$not_sortable = apply_filters( 'gravityview/sortable/field_blacklist', $not_sortable, $field_type, $form );
1810
1811
		if ( in_array( $field_type, $not_sortable ) ) {
1812
			return false;
1813
		}
1814
1815
		return apply_filters( "gravityview/sortable/formfield_{$form['id']}_{$field_id}", apply_filters( "gravityview/sortable/field_{$field_id}", true, $form ) );
1816
1817
	}
1818
1819
}
1820
1821
GravityView_frontend::getInstance();
1822
1823
1824
1825