PluginProbe
HTML Forms – Simple WordPress Forms Plugin / 1.4.2
HTML Forms – Simple WordPress Forms Plugin v1.4.2
trunk 1.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.2.0 1.3.0 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.14 1.3.15 1.3.16 1.3.17 All 66 releases
html-forms / src / class-forms.php

class-forms.php in HTML Forms – Simple WordPress Forms Plugin 1.4.2, at src/class-forms.php

433 lines 12.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace HTML_Forms;
4
5 class Forms {
6
7
8 /**
9 * @var string
10 */
11 private $plugin_file;
12
13 /**
14 * @var array
15 */
16 private $settings;
17
18 /**
19 * Forms constructor.
20 *
21 * @param string $plugin_file
22 * @param array $settings
23 */
24 public function __construct( $plugin_file, array $settings ) {
25 $this->plugin_file = $plugin_file;
26 $this->settings = $settings;
27 }
28
29 public function hook() {
30 add_action( 'init', array( $this, 'register' ) );
31 add_action( 'init', array( $this, 'listen_for_submit' ) );
32 add_action( 'init', array( $this, 'register_assets' ) );
33 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
34 add_action( 'parse_request', array( $this, 'listen_for_preview' ) );
35 add_filter( 'hf_form_markup', 'hf_template' );
36 }
37
38 public function register() {
39 // register post type
40 register_post_type(
41 'html-form',
42 array(
43 'labels' => array(
44 'name' => 'HTML Forms',
45 'singular_name' => 'HTML Form',
46 ),
47 'public' => false,
48 'capability_type' => 'form',
49 )
50 );
51
52 if ( function_exists( 'register_block_type' ) ) {
53 register_block_type(
54 'html-forms/form',
55 array(
56 'render_callback' => array( $this, 'shortcode' ),
57 )
58 );
59 }
60
61 add_shortcode( 'hf_form', array( $this, 'shortcode' ) );
62 }
63
64 public function register_assets() {
65 $assets_url = plugins_url( 'assets/', $this->plugin_file );
66
67 wp_register_script( 'html-forms', $assets_url . 'js/public.js', array(), HTML_FORMS_VERSION, true );
68 wp_localize_script(
69 'html-forms',
70 'hf_js_vars',
71 array(
72 'ajax_url' => admin_url( 'admin-ajax.php?action=hf_form_submit' ),
73 )
74 );
75
76 wp_register_style( 'html-forms', $assets_url . 'css/forms.css', array(), HTML_FORMS_VERSION );
77 add_filter( 'script_loader_tag', array( $this, 'add_defer_attribute' ), 10, 2 );
78 }
79
80 public function enqueue_assets() {
81 if ( $this->settings['load_stylesheet'] ) {
82 wp_enqueue_style( 'html-forms' );
83 }
84 }
85
86 /**
87 * Adds defer attribute to our <script> element
88 */
89 public function add_defer_attribute( $tag, $handle ) {
90 if ( $handle !== 'html-forms' ) {
91 return $tag;
92 }
93
94 return str_replace( ' src=', ' defer src=', $tag );
95 }
96 /**
97 * @param Form $form
98 * @param array $data
99 * @return string
100 */
101 public function validate_form( Form $form, array $data ) {
102 // validate honeypot field
103 $honeypot_key = sprintf( '_hf_h%d', $form->ID );
104 if ( ! isset( $data[ $honeypot_key ] ) || $data[ $honeypot_key ] !== '' ) {
105 return 'spam';
106 }
107
108 // validate size of POST array
109 if ( count( $data ) > $form->get_field_count() && apply_filters( 'hf_validate_form_request_size', true ) ) {
110 return 'spam';
111 }
112
113 $was_required = (array) hf_array_get( $data, '_was_required', array() );
114 $required_fields = $form->get_required_fields();
115 foreach ( $required_fields as $field_name ) {
116 $value = hf_array_get( $data, $field_name );
117 if ( empty( $value ) && ! in_array( $field_name, $was_required ) ) {
118 return 'required_field_missing';
119 }
120 }
121
122 $email_fields = $form->get_email_fields();
123 foreach ( $email_fields as $field_name ) {
124 $value = hf_array_get( $data, $field_name );
125 if ( ! empty( $value ) && ! is_email( $value ) ) {
126 return 'invalid_email';
127 }
128 }
129
130 $error_code = '';
131
132 /**
133 * This filter allows you to perform your own form validation. The dynamic portion of the hook refers to the form slug.
134 *
135 * Return a non-empty string if you want to raise an error.
136 * Error codes with a specific error message are: "required_field_missing", "invalid_email", and "error"
137 *
138 * @param string $error_code
139 * @param Form $form
140 * @param array $data
141 */
142 $error_code = apply_filters( 'hf_validate_form_' . $form->slug, $error_code, $form, $data );
143
144 /**
145 * This filter allows you to perform your own form validation.
146 *
147 * Return a non-empty string if you want to raise an error.
148 * Error codes with a specific error message are: "required_field_missing", "invalid_email", and "error"
149 *
150 * @param string $error_code
151 * @param Form $form
152 * @param array $data
153 */
154 $error_code = apply_filters( 'hf_validate_form', $error_code, $form, $data );
155 if ( ! empty( $error_code ) ) {
156 return $error_code;
157 }
158
159 // all good: no errors!
160 return '';
161 }
162
163 /**
164 * Sanitize array with values before saving. Can be called recursively.
165 *
166 * @param mixed $value
167 * @return mixed
168 */
169 public function sanitize( $value ) {
170 if ( is_string( $value ) ) {
171 // do nothing if empty string
172 if ( $value === '' ) {
173 return $value;
174 }
175
176 // strip slashes
177 $value = stripslashes( $value );
178
179 // strip all whitespace
180 $value = trim( $value );
181
182 // convert &amp; back to &
183 $value = html_entity_decode( $value, ENT_NOQUOTES );
184 } elseif ( is_array( $value ) || is_object( $value ) ) {
185 $new_value = array();
186 $vars = is_array( $value ) ? $value : get_object_vars( $value );
187
188 // do nothing if empty array or object
189 if ( count( $vars ) === 0 ) {
190 return $value;
191 }
192
193 foreach ( $vars as $key => $sub_value ) {
194 // strip all whitespace & HTML from keys (!)
195 $key = trim( strip_tags( $key ) );
196
197 // sanitize sub value
198 $new_value[ $key ] = $this->sanitize( $sub_value );
199 }
200
201 $value = is_object( $value ) ? (object) $new_value : $new_value;
202 }
203
204 return $value;
205 }
206
207 /**
208 * @return array
209 */
210 public function get_request_data() {
211 $data = $_POST;
212
213 if ( ! empty( $_FILES ) ) {
214 foreach ( $_FILES as $field_name => $file ) {
215 // only add non-empty files so that required field validation works as expected
216 // upload could still have errored at this point
217 if ( $file['error'] !== UPLOAD_ERR_NO_FILE ) {
218 $data[ $field_name ] = $file;
219 }
220 }
221 }
222
223 return $data;
224 }
225
226 public function listen_for_submit() {
227 // only respond to AJAX requests with _hf_form_id set.
228 if ( empty( $_POST['_hf_form_id'] )
229 || empty( $_SERVER['HTTP_X_REQUESTED_WITH'] )
230 || strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) !== strtolower( 'XMLHttpRequest' ) ) {
231 return;
232 }
233
234 $data = $this->get_request_data();
235 $form_id = (int) $data['_hf_form_id'];
236 $form = hf_get_form( $form_id );
237 $error_code = $this->validate_form( $form, $data );
238
239 if ( empty( $error_code ) ) {
240 /**
241 * Filters the field names that should be ignored on the Submission object.
242 * Fields starting with an underscore (_) are ignored by default.
243 *
244 * @param array $names
245 */
246 $ignored_field_names = apply_filters( 'hf_ignored_field_names', array() );
247
248 // filter out ignored field names
249 foreach ( $data as $key => $value ) {
250 if ( $key[0] === '_' || in_array( $key, $ignored_field_names ) ) {
251 unset( $data[ $key ] );
252 continue;
253 }
254
255 // this detects the WPBruiser token field to ensure it isn't stored
256 // CAVEAT: this will detect any non-uppercase string with 2 dashes in the field name and no whitespace in the field value
257 if ( class_exists( 'GoodByeCaptcha' ) && is_string( $key ) && is_string( $value ) && strtoupper( $key ) !== $key && substr_count( $key, '-' ) >= 2 && substr_count( trim( $value ), ' ' ) === 0 ) {
258 unset( $data[ $key ] );
259 continue;
260 }
261 }
262
263 // sanitize data: strip tags etc.
264 $data = $this->sanitize( $data );
265
266 // save form submission
267 $submission = new Submission();
268 $submission->form_id = $form_id;
269 $submission->data = $data;
270 $submission->ip_address = ! empty( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( $_SERVER['REMOTE_ADDR'] ) : '';
271 $submission->user_agent = ! empty( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ) : '';
272 $submission->referer_url = ! empty( $_SERVER['HTTP_REFERER'] ) ? sanitize_text_field( $_SERVER['HTTP_REFERER'] ) : '';
273 $submission->submitted_at = gmdate( 'Y-m-d H:i:s' );
274
275 // save submission object so that other form processor have an insert ID to work with (eg file upload)
276 if ( $form->settings['save_submissions'] ) {
277 $submission->save();
278 }
279
280 /**
281 * General purpose hook that runs before all form actions, so we can still modify the submission object that is passed to actions.
282 */
283 do_action( 'hf_process_form', $form, $submission );
284
285 // re-save submission object for convenience in form processors hooked into hf_process_form
286 if ( $form->settings['save_submissions'] ) {
287 $submission->save();
288 }
289
290 // process form actions
291 if ( isset( $form->settings['actions'] ) ) {
292 foreach ( $form->settings['actions'] as $action_settings ) {
293 /**
294 * Processes the specified form action and passes related data.
295 *
296 * @param array $action_settings
297 * @param Submission $submission
298 * @param Form $form
299 */
300 do_action( 'hf_process_form_action_' . $action_settings['type'], $action_settings, $submission, $form );
301 }
302 }
303
304 /**
305 * General purpose hook after all form actions have been processed for this specific form. The dynamic portion of the hook refers to the form slug.
306 *
307 * @param Submission $submission
308 * @param Form $form
309 */
310 do_action( "hf_form_{$form->slug}_success", $submission, $form );
311
312 /**
313 * General purpose hook after all form actions have been processed.
314 *
315 * @param Submission $submission
316 * @param Form $form
317 */
318 do_action( 'hf_form_success', $submission, $form );
319 } else {
320 /**
321 * General purpose hook for when a form error occurred
322 *
323 * @param string $error_code
324 * @param Form $form
325 * @param array $data
326 */
327 do_action( 'hf_form_error', $error_code, $form, $data );
328 }
329
330 // Delay response until "wp_loaded" hook to give other plugins a chance to process stuff.
331 add_action(
332 'wp_loaded',
333 function() use ( $error_code, $form, $data ) {
334 $response = $this->get_response_for_error_code( $error_code, $form, $data );
335
336 // clear output, some plugin or hooked code might have thrown errors by now.
337 if ( ob_get_level() > 0 ) {
338 ob_end_clean();
339 }
340
341 send_origin_headers();
342 send_nosniff_header();
343 nocache_headers();
344
345 wp_send_json( $response, 200 );
346 exit;
347 }
348 );
349 }
350
351 public function listen_for_preview() {
352 if ( empty( $_GET['hf_preview_form'] ) || ! current_user_can( 'edit_forms' ) ) {
353 return;
354 }
355
356 try {
357 $form = hf_get_form( $_GET['hf_preview_form'] );
358 } catch ( \Exception $e ) {
359 return;
360 }
361
362 show_admin_bar( false );
363 add_filter( 'pre_handle_404', '__return_true' );
364 remove_all_actions( 'template_redirect' );
365 add_action(
366 'template_redirect',
367 function() use ( $form ) {
368 // clear output, some plugin or hooked code might have thrown errors by now.
369 if ( ob_get_level() > 0 ) {
370 ob_end_clean();
371 }
372
373 status_header( 200 );
374 require dirname( $this->plugin_file ) . '/views/form-preview.php';
375 exit;
376 }
377 );
378 }
379
380 private function get_response_for_error_code( $error_code, Form $form, $data = array() ) {
381 // return success response for empty error code string or spam (to trick bots)
382 if ( $error_code === '' || $error_code === 'spam' ) {
383 $response = array(
384 'message' => array(
385 'type' => 'success',
386 'text' => $form->get_message( 'success' ),
387 ),
388 'hide_form' => (bool) $form->settings['hide_after_success'],
389 );
390
391 if ( ! empty( $form->settings['redirect_url'] ) ) {
392 $response['redirect_url'] = hf_replace_data_variables( $form->settings['redirect_url'], $data, 'urlencode' );
393 }
394
395 return apply_filters( 'hf_form_response', $response, $form, $data );
396 }
397
398 // get error message
399 $message = $form->get_message( $error_code );
400 if ( empty( $message ) ) {
401 $message = $form->get_message( 'error' );
402 }
403
404 // return error response
405 return array(
406 'message' => array(
407 'type' => 'warning',
408 'text' => $message,
409 ),
410 'error' => $error_code,
411 );
412 }
413
414 public function shortcode( $attributes = array(), $content = '' ) {
415 if ( empty( $attributes['slug'] ) && empty( $attributes['id'] ) ) {
416 return '';
417 }
418
419 $slug_or_id = empty( $attributes['id'] ) ? $attributes['slug'] : $attributes['id'];
420 try {
421 $form = hf_get_form( $slug_or_id );
422 } catch ( \Exception $e ) {
423 if ( ! current_user_can( 'manage_options' ) ) {
424 return $content;
425 }
426
427 return sprintf( '<p><strong>%s</strong> %s</p>', __( 'Error:', 'html-forms' ), sprintf( __( 'No form found with slug %s', 'html-forms' ), $attributes['slug'] ) );
428 }
429
430 return $form . $content;
431 }
432 }
433