Completed
Push — develop ( 52a281...18a26b )
by Gennady
18:27
created

GVLogic_Shortcode::parse_atts()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
nc 12
nop 0
dl 0
loc 39
ccs 5
cts 5
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 432.

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 develop
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 1
	 */
89
	private static $instance;
90 1
91
	/**
92
	 * Instantiate!
93
	 * @return GVLogic_Shortcode
94 1
	 */
95
	public static function get_instance() {
96
97
		if( empty( self::$instance ) ) {
98
			self::$instance = new self;
99
		}
100
101
		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 4
121
	/**
122 4
	 * Get array of supported operators
123
	 * @param bool $with_values
124 4
	 *
125 4
	 * @return array
126 4
	 */
127 4
	private function get_operators( $with_values = false ) {
128
129 4
		$operators = array_merge( self::$SUPPORTED_ARRAY_OPERATORS, self::$SUPPORTED_NUMERIC_OPERATORS, self::$SUPPORTED_SCALAR_OPERATORS, self::$SUPPORTED_CUSTOM_OPERATORS );
130
131 4
		if( $with_values ) {
132
			$operators_with_values = array();
133
			foreach( $operators as $key ) {
134
				$operators_with_values[ $key ] = '';
135
			}
136
			return $operators_with_values;
137
		} else {
138
			return $operators;
139
		}
140
	}
141 4
142
	/**
143 4
	 * Set the operation for the shortcode.
144
	 * @param string $operation
145 4
	 *
146
	 * @return bool True: it's an allowed operation type and was added. False: invalid operation type
147
	 */
148
	private function set_operation( $operation = 'isnot' ) {
149
150 4
		$operators = $this->get_operators( false );
151 4
152
		if( !in_array( $operation, $operators ) ) {
0 ignored issues
show
introduced by
Expected 1 space after "!"; 0 found
Loading history...
153
			gravityview()->log->debug( ' Attempted to add invalid operation type. {operation}', array( 'operation' => $operation ) );
154
			return false;
155
		}
156
157
		$this->operation = $operation;
158
		return true;
159
	}
160
161
	/**
162
	 * Set the operation and comparison for the shortcode
163
	 *
164 4
	 * 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 4
	 * `greater_than` will be set as the operator
167 2
	 * `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 4
	 */
171
	private function setup_operation_and_comparison() {
172 4
173
		if ( empty( $this->atts ) ) {
174 4
			return true;
175 4
		}
176 4
177
		foreach ( $this->atts as $key => $value ) {
178
179
			$valid = $this->set_operation( $key == 'else' ? 'isnot' : $key );
180
181
			if ( $valid ) {
182
				$this->comparison = $key == 'else' ? '' : $value;
183
				return true;
184
			}
185
		}
186
187
		return false;
188
	}
189
190 5
	/**
191
	 * @param array $atts User defined attributes in shortcode tag.
192
	 * @param null $content
193 5
	 * @param string $shortcode_tag
194
	 *
195
	 * @return string|null
196
	 */
197 5
	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
		if ( gravityview()->request->is_admin() ) {
201
			return null;
202 5
		}
203 5
204 5
		if( empty( $atts ) ) {
205 5
			gravityview()->log->error( '$atts are empty.', array( 'data' => $atts ) );
206 5
			return null;
207 5
		}
208
209 5
		$this->passed_atts = $atts;
210
		$this->passed_content = $content;
211
		$this->content = '';
212 5
		$this->else_content = '';
213
		$this->atts = array();
214
		$this->shortcode = $shortcode_tag;
215
216
		$this->parse_atts();
217 5
218
		// Logged in operation
219
		if ( ! is_null( $this->logged_in ) ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
220 5
		} else if ( false === $this->if ) {
221
			gravityview()->log->error( '$atts->if is empty.', array( 'data' => $this->passed_atts ) );
222
			return null;
223
		} else {
224
			$setup = $this->setup_operation_and_comparison();
225
226 5
			// We need an operation and comparison value
227
			if( ! $setup ) {
228
				gravityview()->log->error( 'No valid operators were passed.', array( 'data' => $this->atts ) );
229 5
				return null;
230
			}
231
		}
232 5
233
		// Check if it's a match
234 5
		$this->set_is_match();
235
236 5
		// Set the content and else_content
237
		$this->set_content_and_else_content();
238
239
		// Return the value!
240
		$output = $this->get_output();
241
242
		$this->reset();
243
244
		return $output;
245
	}
246 4
247 4
	/**
248 4
	 * Restore the original settings for the shortcode
249 4
	 *
250 4
	 * @since 2.0 Needed because $atts can now be empty
251 4
	 *
252
	 * @return void
253
	 */
254
	private function reset() {
255
		$this->operation = 'isnot';
256
		$this->comparison = '';
257
		$this->passed_atts = array();
258
		$this->passed_content = '';
259 4
	}
260 4
261 4
	/**
262
	 * Does the if and the comparison match?
263
	 * @uses GVCommon::matches_operation
264
	 *
265
	 * @return void
266
	 */
267
	private function set_is_match() {
268 4
		if ( ! is_null( $this->logged_in ) ) {
269
			$this->is_match = ! $this->logged_in ^ is_user_logged_in(); // XNOR
270 4
		} else {
271 4
			$this->is_match = GVCommon::matches_operation( $this->if, $this->comparison, $this->operation );
272
		}
273 4
	}
274
275
	/**
276
	 * Get the output for the shortcode, based on whether there's a matched value
277 4
	 *
278
	 * @return string HTML/Text output of the shortcode
279 4
	 */
280 4
	private function get_output() {
281
282
		if( $this->is_match ) {
283
			$output = $this->content;
284
		} else {
285
			$output = $this->else_content;
286
		}
287
288 4
		// Get recursive!
289
		$output = do_shortcode( $output );
290 4
291
		if ( class_exists( 'GFCommon' ) ) {
292 4
			$output = GFCommon::replace_variables( $output, array(), array(), false, true, false );
293
		}
294
295
		/**
296
		 * @filter `gravityview/gvlogic/output` Modify the [gvlogic] output
297
		 * @param string $output HTML/text output
298
		 * @param GVLogic_Shortcode $this This class
299
		 */
300
		$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...
301 5
302
		gravityview()->log->debug( 'Output: ', array( 'data' => $output ) );
303 5
304
		return $output;
305 5
	}
306 5
307
	/**
308 5
	 * Check for `[else]` tag inside the shortcode content. If exists, set the else_content variable.
309 5
	 * If not, use the `else` attribute passed by the shortcode, if exists.
310
	 *
311
	 * @return void
312 5
	 */
313
	private function set_content_and_else_content() {
314 5
315 5
		$passed_content = $this->passed_content;
316 1
317
		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...
318 5
		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...
319
320
		$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...
321 5
		$else_content = isset( $after_else ) ? $after_else : $else_attr;
322
323
		// The content is everything OTHER than the [else]
324
		$this->content = $before_else_if;
325
326
		if ( ! $this->is_match ) {
327
			if( $elseif_content = $this->process_elseif( $before_else ) ) {
328
				$this->else_content = $elseif_content;
329
			} else {
330
				$this->else_content = $else_content;
331
			}
332 5
		}
333
	}
334 5
335
	/**
336
	 * Handle additional conditional logic inside the [else] pseudo-shortcode
337 5
	 *
338
	 * @since 1.21.2
339
	 *
340 5
	 * @param string $before_else Shortcode content before the [else] tag (if it exists)
341
	 *
342
	 * @return bool|string False: No [else if] statements found. Otherwise, return the matched content.
343 1
	 */
344
	private function process_elseif( $before_else ) {
345
346 1
		$regex = get_shortcode_regex( array( 'else' ) );
347 1
348
		// 2. Check if there are any ELSE IF statements
349
		preg_match_all( '/' . $regex . '/', $before_else . '[/else]', $else_if_matches, PREG_SET_ORDER );
350 1
351 1
		// 3. The ELSE IF statements that remain should be processed to see if they are valid
352 1
		foreach ( $else_if_matches as $key => $else_if_match ) {
353
354
			// If $else_if_match[5] exists and has content, check for more shortcodes
355 1
			preg_match_all( '/' . $regex . '/', $else_if_match[5] . '[/else]', $recursive_matches, PREG_SET_ORDER );
356 1
357
			// If the logic passes, this is the value that should be used for $this->else_content
358
			$else_if_value = $else_if_match[5];
359 1
			$check_elseif_match = $else_if_match[0];
360 1
361
			// Retrieve the value of the match that is currently being checked, without any other [else] tags
362
			if( ! empty( $recursive_matches[0][0] ) ) {
363
				$else_if_value = str_replace( $recursive_matches[0][0], '', $else_if_value );
364 1
				$check_elseif_match = str_replace( $recursive_matches[0][0], '', $check_elseif_match );
365 1
			}
366
367
			$check_elseif_match = str_replace( '[else', '[gvlogicelse', $check_elseif_match );
368
			$check_elseif_match = str_replace( '[/else', '[/gvlogicelse', $check_elseif_match );
369 1
370
			// Make sure to close the tag
371
			if ( '[/gvlogicelse]' !== substr( $check_elseif_match, -14, 14 ) ) {
372 5
				$check_elseif_match .= '[/gvlogicelse]';
373
			}
374
375
			// The shortcode returned a value; it was a match
376
			if ( $result = do_shortcode( $check_elseif_match ) ) {
377
				return $else_if_value;
378
			}
379 4
380
			// Process any remaining [else] tags
381
			return $this->process_elseif( $else_if_match[5] );
382 4
		}
383
384
		return false;
385
	}
386 4
387
	/**
388
	 * Process the attributes passed to the shortcode. Make sure they're valid
389 4
	 * @return void
390
	 */
391
	private function parse_atts() {
392 4
393
		$supported = array(
394
			'if' => false,
395 4
			'else' => false,
396
			'logged_in' => null,
397
		);
398
399
		$supported_args = $supported + $this->get_operators( true );
400
401
		// Whittle down the attributes to only valid pairs
402
		$this->atts = shortcode_atts( $supported_args, $this->passed_atts, $this->shortcode );
403 4
404
		// Only keep the passed attributes after making sure that they're valid pairs
405
		$this->atts = array_intersect_key( $this->passed_atts, $this->atts );
406 4
407 4
		// Strip whitespace if it's not default false
408
		$this->if = ( isset( $this->atts['if'] ) && is_string( $this->atts['if'] ) ) ? trim( $this->atts['if'] ) : false;
409
410
		if ( isset( $this->atts['logged_in'] ) ) {
411
			// Truthy
412
			if ( in_array( strtolower( $this->atts['logged_in'] ), array( '1', 'true', 'yes' ) ) ) {
413
				$this->logged_in = true;
414
			} else {
415
				$this->logged_in = false;
416
			}
417
		}
418
419
		/**
420
		 * @action `gravityview/gvlogic/parse_atts/after` Modify shortcode attributes after it's been parsed
421
		 * @see https://gist.github.com/zackkatz/def9b295b80c4ae109760ffba200f498 for an example
422
		 * @since 1.21.5
423
		 * @param GVLogic_Shortcode $this The GVLogic_Shortcode instance
424
		 */
425
		do_action( 'gravityview/gvlogic/parse_atts/after', $this );
426
427
		// Make sure the "if" isn't processed in self::setup_operation_and_comparison()
428
		unset( $this->atts['if'] );
429
	}
430
}
431
432
GVLogic_Shortcode::get_instance();
433