PluginProbe
Hash Form – Drag & Drop Form Builder / trunk
Hash Form – Drag & Drop Form Builder vtrunk
1.4.4 1.4.3 1.4.2 1.4.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.6.1 1.2.7 1.2.8 1.2.9 1.3.0 All 47 releases
hash-form / includes / HashFormBuilder.php

HashFormBuilder.php in Hash Form – Drag & Drop Form Builder trunk, at includes/HashFormBuilder.php

1,456 lines 63.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 defined('ABSPATH') || die();
3
4 class HashFormBuilder {
5
6 use HashFormListActions;
7
8 public function __construct() {
9
10 $this->includes();
11
12 add_action('admin_menu', array($this, 'add_menu'), 1);
13 add_filter('set-screen-option', array($this, 'set_screen_option'), 10, 3);
14
15 add_action('wp_ajax_hashform_update_form', array($this, 'update_form'));
16 add_action('wp_ajax_hashform_create_form', array($this, 'create_form'));
17 add_action('wp_ajax_hashform_save_form_settings', array($this, 'save_form_settings'));
18 add_action('wp_ajax_hashform_save_form_style', array($this, 'save_form_style'));
19 add_action('wp_ajax_hashform_form_preview', array($this, 'form_preview'));
20 add_action('wp_ajax_hashform_add_more_condition_block', array($this, 'add_more_condition_block'));
21 add_action('admin_footer', array($this, 'init_overlay_html'));
22
23 // Printed above #wpbody so the bar sits flush under the admin bar and
24 // clear of the Screen Options tab.
25 add_action('in_admin_header', array($this, 'list_header'));
26
27 // Notices are moved inside the screen wrapper; see buffer_notices().
28 add_action('admin_notices', array($this, 'buffer_notices'), -PHP_INT_MAX);
29 add_action('all_admin_notices', array($this, 'capture_notices'), PHP_INT_MAX);
30
31 add_filter('plugin_action_links_' . plugin_basename(HASHFORM_FILE), array($this, 'add_plugin_action_link'), 10, 1);
32
33 add_action('wp_ajax_hashform_file_upload_action', array($this, 'file_upload_action'));
34 add_action('wp_ajax_nopriv_hashform_file_upload_action', array($this, 'file_upload_action'));
35
36 add_action('wp_ajax_hashform_file_delete_action', array($this, 'file_delete_action'));
37 add_action('wp_ajax_nopriv_hashform_file_delete_action', array($this, 'file_delete_action'));
38
39 add_action('wp_loaded', array($this, 'admin_notice'), 20);
40
41 add_action('init', array($this, 'register_translation_strings'));
42 add_filter('hashform_translate_string', array($this, 'translate_string'), 10, 3);
43 }
44
45 public function includes() {
46 include HASHFORM_PATH . 'admin/forms/sanitization.php';
47 }
48
49 public function add_menu() {
50 global $hashform_listing_page;
51 add_menu_page(esc_html__('Hash Form', 'hash-form'), esc_html__('Hash Form', 'hash-form'), 'hashform_view_forms', 'hashform', array($this, 'route'), 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTcuNjYgMTUyLjI3IiBmaWxsPSIjYTdhYWFkIj48Zz48Zz48cGF0aCBkPSJNMCwzLjQ2QTMuNDYsMy40NiwwLDAsMSwzLjE0LDBoODBBMy41MywzLjUzLDAsMCwxLDg1LjYsMWwzMSwzMWEzLjQ3LDMuNDcsMCwwLDEsMSwyLjQzVjE0OC44MWEzLjQ2LDMuNDYsMCwwLDEtMy40NiwzLjQ2SDMxLjYzYTMuNDYsMy40NiwwLDEsMSwwLTYuOTJoNzkuMTFWMzguMDdIODMuMDVhMy40NiwzLjQ2LDAsMCwxLTMuNDYtMy40NlY2LjkySDYuOTJWMTQ1LjM1SDE0YTMuNDYsMy40NiwwLDEsMSwwLDYuOTJIMy40NkEzLjQ2LDMuNDYsMCwwLDEsMCwxNDguODFaTTEwNiwzMS4xNSw4Ni41MSwxMS42OFYzMS4xNVoiLz48cGF0aCBkPSJNNzguNjYsNTkuM0g5NS4wOXY2LjYxSDc4LjY2Vjg1Ljc1SDcyLjA1VjQyLjg3aDYuNjFabTAsMzkuNjd2MTYuNDJINzIuMDVWOTlINTIuMjJWOTIuMzZIOTUuMDlWOTlaTTM5LDk5SDIyLjU3VjkyLjM2SDM5VjcyLjUyaDYuNjFWMTE1LjRIMzlaTTM5LDU5LjNWNDIuODdoNi42MVY1OS4zSDY1LjQ0djYuNjFIMjIuNTdWNTkuM1oiLz48L2c+PC9nPjwvc3ZnPg==', 29);
52 $hashform_listing_page = add_submenu_page('hashform', esc_html__('Forms', 'hash-form'), esc_html__('Forms', 'hash-form'), 'hashform_view_forms', 'hashform', array($this, 'route'));
53 add_action("load-$hashform_listing_page", array($this, 'listing_page_screen_options'));
54 }
55
56 protected static function list_config() {
57 return array(
58 'page' => 'hashform',
59 'table' => 'hashform_forms',
60 'id_key' => 'form_id',
61 'nonce_item' => 'form',
62 'bulk_nonce' => 'bulk-forms',
63 'caps' => array(
64 'delete' => 'hashform_delete_forms',
65 'edit' => 'hashform_edit_forms',
66 'create' => 'hashform_create_forms',
67 ),
68 'actions' => array('edit', 'trash', 'destroy', 'untrash', 'delete_all', 'duplicate', 'settings', 'style'),
69 );
70 }
71
72 protected static function destroy_item($id) {
73 return self::destroy_form($id);
74 }
75
76 protected static function render_list($message = '', $class = 'updated') {
77 self::display_forms_list($message, $class);
78 }
79
80 protected static function message_trashed($count, $undo_open, $undo_close) {
81 /* translators: 1: form count singular & plural, 2: link open, 3: link close */
82 return sprintf(_n('%1$s form moved to the Trash. %2$sUndo%3$s', '%1$s forms moved to the Trash. %2$sUndo%3$s', $count, 'hash-form'), $count, $undo_open, $undo_close);
83 }
84
85 protected static function message_untrashed($count) {
86 /* translators: 1: form count singular & plural */
87 return sprintf(_n('%1$s form restored from the Trash.', '%1$s forms restored from the Trash.', $count, 'hash-form'), $count);
88 }
89
90 protected static function message_destroyed($count) {
91 /* translators: 1: form count singular & plural */
92 return sprintf(_n('%1$s Form Permanently Deleted', '%1$s Forms Permanently Deleted', $count, 'hash-form'), $count);
93 }
94
95 protected static function message_deleted($count) {
96 /* translators: 1: form count singular & plural */
97 return sprintf(_n('%1$s form permanently deleted.', '%1$s forms permanently deleted.', $count, 'hash-form'), $count);
98 }
99
100 protected static function message_none_specified() {
101 return esc_html__('No forms were specified', 'hash-form');
102 }
103
104 public static function display_forms_list($message = '', $class = 'updated') {
105 ?>
106 <div class="hf-content hf-list-screen">
107
108 <?php // The header bar is printed on in_admin_header; see list_header(). ?>
109 <div class="hf-list-wrap wrap">
110 <h1></h1>
111
112 <?php
113 self::print_notices();
114 self::display_message($message, $class);
115
116 $form_table = new HashFormListing();
117 $form_status = HashFormHelper::get_var('status', 'sanitize_title', 'published');
118
119 // Prepared up front so the screen can tell an empty list from
120 // a search that found nothing before deciding what to print.
121 $form_table->prepare_items();
122 $is_searching = '' !== (string) HashFormHelper::get_var('s');
123 ?>
124 <form id="posts-filter" method="get">
125 <input type="hidden" name="page" value="<?php echo esc_attr(HashFormHelper::get_var('page', 'sanitize_title')); ?>" />
126 <input type="hidden" name="status" value="<?php echo esc_attr($form_status); ?>" />
127
128 <div class="hf-list-toolbar">
129 <?php
130 $form_table->views();
131
132 // A search box over nothing is just noise on a first run.
133 if ($form_table->has_items() || $is_searching) {
134 $form_table->search_box(esc_html__('Search', 'hash-form'), 'search');
135 }
136 ?>
137 </div>
138
139 <?php $form_table->display(); ?>
140 </form>
141 </div>
142 </div>
143 <?php
144 }
145
146 /**
147 * Totals for the header bar. Empty on a brand new install, where zeroes
148 * say nothing.
149 */
150 private static function list_stats() {
151 $stats = HashFormListing::get_stats();
152
153 if (!$stats['forms'] && !$stats['trash']) {
154 return array();
155 }
156
157 $chips = array(
158 array(
159 'value' => number_format_i18n($stats['forms']),
160 'label' => _n('Form', 'Forms', $stats['forms'], 'hash-form'),
161 ),
162 array(
163 'value' => number_format_i18n($stats['entries']),
164 'label' => _n('Entry', 'Entries', $stats['entries'], 'hash-form'),
165 'url' => admin_url('admin.php?page=hashform-entries'),
166 ),
167 );
168
169 if ($stats['trash']) {
170 $chips[] = array(
171 'value' => number_format_i18n($stats['trash']),
172 'label' => esc_html__('In Trash', 'hash-form'),
173 'url' => admin_url('admin.php?page=hashform&status=trash'),
174 );
175 }
176
177 return $chips;
178 }
179
180 /**
181 * The bar across the top of the Forms list, through the shared renderer.
182 */
183 public function list_header() {
184 if (!self::is_list_view()) {
185 return;
186 }
187
188 HashFormHelper::render_list_header(array(
189 'title' => esc_html__('Forms', 'hash-form'),
190 'stats' => self::list_stats(),
191 'action' => array(
192 'label' => esc_html__('Add New', 'hash-form'),
193 'url' => '#',
194 'class' => 'hf-trigger-modal',
195 ),
196 ));
197 }
198
199 public function create_form() {
200 HashFormCapabilities::require_cap_ajax('hashform_create_forms');
201
202 check_ajax_referer('hashform_backend_ajax', 'backend_nonce');
203
204 $name = trim(HashFormHelper::get_post('name'));
205
206 // The dialog checks this too, but a form with no name is only ever
207 // reachable as "No Title" in the list, so do not create one.
208 if ('' === $name) {
209 echo wp_json_encode(array('error' => esc_html__('Please give the form a name.', 'hash-form')));
210 wp_die();
211 }
212
213 $new_values = array(
214 'name' => esc_html($name),
215 'description' => '',
216 'form_key' => sanitize_text_field($name),
217 'options' => array(
218 'submit_value' => esc_html__('Submit', 'hash-form'),
219 'show_description' => 'on',
220 'show_title' => 'on',
221 ),
222 'settings' => HashFormHelper::get_form_settings_default($name)
223 );
224 $form_id = self::create($new_values);
225 $response = array('redirect' => admin_url('admin.php?page=hashform&hashform_action=edit&id=' . absint($form_id)));
226 echo wp_json_encode($response);
227 wp_die();
228 }
229
230 public static function create($values) {
231 global $wpdb;
232 $options = isset($values['options']) && is_array($values['options']) ? $values['options'] : array();
233 $options = HashFormHelper::recursive_parse_args($options, HashFormHelper::get_form_options_default());
234 $options = HashFormHelper::sanitize_array($options, HashFormHelper::get_form_options_sanitize_rules());
235
236 $settings = isset($values['settings']) && is_array($values['settings']) ? $values['settings'] : array();
237 $settings = HashFormHelper::recursive_parse_args($settings, HashFormHelper::get_form_settings_default());
238 $settings = HashFormHelper::sanitize_array($settings, HashFormHelper::get_form_settings_sanitize_rules());
239
240 $styles = isset($values['styles']) && is_array($values['styles']) ? $values['styles'] : array();
241 $styles = HashFormHelper::recursive_parse_args($styles, array('form_style' => 'default-style', 'form_style_template' => ''));
242 $styles = HashFormHelper::sanitize_array($styles, HashFormHelper::get_form_styles_sanitize_rules());
243
244 $new_values = array(
245 'form_key' => HashFormHelper::get_unique_key('hashform_forms', 'form_key'),
246 'name' => esc_html($values['name']),
247 'description' => esc_html($values['description']),
248 'status' => isset($values['status']) ? sanitize_text_field($values['status']) : 'published',
249 'created_at' => isset($values['created_at']) ? sanitize_text_field($values['created_at']) : current_time('mysql'),
250 'options' => serialize($options),
251 'settings' => serialize($settings),
252 'styles' => serialize($styles),
253 );
254 $wpdb->insert($wpdb->prefix . 'hashform_forms', $new_values);
255 $id = $wpdb->insert_id;
256 return $id;
257 }
258
259 public function update_form() {
260 HashFormCapabilities::require_cap_ajax('hashform_edit_forms');
261
262 check_ajax_referer('hashform_backend_ajax', 'backend_nonce');
263
264 $fields_array = $settings_array = array();
265
266 $fields = htmlspecialchars_decode(nl2br(str_replace('&quot;', '"', HashFormHelper::get_post('hashform_fields', 'esc_html'))));
267
268 if ($fields) {
269 $fields_array = HashFormHelper::parse_json_array($fields);
270 }
271
272 $settings = htmlspecialchars_decode(nl2br(str_replace('&quot;', '"', HashFormHelper::get_post('hashform_settings', 'esc_html'))));
273 if ($settings) {
274 $settings_array = HashFormHelper::parse_json_array($settings);
275 }
276
277 self::update($fields_array, $settings_array);
278 }
279
280 public static function update($fields_values, $settings_values) {
281 $id = isset($fields_values['id']) ? absint($fields_values['id']) : '';
282
283 self::update_form_options($id, $settings_values);
284 HashFormFields::update_form_fields($id, $fields_values);
285
286 $message = '<span class="mdi mdi-check-circle"></span>' . esc_html__('Form was successfully updated.', 'hash-form');
287
288 if (defined('DOING_AJAX')) {
289 /*
290 * The span is allowed a class: without one, kses stripped it and
291 * the tick that goes with the message never reached the page —
292 * only the empty element it should have been drawn in.
293 */
294 wp_die(wp_kses($message, array(
295 'a' => array('href' => array(), 'target' => array()),
296 'span' => array('class' => array()),
297 )));
298 }
299 }
300
301 public static function update_form_options($id, $args) {
302 global $wpdb;
303 $options = HashFormHelper::recursive_parse_args($args, HashFormHelper::get_form_options_checkbox_settings());
304 $options = HashFormHelper::sanitize_array($options, HashFormHelper::get_form_options_sanitize_rules());
305
306 $query_results = $wpdb->update($wpdb->prefix . 'hashform_forms', array(
307 'name' => esc_html(isset($args['title']) ? $args['title'] : ''),
308 'description' => esc_html(isset($args['description']) ? $args['description'] : ''),
309 'options' => maybe_serialize($options)
310 ), array('id' => $id));
311 return $query_results;
312 }
313
314 public static function edit() {
315 require(HASHFORM_PATH . 'admin/forms/build/edit.php');
316 }
317
318 public static function settings() {
319 require HASHFORM_PATH . 'admin/forms/settings/settings.php';
320 }
321
322 public static function style() {
323 require HASHFORM_PATH . 'admin/forms/style/style.php';
324 }
325
326 public function listing_page_screen_options() {
327
328 global $hashform_listing_page;
329
330 $screen = get_current_screen();
331
332 // get out of here if we are not on our settings page
333 if (!is_object($screen) || $screen->id != $hashform_listing_page) {
334 return;
335 }
336
337 $args = array(
338 'label' => esc_html__('Forms per page', 'hash-form'),
339 'default' => 10,
340 'option' => 'forms_per_page'
341 );
342 add_screen_option('per_page', $args);
343
344 new HashFormListing();
345 }
346
347 public function set_screen_option($status, $option, $value) {
348 return ('forms_per_page' === $option) ? $value : $status;
349 }
350
351 public static function destroy_form($id) {
352 global $wpdb;
353 $form = self::get_form_vars($id);
354 if (!$form) {
355 return false;
356 }
357
358 $id = $form->id;
359 $entries = $wpdb->get_col($wpdb->prepare("SELECT id FROM {$wpdb->prefix}hashform_entries WHERE form_id=%d", $id));
360
361 foreach ($entries as $entry_id) {
362 HashFormEntry::destroy_entry($entry_id);
363 }
364
365 $wpdb->query($wpdb->prepare('DELETE hfi FROM ' . $wpdb->prefix . 'hashform_fields AS hfi LEFT JOIN ' . $wpdb->prefix . 'hashform_forms hfm ON (hfi.form_id = hfm.id) WHERE hfi.form_id=%d', $id));
366
367 $results = $wpdb->query($wpdb->prepare('DELETE FROM ' . $wpdb->prefix . 'hashform_forms WHERE id=%d', $id));
368 return $results;
369 }
370
371 public static function duplicate() {
372 global $wpdb;
373 $message = '';
374 $nonce = HashFormHelper::get_var('_wpnonce');
375 $id = HashFormHelper::get_var('id', 'absint');
376
377 if (!wp_verify_nonce($nonce, 'duplicate_form_' . $id)) {
378 wp_die(esc_html__('Error ! Refresh the page and try again.', 'hash-form'));
379 }
380
381 static::require_list_cap('create');
382
383 $values = self::get_form_vars($id);
384
385 if (!$values) {
386 return false;
387 }
388
389 $options = HashFormHelper::recursive_parse_args($values->options, HashFormHelper::get_form_options_default());
390 $options = HashFormHelper::sanitize_array($options, HashFormHelper::get_form_options_sanitize_rules());
391
392 $settings = HashFormHelper::recursive_parse_args($values->settings, HashFormHelper::get_form_settings_default());
393 $settings = HashFormHelper::sanitize_array($settings, HashFormHelper::get_form_settings_sanitize_rules());
394
395 $styles = HashFormHelper::recursive_parse_args($values->styles, array('form_style' => 'default-style', 'form_style_template' => ''));
396 $styles = HashFormHelper::sanitize_array($styles, HashFormHelper::get_form_styles_sanitize_rules());
397
398 $new_values = array(
399 'form_key' => HashFormHelper::get_unique_key('hashform_forms', 'form_key'),
400 'name' => esc_html($values->name) . ' - ' . esc_html__('Copy', 'hash-form'),
401 'description' => esc_html($values->description),
402 'status' => $values->status ? sanitize_text_field($values->status) : 'published',
403 'created_at' => sanitize_text_field(current_time('mysql')),
404 'options' => serialize($options),
405 'settings' => serialize($settings),
406 'styles' => serialize($styles),
407 );
408
409 $query_results = $wpdb->insert($wpdb->prefix . 'hashform_forms', $new_values);
410
411 $form_id = 0;
412 if ($query_results) {
413 $form_id = $wpdb->insert_id;
414 $map = HashFormFields::duplicate_fields($id, $form_id);
415
416 /*
417 * The rules were written above with the source form's field ids,
418 * which the copy does not have. Rewritten here, once the copy's own
419 * fields exist and the two can be matched up.
420 */
421 self::remap_calculation_formulas($form_id, $map);
422
423 if (!empty($settings['condition_action'])) {
424 $settings = self::remap_conditions($settings, $map, $dropped);
425
426 $wpdb->update(
427 $wpdb->prefix . 'hashform_forms',
428 array('settings' => serialize($settings)),
429 array('id' => $form_id)
430 );
431
432 if ($dropped) {
433 HashFormHelper::log(sprintf(
434 'copying form %d: %d show/hide rule(s) named a field the form no longer has and were not copied',
435 absint($id),
436 $dropped
437 ));
438 }
439 }
440 }
441
442 if ($form_id) {
443 $message = esc_html__('Form was Successfully Copied', 'hash-form');
444 $class = 'updated';
445 } else {
446 $message = esc_html__('Error! Form Can not be Copied', 'hash-form');
447 $class = 'error';
448 }
449
450 self::display_forms_list($message, $class);
451 }
452
453 public static function get_admin_header($atts = array()) {
454 $class = isset($atts['class']) ? $atts['class'] : '';
455 $form = $atts['form'];
456 $form_title = $form->name;
457 ?>
458 <?php
459 /*
460 * Two rows: what is being edited and what can be done to it on top,
461 * where you are underneath. A single row had the form name, four tabs
462 * and four actions competing for the same line, which left no room for
463 * a long form title and no hierarchy between them.
464 */
465 $status = isset($form->status) ? $form->status : 'published';
466 $is_published = ('published' === $status);
467 ?>
468 <div id="hf-header" class="<?php echo esc_attr($class); ?>">
469 <div class="hf-header-top">
470 <a class="hf-header-back" href="<?php echo esc_url(admin_url('admin.php?page=hashform')); ?>" aria-label="<?php esc_attr_e('Back to forms', 'hash-form'); ?>">
471 <span class="mdi mdi-arrow-left"></span>
472 </a>
473
474 <div class="hf-header-identity">
475 <span class="hf-header-mark" aria-hidden="true">
476 <span class="mdi mdi-file-document-outline"></span>
477 </span>
478
479 <h1 class="hf-header-title"><?php echo esc_html($form_title); ?></h1>
480 <span class="hf-header-status<?php echo $is_published ? ' hf-is-published' : ''; ?>">
481 <?php echo $is_published ? esc_html__('Published', 'hash-form') : esc_html__('Draft', 'hash-form'); ?>
482 </span>
483 </div>
484
485 <div class="hf-header-actions">
486 <a class="hf-preview-button" href="<?php echo esc_url(admin_url('admin-ajax.php?action=hashform_preview&form=' . absint($form->id))); ?>" target="_blank">
487 <span class="mdi mdi-eye-outline"></span><?php esc_html_e('Preview', 'hash-form'); ?>
488 </a>
489
490 <button class="hf-embed-button" type="button">
491 <span class="mdi mdi-code-brackets"></span><?php esc_html_e('Embed', 'hash-form'); ?>
492 </button>
493
494 <button class="hashform-ajax-udpate-button" type="button" id="hf-update-button">
495 <span class="mdi mdi-check-circle-outline"></span><?php esc_html_e('Save', 'hash-form'); ?>
496 </button>
497 </div>
498 </div>
499
500 <div class="hf-header-bottom">
501 <?php self::get_form_nav($form); ?>
502 </div>
503 </div>
504 <?php
505 }
506
507 public static function get_form_nav($form) {
508 if (!$form) {
509 return;
510 }
511 $id = $form->id;
512 $nav_items = self::get_form_nav_items($id);
513 ?>
514 <ul class="hf-main-nav">
515 <?php foreach ($nav_items as $nav_item) { ?>
516 <li>
517 <a href="<?php echo esc_url($nav_item['link']); ?>" class="<?php echo self::is_current_page($nav_item['page'], $nav_item['current']) ? 'hf-active-nav' : ''; ?>">
518 <?php echo esc_html($nav_item['label']); ?>
519 </a>
520 </li>
521 <?php } ?>
522 </ul>
523 <?php
524 }
525
526 public static function get_form_nav_items($id) {
527 $nav_items = array(
528 array(
529 'link' => admin_url('admin.php?page=hashform&hashform_action=edit&id=' . absint($id)),
530 'label' => esc_html__('Build', 'hash-form'),
531 'current' => array('edit', 'new', 'duplicate'),
532 'page' => 'hashform'
533 ),
534 array(
535 'link' => admin_url('admin.php?page=hashform&hashform_action=settings&id=' . absint($id)),
536 'label' => esc_html__('Settings', 'hash-form'),
537 'current' => array('settings'),
538 'page' => 'hashform'
539 ),
540 array(
541 'link' => admin_url('admin.php?page=hashform&hashform_action=style&id=' . absint($id)),
542 'label' => esc_html__('Style', 'hash-form'),
543 'current' => array('style'),
544 'page' => 'hashform'
545 ),
546 array(
547 'link' => admin_url('admin.php?page=hashform-entries&form_id=' . absint($id)),
548 'label' => esc_html__('Entries', 'hash-form'),
549 'current' => array(),
550 'page' => 'hashform-entries'
551 ),
552 );
553 return $nav_items;
554 }
555
556 public static function is_current_page($page, $action = array()) {
557 $current_page = HashFormHelper::get_var('page');
558 $hashform_action = HashFormHelper::get_var('hashform_action');
559
560 if (($page == $current_page) && (!empty($hashform_action) && in_array($hashform_action, $action))) {
561 return true;
562 }
563 return false;
564 }
565
566 public static function get_all_forms() {
567 global $wpdb;
568 $results = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$wpdb->prefix}hashform_forms WHERE id!=%d", 0));
569 return $results;
570 }
571
572 /**
573 * The forms a visitor could actually be shown.
574 *
575 * get_all_forms() means all of them, trashed included, which is right for
576 * anything looking back at what exists. It is wrong for a chooser: every
577 * dropdown that offers a form to display was listing forms their owner had
578 * thrown away, and picking one rendered it on the page.
579 *
580 * @return array
581 */
582 public static function get_published_forms() {
583 global $wpdb;
584
585 return $wpdb->get_results($wpdb->prepare(
586 "SELECT * FROM {$wpdb->prefix}hashform_forms WHERE status = %s ORDER BY name",
587 'published'
588 ));
589 }
590
591 public static function get_form_vars($id) {
592 global $wpdb;
593
594 /**
595 * Supply a form without one being stored.
596 *
597 * Return anything other than null and the query below is skipped. It is
598 * what lets a form be rendered from a definition held in memory - the
599 * template demos read their forms out of a json file rather than
600 * writing one row per template into the database just to look at it.
601 *
602 * @param object|null $form null to load it as usual.
603 * @param int $id
604 */
605 $pre = apply_filters('hashform_pre_get_form_vars', null, $id);
606
607 if (null !== $pre) {
608 return $pre;
609 }
610
611 $results = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}hashform_forms WHERE id=%d", $id));
612
613 if (!$results) {
614 return;
615 }
616
617 foreach ($results as $key => $value) {
618 $results->$key = maybe_unserialize($value);
619 }
620
621 return $results;
622 }
623
624 public static function get_form_title($id) {
625 global $wpdb;
626
627 // Rendering calls this once or twice per field; cache per request.
628 static $titles = array();
629 if (array_key_exists($id, $titles)) {
630 return $titles[$id];
631 }
632
633 $name = $wpdb->get_var($wpdb->prepare("SELECT name FROM {$wpdb->prefix}hashform_forms WHERE id=%d", $id));
634 $titles[$id] = (null === $name) ? null : maybe_unserialize($name);
635
636 return $titles[$id];
637 }
638
639 public function init_overlay_html() {
640 $plugin_path = HASHFORM_PATH;
641 $new_form_overlay = apply_filters('hashform_new_form_overlay_template', $plugin_path . 'admin/forms/new-form-overlay.php');
642
643 if (HashFormHelper::is_form_listing_page()) {
644 include $new_form_overlay;
645 }
646
647 if (HashFormHelper::is_form_builder_page()) {
648 include $plugin_path . 'admin/forms/shortcode-overlay.php';
649 }
650 }
651
652 public function save_form_settings() {
653 HashFormCapabilities::require_cap_ajax('hashform_edit_forms');
654
655 $json_vars = htmlspecialchars_decode(nl2br(str_replace('&quot;', '"', HashFormHelper::get_post('hashform_compact_fields'))));
656 $vars = HashFormHelper::parse_json_array($json_vars);
657
658 if (!isset($vars['hashform_process_form_nonce']) || !wp_verify_nonce($vars['hashform_process_form_nonce'], 'hashform_process_form_action')) {
659 wp_die(esc_html__('Sorry, Nonce did not verify.', 'hash-form'));
660 }
661
662 $email_to_array = array();
663 $email_to_rows = isset($vars['email_to']) ? (array) $vars['email_to'] : array();
664 foreach ($email_to_rows as $row) {
665 $email_to_val = trim($row);
666 if ($email_to_val) {
667 $email_to_array[] = $email_to_val;
668 }
669 }
670
671 $vars['email_to'] = implode(',', $email_to_array);
672 $vars = self::drop_incomplete_conditions($vars);
673 $id = isset($vars['id']) ? absint($vars['id']) : HashFormHelper::get_var('id', 'absint');
674 unset($vars['id'], $vars['hashform_process_form_nonce'], $vars['_wp_http_referer']);
675
676 self::update_settings($id, $vars);
677 $message = '<span class="mdi mdi-check-circle"></span>' . esc_html__('Form was successfully updated.', 'hash-form');
678 wp_die(wp_kses_post($message));
679
680 }
681
682 public function save_form_style() {
683 HashFormCapabilities::require_cap_ajax('hashform_edit_forms');
684
685 check_ajax_referer('hashform_backend_ajax', 'backend_nonce');
686
687 $json_vars = htmlspecialchars_decode(nl2br(str_replace('&quot;', '"', HashFormHelper::get_post('hashform_compact_fields'))));
688 $vars = HashFormHelper::parse_json_array($json_vars);
689 $id = isset($vars['id']) ? absint($vars['id']) : HashFormHelper::get_var('id', 'absint');
690
691 self::update_style($id, $vars);
692 $message = '<span class="mdi mdi-check-circle"></span>' . esc_html__('Form was successfully updated.', 'hash-form');
693 wp_die(wp_kses_post($message));
694 }
695
696 public function form_preview() {
697 HashFormCapabilities::require_cap_ajax('hashform_edit_forms');
698
699 check_ajax_referer('hashform_backend_ajax', 'backend_nonce');
700
701 ob_start();
702 remove_action('wp_head', 'print_emoji_detection_script', 7);
703 remove_action('wp_print_styles', 'print_emoji_styles');
704 wp_head();
705
706 $form_id = HashFormHelper::get_post('form_id', 'absint');
707
708 HashFormPreview::show_form($form_id);
709 wp_footer();
710 wp_send_json_success(ob_get_clean());
711
712 }
713
714 public static function update_settings($id, $values) {
715 global $wpdb;
716 $values = HashFormHelper::recursive_parse_args($values, HashFormHelper::get_form_settings_checkbox_settings());
717 $values = HashFormHelper::sanitize_array($values, HashFormHelper::get_form_settings_sanitize_rules());
718
719 $new_values = array(
720 'settings' => serialize($values)
721 );
722 if (!empty($new_values)) {
723 $query_results = $wpdb->update($wpdb->prefix . 'hashform_forms', $new_values, array('id' => $id));
724 }
725 return $query_results;
726 }
727
728 public static function update_style($id, $value) {
729 global $wpdb;
730 $new_values = array(
731 'styles' => serialize(HashFormHelper::sanitize_array($value))
732 );
733 if (!empty($new_values)) {
734 $query_results = $wpdb->update($wpdb->prefix . 'hashform_forms', $new_values, array('id' => $id));
735 }
736 return $query_results;
737 }
738
739 /**
740 * What a rule does when it matches.
741 */
742 public static function condition_actions() {
743 return array(
744 'show' => esc_html__('Show', 'hash-form'),
745 'hide' => esc_html__('Hide', 'hash-form'),
746 );
747 }
748
749 /**
750 * How a rule compares the answer it watches.
751 */
752 public static function condition_operators() {
753 return array(
754 'equal' => esc_html__('Equals to', 'hash-form'),
755 'not_equal' => esc_html__('Not Equals to', 'hash-form'),
756 'greater_than' => esc_html__('Greater Than', 'hash-form'),
757 'greater_than_or_equal' => esc_html__('Greater Than Or Equals to', 'hash-form'),
758 'less_than' => esc_html__('Less Than', 'hash-form'),
759 'less_than_or_equal' => esc_html__('Less Than Or Equals to', 'hash-form'),
760 'is_like' => esc_html__('Is Like', 'hash-form'),
761 'is_not_like' => esc_html__('Is Not Like', 'hash-form'),
762 );
763 }
764
765 /**
766 * Fields a rule can point at.
767 *
768 * Layout fields hold no answer, so they can neither be watched nor be
769 * usefully shown and hidden. The trigger side excludes a little more:
770 * name and address post several values under one id, which the comparison
771 * has no way to pick between.
772 *
773 * This list used to be written out four times — twice in the panel and
774 * twice in the AJAX handler that appends a row — and the copies had
775 * already drifted.
776 *
777 * @param object[] $fields Form fields.
778 * @param bool $trigger Whether this is the watched side of a rule.
779 * @return object[]
780 */
781 public static function condition_fields($fields, $trigger = false) {
782 $skip = array('heading', 'paragraph', 'separator', 'spacer', 'image', 'captcha');
783
784 if ($trigger) {
785 // html and multi_step post nothing, so a rule watching one could
786 // never match. They stay available on the other side, where a
787 // rule can still show and hide them.
788 $skip = array_merge($skip, array('name', 'address', 'html', 'multi_step'));
789 }
790
791 $usable = array();
792
793 foreach ($fields as $field) {
794 if (!in_array($field->type, $skip, true)) {
795 $usable[] = $field;
796 }
797 }
798
799 return $usable;
800 }
801
802 /**
803 * One rule, as shown in the Conditional Logic panel.
804 *
805 * The panel and the AJAX handler that appends a row both render through
806 * here, so a newly added rule cannot look different from a saved one —
807 * which it did: the saved rows carried untranslated English labels.
808 *
809 * @param object[] $fields Form fields.
810 * @param array $row Saved rule, or empty for a new one.
811 */
812 public static function condition_row_html($fields, $row = array()) {
813 $value = isset($row['compare_value']) ? $row['compare_value'] : '';
814 ?>
815 <div class="hf-condition-row">
816 <div class="hf-condition-head">
817 <span class="hf-condition-index" aria-hidden="true"></span>
818 <button type="button" class="hf-condition-remove" title="<?php esc_attr_e('Delete this rule', 'hash-form'); ?>">
819 <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span>
820 <span class="screen-reader-text"><?php esc_html_e('Delete this rule', 'hash-form'); ?></span>
821 </button>
822 </div>
823
824 <div class="hf-condition-grid">
825 <label class="hf-condition-cell hf-condition-cell-action">
826 <span><?php esc_html_e('Action', 'hash-form'); ?></span>
827 <select name="condition_action[]">
828 <?php foreach (self::condition_actions() as $key => $label) { ?>
829 <option value="<?php echo esc_attr($key); ?>" <?php selected(isset($row['condition_action']) ? $row['condition_action'] : '', $key); ?>><?php echo esc_html($label); ?></option>
830 <?php } ?>
831 </select>
832 </label>
833
834 <label class="hf-condition-cell hf-condition-cell-target">
835 <span><?php esc_html_e('This field', 'hash-form'); ?></span>
836 <select name="compare_from[]">
837 <option value=""><?php esc_html_e('Select a field', 'hash-form'); ?></option>
838 <?php foreach (self::condition_fields($fields) as $field) { ?>
839 <option value="<?php echo esc_attr($field->id); ?>" <?php selected(isset($row['compare_from']) ? $row['compare_from'] : '', $field->id); ?>><?php echo esc_html($field->name . ' (ID: ' . $field->id . ')'); ?></option>
840 <?php } ?>
841 </select>
842 </label>
843
844 <label class="hf-condition-cell hf-condition-cell-trigger">
845 <span><?php esc_html_e('When', 'hash-form'); ?></span>
846 <select name="compare_to[]">
847 <option value=""><?php esc_html_e('Select a field', 'hash-form'); ?></option>
848 <?php foreach (self::condition_fields($fields, true) as $field) { ?>
849 <option value="<?php echo esc_attr($field->id); ?>" <?php selected(isset($row['compare_to']) ? $row['compare_to'] : '', $field->id); ?>><?php echo esc_html($field->name . ' (ID: ' . $field->id . ')'); ?></option>
850 <?php } ?>
851 </select>
852 </label>
853
854 <label class="hf-condition-cell hf-condition-cell-operator">
855 <span><?php esc_html_e('Is', 'hash-form'); ?></span>
856 <select name="compare_condition[]">
857 <?php foreach (self::condition_operators() as $key => $label) { ?>
858 <option value="<?php echo esc_attr($key); ?>" <?php selected(isset($row['compare_condition']) ? $row['compare_condition'] : '', $key); ?>><?php echo esc_html($label); ?></option>
859 <?php } ?>
860 </select>
861 </label>
862
863 <label class="hf-condition-cell hf-condition-cell-value">
864 <span><?php esc_html_e('Value', 'hash-form'); ?></span>
865 <input type="text" name="compare_value[]" value="<?php echo esc_attr($value); ?>" placeholder="<?php esc_attr_e('Answer to compare against', 'hash-form'); ?>" />
866 </label>
867 </div>
868 </div>
869 <?php
870 }
871
872 public function add_more_condition_block() {
873 HashFormCapabilities::require_cap_ajax('hashform_edit_forms');
874
875 check_ajax_referer('hashform_backend_ajax', 'backend_nonce');
876
877 $form_id = HashFormHelper::get_post('form_id', 'absint', 0);
878 self::condition_row_html(HashFormFields::get_form_fields($form_id));
879 die();
880 }
881
882 /**
883 * Discards conditional logic rules that name no fields.
884 *
885 * The panel is saved over AJAX from serializeArray(), so the browser's
886 * own required checks never run — a rule left half-filled used to be
887 * stored, then read back on every page load as a rule that can never
888 * match. The five columns are parallel arrays, so they are rebuilt
889 * together or they fall out of step.
890 *
891 * @param array $vars Posted settings.
892 * @return array
893 */
894 private static function drop_incomplete_conditions($vars) {
895 if (!isset($vars['condition_action']) || !is_array($vars['condition_action'])) {
896 return $vars;
897 }
898
899 $columns = array('condition_action', 'compare_from', 'compare_to', 'compare_condition', 'compare_value');
900 $kept = array_fill_keys($columns, array());
901
902 foreach (array_keys($vars['condition_action']) as $key) {
903 $from = isset($vars['compare_from'][$key]) ? trim($vars['compare_from'][$key]) : '';
904 $to = isset($vars['compare_to'][$key]) ? trim($vars['compare_to'][$key]) : '';
905
906 if ('' === $from || '' === $to) {
907 continue;
908 }
909
910 foreach ($columns as $column) {
911 $kept[$column][] = isset($vars[$column][$key]) ? $vars[$column][$key] : '';
912 }
913 }
914
915 return array_merge($vars, $kept);
916 }
917
918 public static function get_show_hide_conditions($id) {
919 $form = HashFormBuilder::get_form_vars($id);
920 $settings = isset($form->settings) ? $form->settings : array();
921 $conditions = array();
922 if (isset($settings['condition_action']) && $settings['condition_action']) {
923 foreach ($settings['condition_action'] as $key => $row) {
924 $condition = array(
925 'condition_action' => $settings['condition_action'][$key],
926 'compare_from' => $settings['compare_from'][$key],
927 'compare_to' => $settings['compare_to'][$key],
928 'compare_condition' => $settings['compare_condition'][$key],
929 'compare_value' => $settings['compare_value'][$key],
930 );
931 $conditions[] = $condition;
932 }
933 }
934 return $conditions;
935 }
936
937 /**
938 * Point a copied form's rules at the copy's own fields.
939 *
940 * A rule stores field ids. Duplicating a form, importing one, or starting
941 * from a template all build fresh fields with fresh ids, and the rules
942 * were carried across untouched - so every one of them named a field that
943 * did not exist on the new form. Nothing was shown or hidden, and the
944 * Conditional Logic panel offered "Select a field" for both ends.
945 *
946 * A rule whose ends cannot both be mapped is dropped rather than kept
947 * pointing at a stranger: the id it holds may since have been handed to an
948 * unrelated field on another form, and a rule that silently governs the
949 * wrong field is worse than no rule.
950 *
951 * @param array $settings The new form's settings, conditions included.
952 * @param array $map old field id => new field id.
953 * @param int $dropped Set to the number of rules that could not be kept.
954 * @return array The settings, with the rules rewritten.
955 */
956 public static function remap_conditions($settings, $map, &$dropped = 0) {
957 $dropped = 0;
958
959 if (empty($settings['condition_action']) || !is_array($settings['condition_action'])) {
960 return $settings;
961 }
962
963 $columns = array('condition_action', 'compare_from', 'compare_to', 'compare_condition', 'compare_value');
964 $kept = array_fill_keys($columns, array());
965
966 foreach (array_keys($settings['condition_action']) as $key) {
967 $from = isset($settings['compare_from'][$key]) ? (int) $settings['compare_from'][$key] : 0;
968 $to = isset($settings['compare_to'][$key]) ? (int) $settings['compare_to'][$key] : 0;
969
970 if (!isset($map[$from], $map[$to])) {
971 $dropped++;
972 continue;
973 }
974
975 foreach ($columns as $column) {
976 $value = isset($settings[$column][$key]) ? $settings[$column][$key] : '';
977
978 if ('compare_from' === $column) {
979 $value = (string) $map[$from];
980 } else if ('compare_to' === $column) {
981 $value = (string) $map[$to];
982 }
983
984 $kept[$column][] = $value;
985 }
986 }
987
988 foreach ($columns as $column) {
989 $settings[$column] = $kept[$column];
990 }
991
992 return $settings;
993 }
994
995 /**
996 * Point a copied form's calculation formulas at the copy's own fields.
997 *
998 * A formula refers to its inputs as #field_id_12, and a repeater column as
999 * #repeater_12_0 - field ids again, with the same problem the show and hide
1000 * rules had: copy the form and every tag names a field that is not there,
1001 * so the total silently stops adding up.
1002 *
1003 * Runs over the new form's own fields once they exist, so it is given the
1004 * form to walk rather than the values to rewrite.
1005 *
1006 * @param int $form_id The form that has just been built.
1007 * @param array $map old field id => new field id.
1008 * @return int How many formulas were rewritten.
1009 */
1010 public static function remap_calculation_formulas($form_id, $map) {
1011 global $wpdb;
1012
1013 if (!$map) {
1014 return 0;
1015 }
1016
1017 $rewritten = 0;
1018
1019 foreach (HashFormFields::get_form_fields($form_id) as $field) {
1020 $options = $field->field_options;
1021
1022 if (!is_array($options) || empty($options['formula'])) {
1023 continue;
1024 }
1025
1026 $formula = $options['formula'];
1027
1028 // Both tag shapes in one pass, so a formula that mixes them cannot
1029 // half-update. The id is looked up as it is met; anything the map
1030 // does not know is left exactly as it was rather than guessed at.
1031 $updated = preg_replace_callback(
1032 '/#(field_id|repeater)_(\d+)(_\d+)?/',
1033 function ($m) use ($map) {
1034 $old = (int) $m[2];
1035
1036 if (!isset($map[$old])) {
1037 return $m[0];
1038 }
1039
1040 return '#' . $m[1] . '_' . $map[$old] . (isset($m[3]) ? $m[3] : '');
1041 },
1042 $formula
1043 );
1044
1045 if ($updated === $formula) {
1046 continue;
1047 }
1048
1049 $options['formula'] = $updated;
1050 $wpdb->update(
1051 $wpdb->prefix . 'hashform_fields',
1052 array('field_options' => serialize($options)),
1053 array('id' => (int) $field->id)
1054 );
1055 $rewritten++;
1056 }
1057
1058 return $rewritten;
1059 }
1060
1061 /**
1062 * Which fields a form's rules touch, and what each rule says.
1063 *
1064 * A rule names two fields: compare_from is the one shown or hidden, and
1065 * compare_to is the one whose answer decides it. Both ends are worth
1066 * marking on the canvas - a field that disappears for some visitors, and a
1067 * field that is doing the deciding - so this returns an entry for each,
1068 * keyed by field id.
1069 *
1070 * Read once per form and held for the request, because it is called from
1071 * inside the loop that renders every field.
1072 *
1073 * @param int $form_id
1074 * @return array field id => array( 'target' => string[], 'trigger' => string[] )
1075 */
1076 public static function get_condition_hints($form_id) {
1077 static $cache = array();
1078
1079 $form_id = absint($form_id);
1080
1081 if (isset($cache[$form_id])) {
1082 return $cache[$form_id];
1083 }
1084
1085 $conditions = self::get_show_hide_conditions($form_id);
1086
1087 if (!$conditions) {
1088 $cache[$form_id] = array();
1089 return $cache[$form_id];
1090 }
1091
1092 $names = array();
1093
1094 foreach (HashFormFields::get_form_fields($form_id) as $field) {
1095 $names[(int) $field->id] = $field->name;
1096 }
1097
1098 $operators = self::condition_operators();
1099 $hints = array();
1100
1101 foreach ($conditions as $row) {
1102 $target = isset($row['compare_from']) ? (int) $row['compare_from'] : 0;
1103 $trigger = isset($row['compare_to']) ? (int) $row['compare_to'] : 0;
1104
1105 // A half-filled rule does nothing on the front end, so it says
1106 // nothing here either.
1107 if (!$target || !$trigger) {
1108 continue;
1109 }
1110
1111 /*
1112 * Raw strings here, escaped where they are printed. Building them
1113 * with esc_html__() turned the quotes into entities before the
1114 * sentence was assembled, which then had to survive a second pass
1115 * to come out right.
1116 */
1117 $is_show = !isset($row['condition_action']) || 'hide' !== $row['condition_action'];
1118 $operator = isset($operators[$row['compare_condition']])
1119 ? strtolower($operators[$row['compare_condition']])
1120 : $row['compare_condition'];
1121 $unknown = __('a deleted field', 'hash-form');
1122 $trigger_name = isset($names[$trigger]) ? $names[$trigger] : $unknown;
1123 $target_name = isset($names[$target]) ? $names[$target] : $unknown;
1124 $value = isset($row['compare_value']) ? $row['compare_value'] : '';
1125
1126 // "Show"/"Hide" name the rule on the settings panel, where they are
1127 // an instruction. Here they describe a field, so they need the
1128 // participle: "Shown when ...", not "Show when ...".
1129 $hints[$target]['target'][] = $is_show
1130 /* translators: 1: the field being watched, 2: the comparison, 3: the value compared against. */
1131 ? sprintf(__('Shown when "%1$s" %2$s "%3$s"', 'hash-form'), $trigger_name, $operator, $value)
1132 /* translators: 1: the field being watched, 2: the comparison, 3: the value compared against. */
1133 : sprintf(__('Hidden when "%1$s" %2$s "%3$s"', 'hash-form'), $trigger_name, $operator, $value);
1134
1135 $hints[$trigger]['trigger'][] = $is_show
1136 /* translators: %s: the field this one decides. */
1137 ? sprintf(__('Decides whether "%s" is shown', 'hash-form'), $target_name)
1138 /* translators: %s: the field this one decides. */
1139 : sprintf(__('Decides whether "%s" is hidden', 'hash-form'), $target_name);
1140 }
1141
1142 $cache[$form_id] = $hints;
1143
1144 return $cache[$form_id];
1145 }
1146
1147 public function add_plugin_action_link($links) {
1148 $custom['settings'] = sprintf(
1149 '<a href="%s" aria-label="%s">%s</a>', esc_url(add_query_arg('page', 'hashform', admin_url('admin.php'))), esc_attr__('Hash Forms', 'hash-form'), esc_html__('Settings', 'hash-form')
1150 );
1151
1152 return array_merge($custom, (array) $links);
1153 }
1154
1155 public function file_upload_action() {
1156 if (!wp_verify_nonce(HashFormHelper::get_var('file_uploader_nonce'), 'hashform-upload-ajax-nonce')) {
1157 die();
1158 }
1159
1160 $allowedExtensions = HashFormHelper::get_var('allowedExtensions');
1161 $sizeLimit = HashFormHelper::get_var('sizeLimit');
1162 $upload_dir = wp_upload_dir();
1163
1164 // One shared list, defined in admin/forms/sanitization.php, so this
1165 // and the field's own sanitizer cannot disagree about a format.
1166 $default_allowed_extenstions = hashform_allowed_file_extensions();
1167
1168 // get_allowed_mime_types() is applied again inside HashFormFileUploader,
1169 // so anything this site has not actually enabled is still refused.
1170
1171 // The request controls the shape of this value; a scalar would emit a
1172 // warning into the middle of the JSON response below.
1173 $allowedExtensions = is_array($allowedExtensions) ? $allowedExtensions : array();
1174
1175 $filtered_allowed_extenstions = array();
1176 foreach ($allowedExtensions as $ext) {
1177 if (in_array($ext, $default_allowed_extenstions, true)) {
1178 $filtered_allowed_extenstions[] = $ext;
1179 }
1180 }
1181
1182 /*
1183 * Tested after filtering rather than before. A request naming only
1184 * extensions this plugin does not recognise used to satisfy a check on
1185 * the raw list and then hand the uploader an empty one, which it read
1186 * as "no restriction configured".
1187 *
1188 * Answered rather than dropped: an empty body left the uploader
1189 * waiting on a response it could not parse, so the file appeared
1190 * to hang instead of failing.
1191 */
1192 if (!$filtered_allowed_extenstions) {
1193 wp_send_json(array('error' => esc_html__('This type of file is not allowed.', 'hash-form')));
1194 }
1195
1196 // Never trust the request for the size limit beyond what the
1197 // server would accept anyway.
1198 $sizeLimit = min(absint($sizeLimit), wp_max_upload_size());
1199
1200 $uploader = new HashFormFileUploader($filtered_allowed_extenstions, $sizeLimit);
1201
1202 /*
1203 * Anything the upload machinery prints - a php notice from a host
1204 * with a hardened open_basedir, a warning out of the mime sniffing -
1205 * would land in front of the json and leave the browser unable to
1206 * parse the reply. Whatever gets emitted is captured and dropped so
1207 * the response is only ever the json below.
1208 */
1209 ob_start();
1210 $result = $uploader->handleUpload($upload_dir['basedir'] . HASHFORM_UPLOAD_DIR, false, $upload_dir['baseurl'] . HASHFORM_UPLOAD_DIR);
1211 $stray_output = ob_get_clean();
1212
1213 if ($stray_output && defined('WP_DEBUG') && WP_DEBUG) {
1214 HashFormHelper::log('Upload handler produced unexpected output: ' . $stray_output);
1215 }
1216
1217 wp_send_json($result);
1218 }
1219
1220 public function file_delete_action() {
1221 if (wp_verify_nonce(HashFormHelper::get_post('_wpnonce'), 'hashform-upload-ajax-nonce')) {
1222 $path = str_replace(' ', '+', HashFormHelper::get_post('path', 'wp_kses_post'));
1223 $file = HashFormHelper::decrypt($path);
1224
1225 // An empty name would resolve to the temp directory itself.
1226 if ($file) {
1227 $upload_dir = wp_upload_dir();
1228 $temp_dir = $upload_dir['basedir'] . HASHFORM_UPLOAD_DIR . '/temp';
1229
1230 // Unlike wp_delete_file(), this confirms the resolved path sits
1231 // inside the temp directory and returns a usable bool on every
1232 // supported WordPress version.
1233 if (wp_delete_file_from_directory($temp_dir . '/' . $file, $temp_dir)) {
1234 die('success');
1235 }
1236 }
1237 }
1238 die('error');
1239 }
1240
1241 public static function remove_old_temp_files() {
1242 $max_file_age = apply_filters('hashform_temp_file_delete_time', 2 * 3600);
1243 $upload_dir = wp_upload_dir();
1244 $temp_dir = $upload_dir['basedir'] . HASHFORM_UPLOAD_DIR . '/temp/';
1245
1246 // Remove old temp files
1247 if (is_dir($temp_dir) and ($dir = opendir($temp_dir))) {
1248 while (($file = readdir($dir)) !== false) {
1249 $temp_file_path = $temp_dir . $file;
1250 if (!is_file($temp_file_path)) {
1251 continue;
1252 }
1253 if ((filemtime($temp_file_path) < time() - $max_file_age)) {
1254 wp_delete_file($temp_file_path);
1255 }
1256 }
1257 closedir($dir);
1258 }
1259 }
1260
1261 public function admin_notice() {
1262 add_action('admin_notices', array($this, 'admin_notice_content'));
1263 }
1264
1265 public function admin_notice_content() {
1266 if (!$this->is_dismissed('review') && !empty(get_option('hashform_first_activation')) && time() > get_option('hashform_first_activation') + 15 * DAY_IN_SECONDS) {
1267 $this->review_notice();
1268 }
1269 }
1270
1271 public static function is_dismissed($notice) {
1272 $dismissed = get_option('hashform_dismissed_notices', array());
1273
1274 // Handle legacy user meta
1275 $dismissed_meta = get_user_meta(get_current_user_id(), 'hashform_dismissed_notices', true);
1276 if (is_array($dismissed_meta)) {
1277 if (array_diff($dismissed_meta, $dismissed)) {
1278 $dismissed = array_merge($dismissed, $dismissed_meta);
1279 update_option('hashform_dismissed_notices', $dismissed);
1280 }
1281 if (!is_multisite()) {
1282 // Don't delete on multisite to avoid the notices to appear in other sites.
1283 delete_user_meta(get_current_user_id(), 'hashform_dismissed_notices');
1284 }
1285 }
1286
1287 return in_array($notice, $dismissed);
1288 }
1289
1290 public function review_notice() {
1291 ?>
1292 <div class="hashform-notice notice notice-info">
1293 <?php $this->dismiss_button('review'); ?>
1294 <div class="hashform-notice-logo">
1295 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 117.66 152.27">
1296 <g>
1297 <g>
1298 <path d="M0,3.46A3.46,3.46,0,0,1,3.14,0h80A3.53,3.53,0,0,1,85.6,1l31,31a3.47,3.47,0,0,1,1,2.43V148.81a3.46,3.46,0,0,1-3.46,3.46H31.63a3.46,3.46,0,1,1,0-6.92h79.11V38.07H83.05a3.46,3.46,0,0,1-3.46-3.46V6.92H6.92V145.35H14a3.46,3.46,0,1,1,0,6.92H3.46A3.46,3.46,0,0,1,0,148.81ZM106,31.15,86.51,11.68V31.15Z" />
1299 <path d="M78.66,59.3H95.09v6.61H78.66V85.75H72.05V42.87h6.61Zm0,39.67v16.42H72.05V99H52.22V92.36H95.09V99ZM39,99H22.57V92.36H39V72.52h6.61V115.4H39ZM39,59.3V42.87h6.61V59.3H65.44v6.61H22.57V59.3Z" />
1300 </g>
1301 </g>
1302 </svg>
1303 </div>
1304
1305 <div class="hashform-notice-content">
1306 <p>
1307 <?php
1308 printf(
1309 /* translators: %1$s is link start tag, %2$s is link end tag. */
1310 esc_html__('Great to see that you have been using Hash Form for some time. We hope you love it, and we would really appreciate it if you would %1$sleave a review%2$s and spread your words to the world.', 'hash-form'), '<a target="_blank" href="https://wordpress.org/support/plugin/hash-form/reviews/">', '</a>'
1311 );
1312 ?>
1313 </p>
1314 <a target="_blank" class="button button-primary button-large" href="https://wordpress.org/support/plugin/hash-form/reviews/"><span class="dashicons dashicons-thumbs-up"></span><?php echo esc_html__('Yes, of course', 'hash-form') ?></a> &nbsp;
1315 <a class="button button-large" href="<?php echo esc_url(wp_nonce_url(add_query_arg('hashform-hide-notice', 'review'), 'review', 'hashform_notice_nonce')); ?>"><span class="dashicons dashicons-yes"></span><?php echo esc_html__('I have already rated', 'hash-form') ?></a>
1316 </div>
1317 </div>
1318 <?php
1319 }
1320
1321 public function welcome_init() {
1322 if (!get_option('hashform_first_activation')) {
1323 update_option('hashform_first_activation', time());
1324 }
1325
1326 if (isset($_GET['hashform-hide-notice'], $_GET['hashform_notice_nonce'])) {
1327 $notice = sanitize_key($_GET['hashform-hide-notice']);
1328 check_admin_referer($notice, 'hashform_notice_nonce');
1329 self::dismiss($notice);
1330 wp_safe_redirect(remove_query_arg(array('hashform-hide-notice', 'hashform_notice_nonce'), wp_get_referer()));
1331 exit;
1332 }
1333 }
1334
1335 public function dismiss_button($name) {
1336 printf('<a class="notice-dismiss" href="%s"><span class="screen-reader-text">%s</span></a>', esc_url(wp_nonce_url(add_query_arg('hashform-hide-notice', $name), $name, 'hashform_notice_nonce')), esc_html__('Dismiss this notice.', 'hash-form'));
1337 }
1338
1339 public static function dismiss($notice) {
1340 $dismissed = get_option('hashform_dismissed_notices', array());
1341
1342 if (!in_array($notice, $dismissed)) {
1343 $dismissed[] = $notice;
1344 update_option('hashform_dismissed_notices', array_unique($dismissed));
1345 }
1346 }
1347
1348
1349 public function register_translation_strings() {
1350 // Without WPML there is nothing to register; skip the full forms +
1351 // fields scan that would otherwise run on every request.
1352 if (!has_action('wpml_register_single_string')) {
1353 return;
1354 }
1355
1356 $all_forms = HashFormListing::get_published_table_data();
1357
1358 foreach ($all_forms as $form) {
1359 $form_title = $form['name'];
1360 $options = HashFormHelper::unserialize_or_decode($form['options']);
1361 $settings = HashFormHelper::unserialize_or_decode($form['settings']);
1362 $string_array = array(
1363 'Name' => $form_title,
1364 'Description' => $form['description'],
1365 'Email Auto Responder Subject' => isset($settings['email_subject_ar']) ? $settings['email_subject_ar'] : '',
1366 'Email Auto Responder Message' => isset($settings['email_message_ar']) ? $settings['email_message_ar'] : '',
1367 'Confirmation Message' => isset($settings['confirmation_message']) ? $settings['confirmation_message'] : '',
1368 'Error Message' => isset($settings['error_message']) ? $settings['error_message'] : '',
1369 'Submit Button Text' => isset($options['submit_value']) ? $options['submit_value'] : ''
1370 );
1371
1372 foreach ($string_array as $title => $strings) {
1373 if (has_action('wpml_register_single_string')) {
1374 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML's own hook, not one this plugin owns.
1375 do_action('wpml_register_single_string', 'Hash Form', $form_title . ' - ' . $title, $strings);
1376 }
1377 }
1378
1379 $form_fields = HashFormFields::get_form_fields($form['id']);
1380 foreach ($form_fields as $field) {
1381 $string_array = array(
1382 'Field Label' => $field->name,
1383 'Field Description' => $field->description,
1384 'Field Validation Message' => isset($field->field_options['invalid']) ? $field->field_options['invalid'] : '',
1385 );
1386
1387 if ($field->type == 'paragraph' && isset($field->field_options['content'])) {
1388 $string_array['Field Content'] = $field->field_options['content'];
1389 }
1390
1391 if ($field->type == 'name') {
1392 $name_arrs = array('full', 'first', 'middle', 'last');
1393 foreach ($name_arrs as $name) {
1394 $value = isset($field->default_value[$name]) ? $field->default_value[$name] : '';
1395 $placeholder = isset($field->placeholder[$name]) ? $field->placeholder[$name] : '';
1396 $label = isset($field->field_options['desc'][$name]) ? $field->field_options['desc'][$name] : '';
1397 $string_array[ucwords($name) . ' Label'] = $label;
1398 $string_array[ucwords($name) . ' Value'] = $value;
1399 $string_array[ucwords($name) . ' Placeholder'] = $placeholder;
1400 }
1401 }
1402
1403 if ($field->type == 'address') {
1404 $address_arrs = array('line1', 'line2', 'city', 'state', 'zip', 'country');
1405 foreach ($address_arrs as $address) {
1406 $value = isset($field->default_value[$address]) ? $field->default_value[$address] : '';
1407 $placeholder = isset($field->placeholder[$address]) ? $field->placeholder[$address] : '';
1408 $label = isset($field->field_options['desc'][$address]) ? $field->field_options['desc'][$address] : '';
1409 $string_array[ucwords($address) . ' Label'] = $label;
1410 $string_array[ucwords($address) . ' Value'] = $value;
1411 $string_array[ucwords($address) . ' Placeholder'] = $placeholder;
1412 }
1413 }
1414
1415 if (isset($field->default_value) && $field->type != 'name') {
1416 if (is_array($field->default_value)) {
1417 foreach ($field->default_value as $key => $defval) {
1418 $string_array['Field Default ' . $key] = $defval;
1419 }
1420 } else {
1421 $string_array['Field Default'] = $field->default_value;
1422 }
1423 }
1424
1425 if (isset($field->field_options['placeholder'])) {
1426 if (is_array($field->field_options['placeholder'])) {
1427 foreach ($field->field_options['placeholder'] as $key => $defval) {
1428 $string_array['Field Placeholder ' . $key] = $defval;
1429 }
1430 } else {
1431 $string_array['Field Placeholder'] = $field->field_options['placeholder'];
1432 }
1433 }
1434
1435 foreach ($string_array as $title => $strings) {
1436 if (has_action('wpml_register_single_string')) {
1437 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML's own hook, not one this plugin owns.
1438 do_action('wpml_register_single_string', 'Hash Form', $form_title . ' - ' . $field->id . ' - ' . $title, $strings);
1439 }
1440 }
1441 }
1442 }
1443 }
1444
1445 public function translate_string($original_value, $domain, $name = '') {
1446 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML's own hook, not one this plugin owns.
1447 $wpml_translation = apply_filters('wpml_translate_single_string', $original_value, $domain, $name);
1448 if ($wpml_translation === $original_value && function_exists('pll__')) {
1449 return pll__($original_value);
1450 }
1451 return $wpml_translation;
1452 }
1453
1454 }
1455
1456 new HashFormBuilder();