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 / HashFormImportExport.php

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

427 lines 18.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 defined('ABSPATH') || die();
4
5 class HashFormImportExport {
6
7 public function __construct() {
8 // Process a settings export that generates a .json file of the form settings
9 add_action('admin_init', array($this, 'process_settings_export'));
10 // Process a settings export that generates a .json file of the form style
11 add_action('admin_init', array($this, 'process_style_export'));
12 // Process a settings import from a json file
13 add_action('admin_init', array($this, 'process_settings_import'));
14 // Process a style import from a json file
15 add_action('admin_init', array($this, 'process_style_import'));
16
17 // The panel imports through this, so it can hold a spinner and report
18 // a bad file in place. The admin_init handler stays as the no-JS path.
19 add_action('wp_ajax_hashform_import_form_settings', array($this, 'ajax_import_form_settings'));
20 }
21
22 /**
23 * Form import over AJAX. Same checks as the plain POST path; the outcome
24 * is JSON so the panel can stay on screen when a file is rejected.
25 */
26 public function ajax_import_form_settings() {
27 if (!HashFormCapabilities::user_can('hashform_edit_forms')) {
28 wp_send_json_error(array('message' => esc_html__('You are not allowed to import forms.', 'hash-form')), 403);
29 }
30
31 if (!wp_verify_nonce(HashFormHelper::get_post('hashform_imex_import_nonce'), 'hashform_imex_import_nonce')) {
32 wp_send_json_error(array('message' => esc_html__('Your session has expired. Reload the page and try again.', 'hash-form')), 403);
33 }
34
35 $form_id = HashFormHelper::get_post('hashform_form_id', 'absint');
36
37 if (!$form_id) {
38 wp_send_json_error(array('message' => esc_html__('No form was specified.', 'hash-form')));
39 }
40
41 $upload = self::read_uploaded_export('hashform_import_file');
42
43 if (is_wp_error($upload)) {
44 wp_send_json_error(array('message' => $upload->get_error_message()));
45 }
46
47 self::apply_export_to_form($form_id, $upload['data']);
48
49 HashFormHelper::set_message(esc_html__('Settings Imported Successfully', 'hash-form'));
50 wp_send_json_success(array('reload' => true));
51 }
52
53 public function process_settings_export() {
54 /*
55 * These four run on admin_init, so they see every admin page load and
56 * must fall through quietly when the request is not theirs. Dying here
57 * would take out the whole of wp-admin for anyone without the
58 * capability.
59 */
60 if (!HashFormCapabilities::user_can('hashform_edit_forms')) {
61 return;
62 }
63
64 $id = HashFormHelper::get_post('hashform_form_id', 'absint');
65
66 if ('export_form' != HashFormHelper::get_post('hashform_imex_action') || !$id) {
67 return;
68 }
69
70 if (!wp_verify_nonce(HashFormHelper::get_post('hashform_imex_export_nonce'), 'hashform_imex_export_nonce')) {
71 return;
72 }
73
74 global $wpdb;
75
76 $forms = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$wpdb->prefix}hashform_forms WHERE id=%d", $id));
77
78 foreach ($forms as $form) {
79 $form_styles = $form->styles ? unserialize($form->styles, array('allowed_classes' => false)) : [];
80 $exdat['form_key'] = $form->form_key ? $form->form_key : '';
81 $exdat['options'] = $form->options ? unserialize($form->options, array('allowed_classes' => false)) : [];
82 $exdat['status'] = $form->status ? $form->status : 'published';
83 $exdat['settings'] = $form->settings ? unserialize($form->settings, array('allowed_classes' => false)) : [];
84 $exdat['styles'] = $form_styles;
85 $exdat['created_at'] = $form->created_at ? $form->created_at : '';
86 $fields = HashFormFields::get_form_fields($form->id);
87 $exfield = array();
88 foreach ($fields as $field) {
89 $efield = array();
90 /*
91 * Carried so the show and hide rules can be pointed at the
92 * fields they mean once those fields are recreated with ids of
93 * their own. Nothing restores this id; it is a reference key
94 * for this file and no more.
95 */
96 $efield['id'] = absint($field->id);
97 $efield['name'] = $field->name;
98 $efield['description'] = $field->description;
99 $efield['type'] = $field->type;
100 $efield['default_value'] = $field->default_value;
101 $efield['options'] = $field->options;
102 $efield['field_order'] = absint($field->field_order);
103 $efield['required'] = absint($field->required);
104 $efield['field_options'] = $field->field_options;
105 $exfield[] = $efield;
106 }
107 $exdat['field'] = $exfield;
108
109 $form_style = isset($form_styles['form_style']) && $form_styles['form_style'] ? $form_styles['form_style'] : 'default-style';
110
111 if ($form_style == 'custom-style') {
112 $form_style_id = $form_styles['form_style_template'];
113 $hashform_styles = get_post_meta($form_style_id, 'hashform_styles', true);
114 $hashform_styles = HashFormHelper::sanitize_array($hashform_styles, HashFormStyles::get_styles_sanitize_array());
115 if ($hashform_styles) {
116 $exdat['style'] = $hashform_styles;
117 }
118 }
119
120 ignore_user_abort(true);
121
122 nocache_headers();
123 header('Content-Type: application/json; charset=utf-8');
124 header('Content-Disposition: attachment; filename=hf-' . $id . '-' . gmdate('m-d-Y') . '.json');
125 header("Expires: 0");
126
127 echo wp_json_encode($exdat);
128 exit;
129 }
130 }
131
132 public function process_style_export() {
133 /*
134 * These four run on admin_init, so they see every admin page load and
135 * must fall through quietly when the request is not theirs. Dying here
136 * would take out the whole of wp-admin for anyone without the
137 * capability.
138 */
139 if (!HashFormCapabilities::user_can('hashform_edit_forms')) {
140 return;
141 }
142
143 $id = HashFormHelper::get_post('hashform_style_id', 'absint');
144
145 if ('export_style' != HashFormHelper::get_post('hashform_imex_action') || !$id) {
146 return;
147 }
148
149 if (!wp_verify_nonce(HashFormHelper::get_post('hashform_imex_export_nonce'), 'hashform_imex_export_nonce')) {
150 return;
151 }
152
153 $hashform_styles = get_post_meta($id, 'hashform_styles', true);
154 $hashform_styles = HashFormHelper::sanitize_array($hashform_styles, HashFormStyles::get_styles_sanitize_array());
155
156 if ($hashform_styles) {
157
158 ignore_user_abort(true);
159
160 nocache_headers();
161 header('Content-Type: application/json; charset=utf-8');
162 header('Content-Disposition: attachment; filename=hf-style-' . $id . '-' . gmdate('m-d-Y') . '.json');
163 header("Expires: 0");
164
165 echo wp_json_encode($hashform_styles);
166 exit;
167 }
168 }
169
170 /**
171 * Reads an uploaded .json export and returns it as an array.
172 *
173 * Shared with the Pro plugin's Create New Form dialog, which uploads the
174 * same kind of file under a different field name. Every caller used to
175 * carry its own copy of these checks, and they drifted: the hardening
176 * here had to be applied three times, and twice it was not.
177 *
178 * @param string $file_key Key within $_FILES.
179 * @return array|WP_Error
180 */
181 // phpcs:disable WordPress.Security.NonceVerification.Missing -- every call site verifies its own nonce immediately before calling this; see import_form(), import_form_submit() and the Pro plugin's run_file_import().
182 public static function read_uploaded_export($file_key) {
183 $upload_error = isset($_FILES[$file_key]['error']) ? (int) $_FILES[$file_key]['error'] : UPLOAD_ERR_NO_FILE;
184
185 if (UPLOAD_ERR_NO_FILE === $upload_error) {
186 return new WP_Error('hashform_no_file', esc_html__('Please upload a file to import', 'hash-form'));
187 }
188
189 if (UPLOAD_ERR_OK !== $upload_error) {
190 return new WP_Error('hashform_upload_failed', esc_html__('The file could not be uploaded. It may be larger than this server allows.', 'hash-form'));
191 }
192
193 $filename = isset($_FILES[$file_key]['name']) ? sanitize_text_field(wp_unslash($_FILES[$file_key]['name'])) : '';
194 $extension = explode('.', $filename);
195 $extension = strtolower(end($extension));
196
197 if ('json' !== $extension) {
198 return new WP_Error('hashform_bad_extension', esc_html__('Please upload a valid .json file', 'hash-form'));
199 }
200
201 $tmp = isset($_FILES[$file_key]['tmp_name']) ? sanitize_text_field($_FILES[$file_key]['tmp_name']) : '';
202
203 // Confirms the path came from this request's upload rather than being
204 // any readable file on the server.
205 if (empty($tmp) || !is_uploaded_file($tmp)) {
206 return new WP_Error('hashform_no_file', esc_html__('Please upload a file to import', 'hash-form'));
207 }
208 // phpcs:enable
209
210 $contents = file_get_contents($tmp);
211 $imdat = (false === $contents) ? null : json_decode($contents, true);
212
213 if (!self::is_valid_export($imdat)) {
214 return new WP_Error('hashform_bad_file', esc_html__('Please upload a valid file to import', 'hash-form'));
215 }
216
217 return array('data' => $imdat, 'filename' => $filename);
218 }
219
220 /**
221 * The three keys every importer relies on being present.
222 */
223 public static function is_valid_export($imdat) {
224 return is_array($imdat) && isset($imdat['options'], $imdat['settings'], $imdat['styles']);
225 }
226
227 /**
228 * Writes a decoded export onto an existing form, replacing its fields.
229 *
230 * The single copy of what used to live in three places. Assumes the
231 * caller has already checked capability, nonce and that $imdat is valid.
232 *
233 * @param int $form_id Form to write onto.
234 * @param array $imdat Decoded export.
235 */
236 public static function apply_export_to_form($form_id, $imdat) {
237 global $wpdb;
238
239 $form_id = absint($form_id);
240
241 $options = HashFormHelper::recursive_parse_args($imdat['options'], HashFormHelper::get_form_options_default());
242 $options = HashFormHelper::sanitize_array($options, HashFormHelper::get_form_options_sanitize_rules());
243
244 $settings = HashFormHelper::recursive_parse_args($imdat['settings'], HashFormHelper::get_form_settings_default());
245 $settings = HashFormHelper::sanitize_array($settings, HashFormHelper::get_form_settings_sanitize_rules());
246
247 $styles = HashFormHelper::recursive_parse_args($imdat['styles'], array('form_style' => 'default-style', 'form_style_template' => ''));
248 $styles = HashFormHelper::sanitize_array($styles, HashFormHelper::get_form_styles_sanitize_rules());
249
250 if (isset($imdat['style'])) {
251 $new_post = array(
252 'post_type' => 'hashform-styles',
253 'post_title' => 'hashform-style-' . $form_id,
254 'post_status' => 'publish',
255 );
256 $style_id = wp_insert_post($new_post);
257 $hashform_styles = HashFormHelper::recursive_parse_args($imdat['style'], HashFormStyles::default_styles());
258 $hashform_styles = HashFormHelper::sanitize_array($hashform_styles, HashFormStyles::get_styles_sanitize_array());
259 update_post_meta($style_id, 'hashform_styles', $hashform_styles);
260 $styles['form_style_template'] = $style_id;
261 }
262
263 // An export can carry any status string; only these two leave the
264 // form reachable in the list.
265 $status = isset($imdat['status']) && in_array($imdat['status'], array('published', 'trash'), true) ? $imdat['status'] : 'published';
266
267 $form = array(
268 'options' => serialize($options),
269 'status' => $status,
270 'settings' => serialize($settings),
271 'styles' => serialize($styles),
272 'created_at' => current_time('mysql'),
273 );
274
275 if (!empty($imdat['created_at']) && strtotime($imdat['created_at'])) {
276 $form['created_at'] = gmdate('Y-m-d H:i:s', strtotime($imdat['created_at']));
277 }
278
279 $wpdb->update($wpdb->prefix . 'hashform_forms', $form, array('id' => $form_id));
280 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}hashform_fields WHERE form_id=%d", $form_id));
281
282 $map = array();
283
284 if (isset($imdat['field']) && is_array($imdat['field']) && !empty($imdat['field'])) {
285 foreach ($imdat['field'] as $field) {
286 $new_id = HashFormFields::create_row(array(
287 'name' => isset($field['name']) ? $field['name'] : '',
288 'description' => isset($field['description']) ? $field['description'] : '',
289 'type' => isset($field['type']) ? $field['type'] : 'text',
290 'default_value' => isset($field['default_value']) ? $field['default_value'] : '',
291 'options' => isset($field['options']) ? $field['options'] : '',
292 'field_order' => isset($field['field_order']) ? $field['field_order'] : '',
293 'form_id' => $form_id,
294 'required' => isset($field['required']) ? $field['required'] : false,
295 'field_options' => isset($field['field_options']) ? $field['field_options'] : array()
296 ));
297
298 if ($new_id && isset($field['id'])) {
299 $map[absint($field['id'])] = (int) $new_id;
300 }
301 }
302 }
303
304 /*
305 * The rules name fields by id, and the ids in the file belong to the
306 * form it was taken from. Rewritten now that this form's own fields
307 * exist, which is also why the settings are written twice: the form row
308 * has to exist before a field can point at it.
309 *
310 * A file exported before ids were carried has nothing to match on, so
311 * its rules cannot be salvaged - remap_conditions() drops them rather
312 * than leave the form carrying rules that can never fire.
313 */
314 // Calculation formulas name their inputs by field id too.
315 HashFormBuilder::remap_calculation_formulas($form_id, $map);
316
317 if (!empty($settings['condition_action'])) {
318 $settings = HashFormBuilder::remap_conditions($settings, $map, $dropped);
319
320 $wpdb->update(
321 $wpdb->prefix . 'hashform_forms',
322 array('settings' => serialize($settings)),
323 array('id' => $form_id)
324 );
325
326 if ($dropped) {
327 HashFormHelper::log(sprintf(
328 'importing into form %d: %d show/hide rule(s) could not be matched to a field and were dropped',
329 $form_id,
330 $dropped
331 ));
332 }
333 }
334 }
335
336 /**
337 * Plain POST entry point for the per-form Import/Export panel.
338 */
339 public function process_settings_import() {
340 /*
341 * These four run on admin_init, so they see every admin page load and
342 * must fall through quietly when the request is not theirs. Dying here
343 * would take out the whole of wp-admin for anyone without the
344 * capability.
345 */
346 if (!HashFormCapabilities::user_can('hashform_edit_forms')) {
347 return;
348 }
349
350 if (wp_doing_ajax()) {
351 return;
352 }
353
354 $form_id = HashFormHelper::get_post('hashform_form_id', 'absint');
355
356 if ('import_form' != HashFormHelper::get_post('hashform_imex_action') || !$form_id) {
357 return;
358 }
359
360 if (!wp_verify_nonce(HashFormHelper::get_post('hashform_imex_import_nonce'), 'hashform_imex_import_nonce')) {
361 return;
362 }
363
364 $upload = self::read_uploaded_export('hashform_import_file');
365
366 if (is_wp_error($upload)) {
367 wp_die(esc_html($upload->get_error_message()));
368 }
369
370 self::apply_export_to_form($form_id, $upload['data']);
371
372 HashFormHelper::set_message(esc_html__('Settings Imported Successfully', 'hash-form'));
373 }
374
375 public function process_style_import() {
376 /*
377 * These four run on admin_init, so they see every admin page load and
378 * must fall through quietly when the request is not theirs. Dying here
379 * would take out the whole of wp-admin for anyone without the
380 * capability.
381 */
382 if (!HashFormCapabilities::user_can('hashform_edit_forms')) {
383 return;
384 }
385
386 $style_id = HashFormHelper::get_post('hashform_style_id', 'absint');
387
388 if ('import_style' != HashFormHelper::get_post('hashform_imex_action') || !$style_id) {
389 return;
390 }
391
392 if (!wp_verify_nonce(HashFormHelper::get_post('hashform_imex_import_nonce'), 'hashform_imex_import_nonce')) {
393 return;
394 }
395
396 $filename = isset($_FILES['hashform_import_file']['name']) ? sanitize_text_field(wp_unslash($_FILES['hashform_import_file']['name'])) : '';
397 $extension = explode('.', $filename);
398 $extension = end($extension);
399
400 if ($extension != 'json') {
401 wp_die(esc_html__('Please upload a valid .json file', 'hash-form'));
402 }
403
404 $hashform_import_file = isset($_FILES['hashform_import_file']['tmp_name']) ? sanitize_text_field($_FILES['hashform_import_file']['tmp_name']) : '';
405
406 if (empty($hashform_import_file)) {
407 wp_die(esc_html__('Please upload a file to import', 'hash-form'));
408 }
409
410 // Retrieve the settings from the file and convert the json object to an array.
411 $imdat = json_decode(file_get_contents($hashform_import_file), true);
412
413 if (!is_array($imdat)) {
414 wp_die(esc_html__('Please upload a valid file to import', 'hash-form'));
415 }
416
417 $hashform_styles = HashFormHelper::recursive_parse_args($imdat, HashFormStyles::default_styles());
418 $hashform_styles = HashFormHelper::sanitize_array($hashform_styles, HashFormStyles::get_styles_sanitize_array());
419 update_post_meta($style_id, 'hashform_styles', $hashform_styles);
420
421 HashFormHelper::set_message(esc_html__('Form Style Imported Successfully', 'hash-form'));
422 }
423
424 }
425
426 new HashFormImportExport();
427