Completed
Push — master ( 4ba683...3a59f6 )
by Zack
31:21 queued 27:45
created

GVLogic_Shortcode::parse_atts()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
nc 12
nop 0
dl 0
loc 39
ccs 13
cts 13
cp 1
crap 5
rs 8.9848
c 0
b 0
f 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 6 and the first side effect is on line 444.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
/**
4
 * Shortcode to handle showing/hiding content in merge tags. Works great with GravityView Custom Content fields
5
 */
6
class GVLogic_Shortcode {
0 ignored issues
show
Coding Style introduced by
Since you have declared the constructor as private, maybe you should also declare the class as final.
Loading history...
7
8
	private static $SUPPORTED_SCALAR_OPERATORS = array( 'is', 'isnot', 'contains', 'starts_with', 'ends_with' );
9
10
	private static $SUPPORTED_NUMERIC_OPERATORS = array( 'greater_than', 'less_than' );
11
12
	private static $SUPPORTED_ARRAY_OPERATORS = array( 'in', 'not_in', 'isnot', 'contains' );
13
14
	private static $SUPPORTED_CUSTOM_OPERATORS = array( 'equals', 'greater_than_or_is', 'greater_than_or_equals', 'less_than_or_is', 'less_than_or_equals', 'not_contains' );
15
16
	/**
17
	 * Attributes passed to the shortcode
18
	 * @var array
19
	 */
20
	var $passed_atts;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $passed_atts.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
21
22
	/**
23
	 * Content inside the shortcode, displayed if matched
24
	 * @var string
25
	 */
26
	var $passed_content;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $passed_content.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
27
28
	/**
29
	 * Parsed attributes
30
	 * @var array
31
	 */
32
	var $atts = array();
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $atts.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
33
34
	/**
35
	 * Parsed content, shown if matched
36
	 * @var string
37
	 */
38
	var $content = '';
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $content.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
39
40
	/**
41
	 * Content shown if not matched
42
	 * This is set by having `[else]` inside the $content block
43
	 * @var string
44
	 */
45
	var $else_content = '';
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $else_content.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
46
47
	/**
48
	 * The current shortcode name being processed
49
	 * @var string
50
	 */
51
	var $shortcode = 'gvlogic';
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $shortcode.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
52
53
	/**
54
	 * The left side of the comparison
55
	 * @var string
56
	 */
57
	var $if = '';
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $if.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
58
59
	/**
60
	 * Special logged_in condition.
61
	 * @since 2.3
62
	 * @var bool
63
	 */
64
	var $logged_in = null;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $logged_in.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
65
66
	/**
67
	 * The right side of the comparison
68
	 * @var string
69
	 */
70
	var $comparison = '';
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $comparison.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
71
72
	/**
73
	 * The comparison operator
74
	 * @since 1.21.5
75
	 * @since 2.0 Changed default from "is" to "isnot"
76
	 * @var string
77
	 */
78
	var $operation = 'isnot';
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $operation.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
79
80
	/**
81
	 * Does the comparison pass?
82
	 * @var bool
83
	 */
84
	var $is_match = false;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $is_match.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
85
86
	/**
87
	 * @var GVLogic_Shortcode
88
	 */
89
	private static $instance;
90
91
	/**
92
	 * Instantiate!
93
	 * @return GVLogic_Shortcode
94
	 */
95 1
	public static function get_instance() {
96
97 1
		if( empty( self::$instance ) ) {
98
			self::$instance = new self;
99
		}
100
101 1
		return self::$instance;
102
	}
103
104
	/**
105
	 * Add the WordPress hooks
106
	 * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
107
	 */
108
	private function __construct() {
109
		$this->add_hooks();
110
	}
111
112
	/**
113
	 * Register the shortcode
114
	 * @return void
115
	 */
116
	private function add_hooks() {
117
		add_shortcode( 'gvlogic', array( $this, 'shortcode' ) );
118
		add_shortcode( 'gvlogicelse', array( $this, 'shortcode' ) );
119
	}
120
121
	/**
122
	 * Get array of supported operators
123
	 * @param bool $with_values
124
	 *
125
	 * @return array
126
	 */
127 21
	private function get_operators( $with_values = false ) {
128
129 21
		$operators = array_merge( self::$SUPPORTED_ARRAY_OPERATORS, self::$SUPPORTED_NUMERIC_OPERATORS, self::$SUPPORTED_SCALAR_OPERATORS, self::$SUPPORTED_CUSTOM_OPERATORS );
130
131 21
		if( $with_values ) {
132 21
			$operators_with_values = array();
133 21
			foreach( $operators as $key ) {
134 21
				$operators_with_values[ $key ] = '';
135
			}
136 21
			return $operators_with_values;
137
		} else {
138 21
			return $operators;
139
		}
140
	}
141
142
	/**
143
	 * Set the operation for the shortcode.
144
	 * @param string $operation
145
	 *
146
	 * @return bool True: it's an allowed operation type and was added. False: invalid operation type
147
	 */
148 21
	private function set_operation( $operation = 'isnot' ) {
149
150 21
		$operators = $this->get_operators( false );
151
152 21
		if( !in_array( $operation, $operators ) ) {
0 ignored issues
show
introduced by
Expected 1 space after "!"; 0 found
Loading history...
153 1
			gravityview()->log->debug( ' Attempted to add invalid operation type. {operation}', array( 'operation' => $operation ) );
154 1
			return false;
155
		}
156
157 21
		$this->operation = $operation;
158 21
		return true;
159
	}
160
161
	/**
162
	 * Set the operation and comparison for the shortcode
163
	 *
164
	 * Loop through each attribute passed to the shortcode and see if it's a valid operator. If so, set it.
165
	 * Example: [gvlogic if="{example}" greater_than="5"]
166
	 * `greater_than` will be set as the operator
167
	 * `5` will be set as the comparison value
168
	 *
169
	 * @return bool True: we've got an operation and comparison value; False: no, we don't
170
	 */
171 21
	private function setup_operation_and_comparison() {
172
173 21
		if ( empty( $this->atts ) ) {
174 3
			return true;
175
		}
176
177 21
		foreach ( $this->atts as $key => $value ) {
178
179 21
			$valid = $this->set_operation( $key == 'else' ? 'isnot' : $key );
180
181 21
			if ( $valid ) {
182 21
				$this->comparison = $key == 'else' ? '' : $value;
183 21
				return true;
184
			}
185
		}
186
187 1
		return false;
188
	}
189
190
	/**
191
	 * @param array $atts User defined attributes in shortcode tag.
192
	 * @param null $content
193
	 * @param string $shortcode_tag
194
	 *
195
	 * @return string|null
196
	 */
197 22
	public function shortcode( $atts = array(), $content = NULL, $shortcode_tag = '' ) {
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected null, but found NULL.
Loading history...
198
199
		// Don't process except on frontend
200 22
		if ( gravityview()->request->is_admin() ) {
201
			return null;
202
		}
203
204 22
		if( empty( $atts ) ) {
205
			gravityview()->log->error( '$atts are empty.', array( 'data' => $atts ) );
206
			return null;
207
		}
208
209 22
		$this->passed_atts = $atts;
210 22
		$this->passed_content = $content;
211 22
		$this->content = '';
212 22
		$this->else_content = '';
213 22
		$this->atts = array();
214 22
		$this->shortcode = $shortcode_tag;
215
216 22
		$this->parse_atts();
217
218
		// Logged in operation
219 22
		if ( ! is_null( $this->logged_in ) ) {
220 1
			$this->setup_operation_and_comparison();
221 21
		} else if ( false === $this->if ) {
222
			gravityview()->log->error( '$atts->if is empty.', array( 'data' => $this->passed_atts ) );
223
			return null;
224
		} else {
225 21
			$setup = $this->setup_operation_and_comparison();
226
227
			// We need an operation and comparison value
228 21
			if( ! $setup ) {
229
				gravityview()->log->error( 'No valid operators were passed.', array( 'data' => $this->atts ) );
230
				return null;
231
			}
232
		}
233
234
		// Check if it's a match
235 22
		$this->set_is_match();
236
237
		// Set the content and else_content
238 22
		$this->set_content_and_else_content();
239
240
		// Return the value!
241 22
		$output = $this->get_output();
242
243 22
		$this->reset();
244
245 22
		return $output;
246
	}
247
248
	/**
249
	 * Restore the original settings for the shortcode
250
	 *
251
	 * @since 2.0 Needed because $atts can now be empty
252
	 *
253
	 * @return void
254
	 */
255 21
	private function reset() {
256 21
		$this->operation = 'isnot';
257 21
		$this->comparison = '';
258 21
		$this->passed_atts = array();
259 21
		$this->passed_content = '';
260 21
	}
261
262
	/**
263
	 * Does the if and the comparison match?
264
	 * @uses GVCommon::matches_operation
265
	 *
266
	 * @return void
267
	 */
268 21
	private function set_is_match() {
269
270 21
		$comparison_match = GVCommon::matches_operation( $this->if, $this->comparison, $this->operation );
271
272 21
		if ( is_null( $this->logged_in ) ) {
273 20
			$this->is_match = $comparison_match;
274 20
			return;
275
		}
276
277 1
		$logged_in_match = ! $this->logged_in ^ is_user_logged_in(); // XNOR
278
279
		// Only logged-in match
280 1
		if( 1 === sizeof( $this->passed_atts ) ) {
281 1
			$this->is_match = $logged_in_match;
282
		} else {
283 1
			$this->is_match = $logged_in_match && $comparison_match;
284
		}
285 1
	}
286
287
	/**
288
	 * Get the output for the shortcode, based on whether there's a matched value
289
	 *
290
	 * @return string HTML/Text output of the shortcode
291
	 */
292 21
	private function get_output() {
293
294 21
		if( $this->is_match ) {
295 13
			$output = $this->content;
296
		} else {
297 13
			$output = $this->else_content;
298
		}
299
300
		// Get recursive!
301 21
		$output = do_shortcode( $output );
302
303 21
		if ( class_exists( 'GFCommon' ) ) {
304 21
			$output = GFCommon::replace_variables( $output, array(), array(), false, true, false );
305
		}
306
307
		/**
308
		 * @filter `gravityview/gvlogic/output` Modify the [gvlogic] output
309
		 * @param string $output HTML/text output
310
		 * @param GVLogic_Shortcode $this This class
311
		 */
312 21
		$output = apply_filters('gravityview/gvlogic/output', $output, $this );
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after opening bracket; 0 found
Loading history...
313
314 21
		gravityview()->log->debug( 'Output: ', array( 'data' => $output ) );
315
316 21
		return $output;
317
	}
318
319
	/**
320
	 * Check for `[else]` tag inside the shortcode content. If exists, set the else_content variable.
321
	 * If not, use the `else` attribute passed by the shortcode, if exists.
322
	 *
323
	 * @return void
324
	 */
325 22
	private function set_content_and_else_content() {
326
327 22
		$passed_content = $this->passed_content;
328
329 22
		list( $before_else, $after_else ) = array_pad( explode( '[else]', $passed_content ), 2, NULL );
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected null, but found NULL.
Loading history...
330 22
		list( $before_else_if, $after_else_if ) = array_pad( explode( '[else', $passed_content ), 2, NULL );
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected null, but found NULL.
Loading history...
331
332 22
		$else_attr = isset( $this->atts['else'] ) ? $this->atts['else'] : NULL;
0 ignored issues
show
Coding Style introduced by
TRUE, FALSE and NULL must be lowercase; expected null, but found NULL.
Loading history...
333 22
		$else_content = isset( $after_else ) ? $after_else : $else_attr;
334
335
		// The content is everything OTHER than the [else]
336 22
		$this->content = $before_else_if;
337
338 22
		if ( ! $this->is_match ) {
339 14
			if( $elseif_content = $this->process_elseif( $before_else ) ) {
340 1
				$this->else_content = $elseif_content;
341
			} else {
342 14
				$this->else_content = $else_content;
343
			}
344
		}
345 22
	}
346
347
	/**
348
	 * Handle additional conditional logic inside the [else] pseudo-shortcode
349
	 *
350
	 * @since 1.21.2
351
	 *
352
	 * @param string $before_else Shortcode content before the [else] tag (if it exists)
353
	 *
354
	 * @return bool|string False: No [else if] statements found. Otherwise, return the matched content.
355
	 */
356 14
	private function process_elseif( $before_else ) {
357
358 14
		$regex = get_shortcode_regex( array( 'else' ) );
359
360
		// 2. Check if there are any ELSE IF statements
361 14
		preg_match_all( '/' . $regex . '/', $before_else . '[/else]', $else_if_matches, PREG_SET_ORDER );
362
363
		// 3. The ELSE IF statements that remain should be processed to see if they are valid
364 14
		foreach ( $else_if_matches as $key => $else_if_match ) {
365
366
			// If $else_if_match[5] exists and has content, check for more shortcodes
367 1
			preg_match_all( '/' . $regex . '/', $else_if_match[5] . '[/else]', $recursive_matches, PREG_SET_ORDER );
368
369
			// If the logic passes, this is the value that should be used for $this->else_content
370 1
			$else_if_value = $else_if_match[5];
371 1
			$check_elseif_match = $else_if_match[0];
372
373
			// Retrieve the value of the match that is currently being checked, without any other [else] tags
374 1
			if( ! empty( $recursive_matches[0][0] ) ) {
375 1
				$else_if_value = str_replace( $recursive_matches[0][0], '', $else_if_value );
376 1
				$check_elseif_match = str_replace( $recursive_matches[0][0], '', $check_elseif_match );
377
			}
378
379 1
			$check_elseif_match = str_replace( '[else', '[gvlogicelse', $check_elseif_match );
380 1
			$check_elseif_match = str_replace( '[/else', '[/gvlogicelse', $check_elseif_match );
381
382
			// Make sure to close the tag
383 1
			if ( '[/gvlogicelse]' !== substr( $check_elseif_match, -14, 14 ) ) {
384 1
				$check_elseif_match .= '[/gvlogicelse]';
385
			}
386
387
			// The shortcode returned a value; it was a match
388 1
			if ( $result = do_shortcode( $check_elseif_match ) ) {
389 1
				return $else_if_value;
390
			}
391
392
			// Process any remaining [else] tags
393 1
			return $this->process_elseif( $else_if_match[5] );
394
		}
395
396 14
		return false;
397
	}
398
399
	/**
400
	 * Process the attributes passed to the shortcode. Make sure they're valid
401
	 * @return void
402
	 */
403 21
	private function parse_atts() {
404
405
		$supported = array(
406 21
			'if' => false,
407
			'else' => false,
408
			'logged_in' => null,
409
		);
410
411 21
		$supported_args = $supported + $this->get_operators( true );
412
413
		// Whittle down the attributes to only valid pairs
414 21
		$this->atts = shortcode_atts( $supported_args, $this->passed_atts, $this->shortcode );
415
416
		// Only keep the passed attributes after making sure that they're valid pairs
417 21
		$this->atts = array_intersect_key( $this->passed_atts, $this->atts );
418
419
		// Strip whitespace if it's not default false
420 21
		$this->if = ( isset( $this->atts['if'] ) && is_string( $this->atts['if'] ) ) ? trim( $this->atts['if'] ) : false;
421
422 21
		if ( isset( $this->atts['logged_in'] ) ) {
423
			// Truthy
424 1
			if ( in_array( strtolower( $this->atts['logged_in'] ), array( '0', 'false', 'no' ) ) ) {
425 1
				$this->logged_in = false;
426
			} else {
427 1
				$this->logged_in = true;
428
			}
429
		}
430
431
		/**
432
		 * @action `gravityview/gvlogic/parse_atts/after` Modify shortcode attributes after it's been parsed
433
		 * @see https://gist.github.com/zackkatz/def9b295b80c4ae109760ffba200f498 for an example
434
		 * @since 1.21.5
435
		 * @param GVLogic_Shortcode $this The GVLogic_Shortcode instance
436
		 */
437 21
		do_action( 'gravityview/gvlogic/parse_atts/after', $this );
438
439
		// Make sure the "if" isn't processed in self::setup_operation_and_comparison()
440 21
		unset( $this->atts['if'] );
441 21
	}
442
}
443
444
GVLogic_Shortcode::get_instance();
445