Completed
Push — master ( 6b2386...2bf093 )
by Ahmad
04:44
created

ajaxSecurityChecker()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 8
rs 9.4286
cc 3
eloc 5
nc 4
nop 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 5 and the first side effect is on line 3.

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
if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly
4
}
5
class TitanFrameworkOptionAjaxButton extends TitanFrameworkOption {
6
7
	private static $firstLoad = true;
8
9
	public $defaultSecondarySettings = array(
10
		'action' => 'custom_action',
11
		'label' => '',
12
		'class' => 'button-secondary',
13
		'wait_label' => '',
14
		'success_label' => '',
15
		'error_label' => '',
16
		'success_callback' => '',
17
		'error_callback' => '',
18
	);
19
20
21
	/**
22
	 * This is first called when an ajax button is clicked. This checks whether the nonce
23
	 * is valid and if we should continue;
24
	 *
25
	 * @return	void
26
	 */
27
	public function ajaxSecurityChecker() {
28
		if ( empty( $_POST['nonce'] ) ) {
29
			wp_send_json_error( __( 'Security check failed, please refresh the page and try again.', TF_I18NDOMAIN ) );
30
		}
31
		if ( ! wp_verify_nonce( $_POST['nonce'], 'tf-ajax-button' ) ) {
32
			wp_send_json_error( __( 'Security check failed, please refresh the page and try again.', TF_I18NDOMAIN ) );
33
		}
34
	}
35
36
37
	/**
38
	 * This is last called when an ajax button is clicked. This just exist with a successful state,
39
	 * since doing nothing reads as an error with wp.ajax
40
	 *
41
	 * @return	void
42
	 */
43
	public function ajaxLastSuccess() {
44
		wp_send_json_success();
45
	}
46
47
48
	/**
49
	 * Constructor, fixes the settings to allow for multiple ajax buttons in a single option
50
	 *
51
	 * @param	$settings	Array	Option settings
52
	 * @param	$owner		Object	The container of the option
53
	 * @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...
54
	 */
55
	function __construct( $settings, $owner ) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
56
		parent::__construct( $settings, $owner );
57
58
		add_action( 'admin_head', array( __CLASS__, 'createAjaxScript' ) );
59
60
		// Adjust the settings
61
		foreach ( $this->defaultSecondarySettings as $key => $default ) {
62
			if ( ! is_array( $this->settings[ $key ] ) ) {
63
				$this->settings[ $key ] = array( $this->settings[ $key ] );
64
			}
65
		}
66
67
		while ( count( $this->settings['label'] ) < count( $this->settings['action'] ) ) {
68
			$this->settings['label'][] = $this->settings['label'][ count( $this->settings['label'] ) - 1 ];
69
		}
70
		while ( count( $this->settings['class'] ) < count( $this->settings['action'] ) ) {
71
			$this->settings['class'][] = 'button-secondary';
72
		}
73
		while ( count( $this->settings['wait_label'] ) < count( $this->settings['action'] ) ) {
74
			$this->settings['wait_label'][] = $this->settings['wait_label'][ count( $this->settings['wait_label'] ) - 1 ];
75
		}
76
		while ( count( $this->settings['error_label'] ) < count( $this->settings['action'] ) ) {
77
			$this->settings['error_label'][] = $this->settings['error_label'][ count( $this->settings['error_label'] ) - 1 ];
78
		}
79
		while ( count( $this->settings['success_label'] ) < count( $this->settings['action'] ) ) {
80
			$this->settings['success_label'][] = $this->settings['success_label'][ count( $this->settings['success_label'] ) - 1 ];
81
		}
82
		while ( count( $this->settings['success_callback'] ) < count( $this->settings['action'] ) ) {
83
			$this->settings['success_callback'][] = '';
84
		}
85
		while ( count( $this->settings['error_callback'] ) < count( $this->settings['action'] ) ) {
86
			$this->settings['error_callback'][] = __( 'Something went wrong', TF_I18NDOMAIN );
87
		}
88
89
		foreach ( $this->settings['label'] as $i => $label ) {
90
			if ( empty( $label ) ) {
91
				$this->settings['label'][ $i ] = __( 'Click me', TF_I18NDOMAIN );
92
			}
93
		}
94
		foreach ( $this->settings['wait_label'] as $i => $label ) {
95
			if ( empty( $label ) ) {
96
				$this->settings['wait_label'][ $i ] = __( 'Please wait...', TF_I18NDOMAIN );
97
			}
98
		}
99
		foreach ( $this->settings['error_label'] as $i => $label ) {
100
			if ( empty( $label ) ) {
101
				$this->settings['error_label'][ $i ] = $this->settings['label'][ $i ];
102
			}
103
		}
104
		foreach ( $this->settings['success_label'] as $i => $label ) {
105
			if ( empty( $label ) ) {
106
				$this->settings['success_label'][ $i ] = $this->settings['label'][ $i ];
107
			}
108
		}
109
110
		/**
111
		 * Create ajax handlers for security and last resort success returns
112
		 */
113
		foreach ( $this->settings['action'] as $i => $action ) {
114
			if ( ! empty( $action ) ) {
115
				add_action( 'wp_ajax_' . $action, array( $this, 'ajaxSecurityChecker' ), 1 );
116
				add_action( 'wp_ajax_' . $action, array( $this, 'ajaxLastSuccess' ), 99999 );
117
			}
118
		}
119
120
	}
121
122
123
	/**
124
	 * Renders the option
125
	 *
126
	 * @return	void
127
	 */
128
	public function display() {
129
		$this->echoOptionHeader();
130
131
		foreach ( $this->settings['action'] as $i => $action ) {
0 ignored issues
show
Bug introduced by
The expression $this->settings['action'] of type array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
132
			printf( '<button class="button %s" data-action="%s" data-label="%s" data-wait-label="%s" data-error-label="%s" data-success-label="%s" data-nonce="%s" data-success-callback="%s" data-error-callback="%s">%s</button>',
133
				$this->settings['class'][ $i ],
134
				esc_attr( $action ),
135
				esc_attr( $this->settings['label'][ $i ] ),
136
				esc_attr( $this->settings['wait_label'][ $i ] ),
137
				esc_attr( $this->settings['error_label'][ $i ] ),
138
				esc_attr( $this->settings['success_label'][ $i ] ),
139
				esc_attr( wp_create_nonce( 'tf-ajax-button' ) ),
140
				esc_attr( $this->settings['success_callback'][ $i ] ),
141
				esc_attr( $this->settings['error_callback'][ $i ] ),
142
				esc_attr( $this->settings['label'][ $i ] )
143
			);
144
		}
145
146
		$this->echoOptionFooter();
147
	}
148
149
150
	/**
151
	 * Prints the Javascript needed by ajax buttons. The script is only echoed once
152
	 *
153
	 * @return	void
154
	 */
155
	public static function createAjaxScript() {
156
		if ( ! self::$firstLoad ) {
157
			return;
158
		}
159
		self::$firstLoad = false;
160
161
		?>
162
		<script>
163
		jQuery(document).ready(function($) {
164
			"use strict";
165
			
166
			$('.form-table, .customize-control').on( 'click', '.tf-ajax-button .button', function( e ) {
167
168
				// Only perform one ajax at a time
169
				if ( typeof this.doingAjax == 'undefined' ) {
170
					this.doingAjax = false;
171
				}
172
				e.preventDefault();
173
				if ( this.doingAjax ) {
174
					return false;
175
				}
176
				this.doingAjax = true;
177
				
178
				// Form the data to send, we send the nonce and the post ID if possible
179
				var data = { nonce: $(this).attr('data-nonce') };
180
				<?php
181
				global $post;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
182
				if ( ! empty( $post ) ) {
183
					?>data['id'] = <?php echo esc_attr( $post->ID ) ?>;<?php
184
				}
185
				?>
186
				
187
				// Perform the ajax call
188
				wp.ajax.send( $(this).attr('data-action'), {
189
					
190
					// Success callback
191
					success: function( successMessage ) {
192
						
193
						this.labelTimer = setTimeout(function() {
194
							$(this).text( $(this).attr('data-label') );
195
							this.labelTimer = undefined;
196
						}.bind(this), 3000 );
197
						
198
						$(this).text( successMessage || $(this).attr('data-success-label') );
199
						
200
						// Call the error callback
201
						if ( $(this).attr('data-success-callback') != '' ) {
202
							if ( typeof window[ $(this).attr('data-success-callback') ] != 'undefined' ) {
203
								window[ $(this).attr('data-success-callback') ]();
204
							}
205
						}
206
						this.doingAjax = false;
207
						
208
					}.bind(this),
209
					
210
					// Error callback
211
					error: function( errorMessage ) {
212
						this.labelTimer = setTimeout(function() {
213
							$(this).text( $(this).attr('data-label') );
214
							this.labelTimer = undefined;
215
						}.bind(this), 3000 );
216
217
						$(this).text( errorMessage || $(this).attr('data-error-label') );
218
						
219
						// Call the error callback
220
						if ( $(this).attr('data-error-callback') != '' ) {
221
							if ( typeof window[ $(this).attr('data-error-callback') ] != 'undefined' ) {
222
								window[ $(this).attr('data-error-callback') ]();
223
							}
224
						}
225
						this.doingAjax = false;
226
						
227
					}.bind(this),
228
				
229
					// Pass the data
230
					data: data
231
					
232
				});
233
				
234
				// Clear the label timer
235
				if ( typeof this.labelTimer != 'undefined' ) {
236
					clearTimeout( this.labelTimer );
237
					this.labelTimer = undefined;
238
				}
239
				$(this).text( $(this).attr('data-wait-label') );
240
				
241
				return false;
242
			} );
243
		});
244
		</script>
245
		<?php
246
	}
247
248
	/*
249
	 * Display for theme customizer
250
	 */
251
	public function registerCustomizerControl( $wp_customize, $section, $priority = 1 ) {
252
		// var_dump($section->getID());
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
253
		$wp_customize->add_control( new TitanFrameworkOptionAjaxButtonControl( $wp_customize, '', array(
254
			'label' => $this->settings['name'],
255
			'section' => $section->getID(),
256
			'settings' => $this->getID(),
257
			'description' => $this->settings['desc'],
258
			'priority' => $priority,
259
			'options' => $this->settings,
260
		) ) );
261
	}
262
}
263
264
/*
265
 * WP_Customize_Control with description
266
 */
267
add_action( 'customize_register', 'registerTitanFrameworkOptionAjaxButtonControl', 1 );
268
function registerTitanFrameworkOptionAjaxButtonControl() {
269
	class TitanFrameworkOptionAjaxButtonControl extends WP_Customize_Control {
270
		public $description;
271
		public $options;
272
273
		public function render_content() {
274
			TitanFrameworkOptionAjaxButton::createAjaxScript();
275
276
			?>
277
			<label class='tf-ajax-button'>
278
				<span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span><?php
279
280
				foreach ( $this->options['action'] as $i => $action ) {
281
					printf( '<button class="button %s" data-action="%s" data-label="%s" data-wait-label="%s" data-error-label="%s" data-success-label="%s" data-nonce="%s" data-success-callback="%s" data-error-callback="%s">%s</button>',
282
						$this->options['class'][ $i ],
283
						esc_attr( $action ),
284
						esc_attr( $this->options['label'][ $i ] ),
285
						esc_attr( $this->options['wait_label'][ $i ] ),
286
						esc_attr( $this->options['error_label'][ $i ] ),
287
						esc_attr( $this->options['success_label'][ $i ] ),
288
						esc_attr( wp_create_nonce( 'tf-ajax-button' ) ),
289
						esc_attr( $this->options['success_callback'][ $i ] ),
290
						esc_attr( $this->options['error_callback'][ $i ] ),
291
						esc_attr( $this->options['label'][ $i ] )
292
					);
293
				}
294
295
				if ( ! empty( $this->description ) ) {
296
					echo "<p class='description'>" . $this->description . '</p>';
297
				}
298
299
			?></label><?php
300
		}
301
	}
302
}
303