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

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

843 lines 31.7 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 /*
5 * meta_value here is a column in the plugin's own hashform_entry_meta table,
6 * not the WP_Query argument the sniff is looking for. There is no meta query
7 * in this file to be slow.
8 */
9 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_value
10
11 class HashFormEntry {
12
13 use HashFormListActions;
14
15 public function __construct() {
16 // Printed above #wpbody so the bar sits flush under the admin bar and
17 // clear of the Screen Options tab, as on the style templates list.
18 add_action('in_admin_header', array($this, 'list_header'));
19
20 // Notices are moved inside the screen wrapper; see buffer_notices().
21 add_action('admin_notices', array($this, 'buffer_notices'), -PHP_INT_MAX);
22 add_action('all_admin_notices', array($this, 'capture_notices'), PHP_INT_MAX);
23 add_action('admin_menu', array($this, 'add_menu'), 10);
24 add_filter('set-screen-option', array($this, 'set_screen_option'), 15, 3);
25
26 add_action('wp_ajax_hashform_process_entry', array($this, 'process_entry'));
27 add_action('wp_ajax_nopriv_hashform_process_entry', array($this, 'process_entry'));
28
29 add_action('wp_ajax_hashform_toggle_star', array($this, 'toggle_star'));
30 add_action('wp_ajax_hashform_save_entry_note', array($this, 'save_entry_note'));
31 add_action('wp_ajax_hashform_resend_notification', array($this, 'resend_notification'));
32 }
33
34 /* ===== Entry workflow ===== */
35
36 public static function set_flag($id, $column, $value) {
37 global $wpdb;
38
39 if (!in_array($column, array('is_read', 'is_starred'), true)) {
40 return false;
41 }
42
43 return $wpdb->update($wpdb->prefix . 'hashform_entries', array($column => (int) $value), array('id' => absint($id)));
44 }
45
46 public static function mark_read($id) {
47 return self::set_flag($id, 'is_read', 1);
48 }
49
50 public static function get_unread_count() {
51 global $wpdb;
52 return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}hashform_entries WHERE status='published' AND is_read = 0");
53 }
54
55 /**
56 * Star and unstar from the entries list without a page load.
57 */
58 public function toggle_star() {
59 HashFormCapabilities::require_cap_ajax('hashform_edit_entries');
60
61 check_ajax_referer('hashform_entry_action', 'nonce');
62
63 $id = HashFormHelper::get_post('entry_id', 'absint');
64 $starred = HashFormHelper::get_post('starred', 'absint') ? 1 : 0;
65
66 if (!$id || !self::entry_exists($id)) {
67 wp_send_json_error();
68 }
69
70 self::set_flag($id, 'is_starred', $starred);
71
72 wp_send_json_success(array('starred' => $starred));
73 }
74
75 /**
76 * A private note on an entry, for whoever picks it up next.
77 */
78 public function save_entry_note() {
79 HashFormCapabilities::require_cap_ajax('hashform_edit_entries');
80
81 check_ajax_referer('hashform_entry_action', 'nonce');
82
83 global $wpdb;
84 $id = HashFormHelper::get_post('entry_id', 'absint');
85 $note = HashFormHelper::get_post('note', 'sanitize_textarea_field');
86
87 if (!$id || !self::entry_exists($id)) {
88 wp_send_json_error();
89 }
90
91 $wpdb->update($wpdb->prefix . 'hashform_entries', array('notes' => $note), array('id' => $id));
92
93 wp_send_json_success(array('note' => $note));
94 }
95
96 /**
97 * Sends the notification emails for an entry again, for when the original
98 * bounced or the address was wrong at the time.
99 */
100 public function resend_notification() {
101 HashFormCapabilities::require_cap_ajax('hashform_edit_entries');
102
103 check_ajax_referer('hashform_entry_action', 'nonce');
104
105 global $wpdb;
106 $id = HashFormHelper::get_post('entry_id', 'absint');
107 $entry = self::get_entry_vars($id);
108
109 if (!$entry) {
110 wp_send_json_error(array('message' => esc_html__('That entry no longer exists.', 'hash-form')));
111 }
112
113 $form = HashFormBuilder::get_form_vars($entry->form_id);
114
115 if (!$form) {
116 wp_send_json_error(array('message' => esc_html__('The form for this entry no longer exists.', 'hash-form')));
117 }
118
119 // Resending must only send the mail. Without this the post submission
120 // actions would run again, which for a payment form means dispatching
121 // a second charge.
122 HashFormEmail::$sending_deferred = true;
123
124 $send_mail = new HashFormEmail($form, $id, '');
125 $sent = $send_mail->send_email();
126
127 HashFormEmail::$sending_deferred = false;
128
129 $wpdb->update($wpdb->prefix . 'hashform_entries', array('delivery_status' => $sent ? 1 : 0), array('id' => $id));
130
131 if (!$sent) {
132 wp_send_json_error(array('message' => esc_html__('The notification could not be sent. Check your email settings.', 'hash-form')));
133 }
134
135 wp_send_json_success(array('message' => esc_html__('Notification sent.', 'hash-form')));
136 }
137
138 private static function entry_exists($id) {
139 global $wpdb;
140 return (bool) $wpdb->get_var($wpdb->prepare("SELECT id FROM {$wpdb->prefix}hashform_entries WHERE id = %d", absint($id)));
141 }
142
143 public function add_menu() {
144 global $hash_entry_listing_page;
145 $hash_entry_listing_page = add_submenu_page('hashform', esc_html__('Entries', 'hash-form'), esc_html__('Entries', 'hash-form'), 'hashform_view_entries', 'hashform-entries', array($this, 'route'));
146 add_action("load-$hash_entry_listing_page", array($this, 'listing_page_screen_options'));
147 }
148
149 protected static function list_config() {
150 return array(
151 'page' => 'hashform-entries',
152 'table' => 'hashform_entries',
153 'id_key' => 'entry_id',
154 'nonce_item' => 'entry',
155 'bulk_nonce' => 'bulk-entries',
156 'caps' => array(
157 'delete' => 'hashform_delete_entries',
158 'edit' => 'hashform_edit_entries',
159 ),
160 'actions' => array('view', 'destroy', 'untrash', 'trash', 'delete_all'),
161 );
162 }
163
164 protected static function destroy_item($id) {
165 return self::destroy_entry($id);
166 }
167
168 protected static function render_list($message = '', $class = 'updated') {
169 self::display_entry_list($message, $class);
170 }
171
172 protected static function message_trashed($count, $undo_open, $undo_close) {
173 /* translators: 1: entry count singular & plural, 2: link open, 3: link close */
174 return sprintf(_n('%1$s entry moved to the Trash. %2$sUndo%3$s', '%1$s entries moved to the Trash. %2$sUndo%3$s', $count, 'hash-form'), $count, $undo_open, $undo_close);
175 }
176
177 protected static function message_untrashed($count) {
178 /* translators: 1: entry count singular & plural */
179 return sprintf(_n('%1$s entry restored from the Trash.', '%1$s entries restored from the Trash.', $count, 'hash-form'), $count);
180 }
181
182 protected static function message_destroyed($count) {
183 /* translators: 1: entry count singular & plural */
184 return sprintf(_n('%1$s Entry Permanently Deleted', '%1$s Entries Permanently Deleted', $count, 'hash-form'), $count);
185 }
186
187 protected static function message_deleted($count) {
188 /* translators: 1: entry count singular & plural */
189 return sprintf(_n('%1$s entry permanently deleted.', '%1$s entries permanently deleted.', $count, 'hash-form'), $count);
190 }
191
192 protected static function message_none_specified() {
193 return esc_html__('No Entries were specified', 'hash-form');
194 }
195
196 /**
197 * May the current user read this entry?
198 *
199 * The check lives here rather than only in the callers because this
200 * method renders an entry in full - every answer somebody submitted - and
201 * a caller that forgets to ask is one line away from publishing it.
202 *
203 * @param int $entry_id
204 * @return bool
205 */
206 public static function current_user_can_view($entry_id) {
207 /**
208 * Final say on whether an entry may be read.
209 *
210 * Add-ons that decide access some other way - by form, by ownership,
211 * by a membership plugin - hook this rather than replacing the check.
212 *
213 * @param bool $allowed
214 * @param int $entry_id
215 */
216 return (bool) apply_filters(
217 'hashform_user_can_view_entry',
218 HashFormCapabilities::user_can('hashform_view_entries'),
219 absint($entry_id)
220 );
221 }
222
223 public static function view($id = 0) {
224 if (!$id) {
225 $id = HashFormHelper::get_var('id', 'absint');
226 }
227
228 if (!self::current_user_can_view($id)) {
229 ?>
230 <div id="message" class="error notice is-dismissible">
231 <p><?php esc_html_e('You do not have permission to view this entry.', 'hash-form'); ?></p>
232 </div>
233 <?php
234 return;
235 }
236
237 $entry = self::get_entry_vars($id);
238
239 if (!$entry) {
240 ?>
241 <div id="message" class="error notice is-dismissible">
242 <p><?php esc_html_e('You are trying to view an entry that does not exist.', 'hash-form'); ?></p>
243 </div>
244 <?php
245 return;
246 }
247
248 // Opening an entry is what marks it read.
249 if (empty($entry->is_read)) {
250 self::mark_read($id);
251 $entry->is_read = 1;
252 }
253
254 include(HASHFORM_PATH . 'admin/entries/entry-detail.php');
255 }
256
257 /**
258 * The bar across the top of the Entries list. Same placement as the Forms
259 * and style template lists — see HashFormBuilder::list_header().
260 */
261 public function list_header() {
262 if (!self::is_list_view()) {
263 return;
264 }
265
266 HashFormHelper::render_list_header(array(
267 'title' => esc_html__('Entries', 'hash-form'),
268 'docs' => 'https://hashthemes.com/documentation/hash-form-drag-and-drop-form-builder-documentation/',
269 ));
270 }
271
272 public static function display_entry_list($message = '', $class = 'updated') {
273 ?>
274 <div class="hf-content hf-list-screen">
275
276 <?php // The header bar is printed on in_admin_header; see list_header(). ?>
277 <div class="hf-list-wrap wrap">
278 <h1></h1>
279
280 <?php self::print_notices(); ?>
281
282 <div id="hf-entry-list">
283 <?php
284 self::display_message($message, $class);
285 $entry_table = new HashFormEntryListing();
286 $entry_status = HashFormHelper::get_var('status', 'sanitize_title', 'published');
287 $entry_table->prepare_items();
288 ?>
289 <form id="posts-filter" method="get">
290 <input type="hidden" name="page" value="<?php echo esc_attr(HashFormHelper::get_var('page', 'sanitize_title')); ?>" />
291 <input type="hidden" name="status" value="<?php echo esc_attr($entry_status); ?>" />
292
293 <div class="hf-list-toolbar">
294 <?php
295 $entry_table->views();
296 $entry_table->search_box(esc_html__('Search', 'hash-form'), 'search');
297 ?>
298 </div>
299
300 <?php $entry_table->display(); ?>
301 </form>
302 </div>
303 </div>
304 </div>
305 <?php
306 }
307
308 public function listing_page_screen_options() {
309
310 global $hash_entry_listing_page;
311
312 $screen = get_current_screen();
313 $hashform_action = HashFormHelper::get_var('hashform_action');
314
315 // get out of here if we are not on our settings page
316 if (!is_object($screen) || $screen->id != $hash_entry_listing_page || ($hashform_action == 'view'))
317 return;
318
319 $args = array(
320 'label' => esc_html__('Entries per page', 'hash-form'),
321 'default' => 10,
322 'option' => 'entries_per_page'
323 );
324
325 add_screen_option('per_page', $args);
326
327 //new HashFormEntryListing();
328 }
329
330 public function set_screen_option($status, $option, $value) {
331 return ('entries_per_page' === $option) ? $value : $status;
332 }
333
334 public static function destroy_entry($id) {
335 global $wpdb;
336 $entry = self::get_entry_vars($id); // Item meta is required for conditional logic in actions with 'delete' events.
337 if (!$entry) {
338 return false;
339 }
340
341 /**
342 * An entry is about to be deleted.
343 *
344 * Fires while the entry and its meta can still be read, so an add-on
345 * can clear whatever it stored alongside the entry before the row it
346 * keys on disappears. Without this, anything an add-on wrote against
347 * an entry id outlives the entry with nothing left to identify it.
348 *
349 * @param int $id
350 * @param object $entry The entry, with its meta loaded.
351 */
352 do_action('hashform_before_destroy_entry', $id, $entry);
353
354 // Files first: once the meta rows are gone there is nothing left to
355 // say which uploads belonged to this entry, and they would sit in the
356 // uploads directory forever - still reachable by url, which for a
357 // deletion made on a privacy request is the opposite of what was
358 // asked for.
359 self::delete_entry_files($entry);
360
361 $wpdb->query($wpdb->prepare('DELETE FROM ' . $wpdb->prefix . 'hashform_entry_meta WHERE item_id=%d', $id));
362 $result = $wpdb->query($wpdb->prepare('DELETE FROM ' . $wpdb->prefix . 'hashform_entries WHERE id=%d', $id));
363 return $result;
364 }
365
366 /**
367 * Remove the files an entry's upload fields point at.
368 *
369 * Only paths that resolve inside the plugin's own upload directory are
370 * touched, so a value pointing anywhere else - a media library item
371 * shared with other content, or an absolute path from a tampered row -
372 * is left alone.
373 */
374 private static function delete_entry_files($entry) {
375 if (!$entry || empty($entry->metas) || !is_array($entry->metas)) {
376 return;
377 }
378
379 $upload_dir = wp_upload_dir();
380
381 if (!empty($upload_dir['error'])) {
382 return;
383 }
384
385 $base_dir = wp_normalize_path(trailingslashit($upload_dir['basedir'] . HASHFORM_UPLOAD_DIR));
386 $base_url = trailingslashit($upload_dir['baseurl'] . HASHFORM_UPLOAD_DIR);
387
388 foreach ($entry->metas as $meta) {
389 if (empty($meta['type']) || 'upload' !== $meta['type']) {
390 continue;
391 }
392
393 $value = HashFormHelper::unserialize_or_decode($meta['value']);
394 $urls = is_array($value) ? $value : explode(',', (string) $value);
395
396 foreach ($urls as $url) {
397 $url = trim((string) $url);
398
399 if ('' === $url || 0 !== strpos($url, $base_url)) {
400 continue;
401 }
402
403 $relative = ltrim(substr($url, strlen($base_url)), '/');
404
405 // A stored value is not a trusted path.
406 if ('' === $relative || false !== strpos($relative, '..')) {
407 continue;
408 }
409
410 wp_delete_file_from_directory($base_dir . $relative, $base_dir);
411 }
412 }
413 }
414
415 public static function get_entry_vars($id) {
416 global $wpdb;
417 $entry = $wpdb->get_row($wpdb->prepare("SELECT e.*, f.name AS form_name, f.form_key AS form_key
418 FROM {$wpdb->prefix}hashform_entries AS e
419 LEFT OUTER JOIN {$wpdb->prefix}hashform_forms AS f ON e.form_id = f.id
420 WHERE e.id = %d", $id));
421 $entry = self::get_meta($entry);
422 return $entry;
423 }
424
425 public static function get_meta($entry) {
426 if (!$entry) {
427 return $entry;
428 }
429
430 global $wpdb;
431 $metas = $wpdb->get_results($wpdb->prepare("SELECT m.*, f.type AS field_type, f.field_key, f.name, f.field_options FROM {$wpdb->prefix}hashform_entry_meta AS m LEFT JOIN {$wpdb->prefix}hashform_fields AS f ON m.field_id = f.id WHERE m.item_id = %d AND m.field_id != %d ORDER BY m.id ASC", $entry->id, 0));
432 $entry->metas = array();
433
434 foreach ($metas as $meta_val) {
435 $entry->metas[$meta_val->field_id] = array(
436 'name' => $meta_val->name,
437 'value' => $meta_val->meta_value,
438 'type' => $meta_val->field_type,
439 // Carried through so a value can be rendered the way its field
440 // was configured, rather than every display path re-querying.
441 'options' => maybe_unserialize($meta_val->field_options)
442 );
443 }
444
445 return $entry;
446 }
447
448 public function process_entry() {
449 global $wpdb;
450 parse_str(htmlspecialchars_decode(HashFormHelper::get_post('data', 'esc_html')), $data);
451 $location = esc_url(HashFormHelper::get_post('location', 'esc_html'));
452
453 /*
454 * Every exit from here answers in json. The failure paths used to
455 * `return` with nothing written, so the browser got a 200 with an
456 * empty body: the front end's success handler saw no recognised
457 * status, left the submit button spinning and told the visitor
458 * nothing at all.
459 */
460 if (empty($data) || empty($data['form_id']) || !isset($data['form_key'])) {
461 return self::submission_failed(esc_html__('There was a problem with your submission. Please reload the page and try again.', 'hash-form'));
462 }
463
464 do_action('hash_form_before_submit', $data);
465
466 $form_id = absint($data['form_id']);
467 $form = HashFormBuilder::get_form_vars($form_id);
468
469 if (!$form) {
470 return self::submission_failed(esc_html__('This form is no longer available.', 'hash-form'));
471 }
472
473 /*
474 * The submitted form_key must match the one stored for this form.
475 * Presence alone was checked before (isset), which let a request name
476 * one form's id with any key at all. A mismatch means the field was
477 * tampered with, so the submission is dropped.
478 */
479 if (!hash_equals((string) $form->form_key, (string) $data['form_key'])) {
480 return self::submission_failed(esc_html__('There was a problem with your submission. Please reload the page and try again.', 'hash-form'));
481 }
482
483 // Checked again here: the form may have closed, filled up or already
484 // been submitted since the page was loaded.
485 $restriction = HashFormRestrictions::check($form);
486
487 if (empty($restriction['allowed'])) {
488 return self::submission_failed(esc_html($restriction['message']));
489 }
490
491 // Cheap flood control before any of the expensive work below. A
492 // submission that trips it never reaches validation, the database or
493 // the mailer.
494 $throttle = self::check_rate_limit($form);
495
496 if ($throttle) {
497 return self::submission_failed($throttle);
498 }
499
500 $errors = HashFormValidate::validate(wp_unslash($data));
501
502 if (!empty($errors)) {
503 return wp_send_json(array(
504 'status' => 'error',
505 'message' => $errors
506 ));
507 }
508
509 $form_settings = $form->settings;
510 $entry_id = self::create($data);
511
512 /*
513 * A failed insert used to be handed to the mailer regardless, which
514 * then read ->metas on the null entry it got back and died with a
515 * fatal inside the ajax handler - a 500 and a blank response for the
516 * visitor.
517 */
518 if (!$entry_id) {
519 return self::submission_failed(self::error_message($form, $form_settings));
520 }
521
522 self::record_submission($form);
523
524 $send_mail = new HashFormEmail($form, $entry_id, $location);
525 $check_mail = $send_mail->send_email();
526
527 // send_email() answers the request itself on success, so reaching
528 // here means the mail was refused.
529 if (!$check_mail) {
530 $wpdb->update($wpdb->prefix . 'hashform_entries', array('delivery_status' => 0), array('id' => $entry_id));
531 return self::submission_failed(self::error_message($form, $form_settings));
532 }
533
534 // Nothing left to say: send_email() has already answered.
535 return wp_send_json(array(
536 'status' => 'success',
537 'message' => ''
538 ));
539 }
540
541 /**
542 * The form's own "something went wrong" wording, falling back to a
543 * generic line when the setting was never filled in.
544 */
545 private static function error_message($form, $form_settings) {
546 $message = isset($form_settings['error_message']) ? $form_settings['error_message'] : '';
547
548 if ('' === trim((string) $message)) {
549 return esc_html__('Your submission could not be saved. Please try again.', 'hash-form');
550 }
551
552 return esc_html(apply_filters('hashform_translate_string', $message, 'Hash Form', $form->name . ' - ' . 'Error Message'));
553 }
554
555 /**
556 * One shape for every refusal, so the front end always has something to
557 * show the visitor.
558 */
559 private static function submission_failed($message) {
560 return wp_send_json(array(
561 'status' => 'failed',
562 'message' => $message
563 ));
564 }
565
566 /**
567 * Flood control for public submissions.
568 *
569 * Keyed on form and submitter so one abusive source cannot lock a form
570 * for everyone. Anyone who can edit forms is exempt, and the whole thing
571 * stays off until a site sets a limit through the filter, so existing
572 * installs behave exactly as before unless they opt in.
573 *
574 * @return string Empty when the submission may proceed, otherwise the
575 * message to show.
576 */
577 private static function check_rate_limit($form) {
578 $limit = (int) apply_filters('hashform_submission_rate_limit', 0, $form);
579 $window = (int) apply_filters('hashform_submission_rate_window', MINUTE_IN_SECONDS, $form);
580
581 if ($limit < 1 || $window < 1 || HashFormCapabilities::user_can('hashform_edit_forms')) {
582 return '';
583 }
584
585 $key = self::rate_limit_key($form);
586
587 if (!$key) {
588 return '';
589 }
590
591 if ((int) get_transient($key) >= $limit) {
592 return esc_html__('You are sending submissions too quickly. Please wait a moment and try again.', 'hash-form');
593 }
594
595 return '';
596 }
597
598 /**
599 * Counts one accepted submission against the flood-control window.
600 */
601 private static function record_submission($form) {
602 $limit = (int) apply_filters('hashform_submission_rate_limit', 0, $form);
603 $window = (int) apply_filters('hashform_submission_rate_window', MINUTE_IN_SECONDS, $form);
604
605 if ($limit < 1 || $window < 1 || HashFormCapabilities::user_can('hashform_edit_forms')) {
606 return;
607 }
608
609 $key = self::rate_limit_key($form);
610
611 if (!$key) {
612 return;
613 }
614
615 set_transient($key, ((int) get_transient($key)) + 1, $window);
616 }
617
618 private static function rate_limit_key($form) {
619 $user_id = get_current_user_id();
620
621 if ($user_id) {
622 $who = 'u' . $user_id;
623 } else {
624 $ip = HashFormHelper::get_ip();
625
626 // Without either an account or a usable address there is nothing
627 // stable to count against, so the limit simply does not apply.
628 if (!$ip) {
629 return '';
630 }
631
632 $who = 'i' . $ip;
633 }
634
635 // Hashed to keep the key short and free of characters a transient
636 // name does not allow.
637 return 'hf_rl_' . md5($form->id . '|' . $who);
638 }
639
640 public static function create($values) {
641 global $wpdb;
642 $current_user_id = get_current_user_id();
643 $user_id = $current_user_id ? $current_user_id : 0;
644 $new_values = array(
645 'ip' => sanitize_text_field(HashFormHelper::get_ip()),
646 'delivery_status' => 1,
647 'form_id' => isset($values['form_id']) ? absint($values['form_id']) : '',
648 'created_at' => sanitize_text_field(current_time('mysql')),
649 'user_id' => absint($user_id),
650 'status' => 'published'
651 );
652
653 $query_results = $wpdb->insert($wpdb->prefix . 'hashform_entries', $new_values);
654 if (!$query_results) {
655 return false;
656 } else {
657 $entry_id = $wpdb->insert_id;
658 }
659
660 if (isset($values['item_meta']) && is_array($values['item_meta'])) {
661 foreach ($values['item_meta'] as $field_id => $meta_value) {
662 /*
663 * Only a genuinely unanswered field is skipped. This used to
664 * be !empty(), which also threw away every answer PHP treats as
665 * falsy: a number field holding 0, a select whose value is
666 * "0", a calculation that came out to zero. Those submissions
667 * were accepted and the answer silently never reached the
668 * database, so the entry showed a gap where the visitor had
669 * typed a valid number.
670 */
671 if (!self::is_blank_meta_value($meta_value)) {
672 if (!is_array($meta_value)) {
673 $meta_value = sanitize_textarea_field($meta_value);
674
675 /*
676 * A scalar answer must never be a PHP-serialized
677 * string. sanitize_*_field() leaves such a string
678 * intact, so without this it would reach the database
679 * and be unserialized on the Entries screen. Multi
680 * value fields (arrays) are serialized by us further
681 * down and are unaffected. The value is stored inert
682 * rather than instantiated later.
683 */
684 if (is_serialized($meta_value)) {
685 $meta_value = '';
686 }
687 }
688
689 $meta_values = array(
690 'meta_value' => $meta_value,
691 'item_id' => absint($entry_id),
692 'field_id' => absint($field_id),
693 'created_at' => sanitize_text_field(current_time('mysql')),
694 );
695
696 /*
697 * The field gets the value in the shape it was posted. It
698 * used to be serialized first, so a field that posts more
699 * than one value — a repeater's rows, an address, a set of
700 * boxes — only ever saw a string and could not put its own
701 * shape on what got stored.
702 */
703 self::sanitize_meta_value($meta_values);
704
705 if (is_array($meta_values['meta_value'])) {
706 $meta_values['meta_value'] = serialize($meta_values['meta_value']);
707 }
708
709 $query_results = $wpdb->insert($wpdb->prefix . 'hashform_entry_meta', $meta_values);
710 }
711 }
712 }
713 return $entry_id;
714 }
715
716 /**
717 * Whether a submitted answer counts as nothing entered.
718 *
719 * Mirrors HashFormValidate::is_blank_value(): the two must agree, or a
720 * field could pass required validation and then not be stored. "0" is a
721 * real answer and is never blank.
722 *
723 * @param mixed $value
724 * @return bool
725 */
726 private static function is_blank_meta_value($value) {
727 if (is_array($value)) {
728 foreach ($value as $part) {
729 if (!self::is_blank_meta_value($part)) {
730 return false;
731 }
732 }
733
734 return true;
735 }
736
737 if (is_null($value)) {
738 return true;
739 }
740
741 return '' === trim((string) $value);
742 }
743
744 private static function sanitize_meta_value(&$values) {
745 $field = HashFormFields::get_field_vars($values['field_id']);
746 if ($field) {
747 $field_obj = HashFormFields::get_field_object($field);
748 $values['meta_value'] = $field_obj->set_value_before_save($values['meta_value']);
749 $values['meta_value'] = $field_obj->sanitize_value($values['meta_value']);
750 }
751 }
752
753 public static function get_count() {
754 global $wpdb;
755 $results = $wpdb->get_results("SELECT status, COUNT(*) AS count FROM {$wpdb->prefix}hashform_entries GROUP BY status");
756 $counts = array('published' => 0, 'trash' => 0, 'unread' => 0, 'starred' => 0);
757 foreach ($results as $row) {
758 if ('published' == $row->status) {
759 $counts['published'] += $row->count;
760 } else {
761 $counts['trash'] += $row->count;
762 }
763 }
764
765 $counts['unread'] = self::get_unread_count();
766 $counts['starred'] = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}hashform_entries WHERE status='published' AND is_starred = 1");
767
768 return $counts;
769 }
770
771 public static function get_entry_count($form_id) {
772 global $wpdb;
773 $count = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}hashform_entries WHERE form_id=%d AND status='published'", $form_id));
774 return $count;
775 }
776
777 // Published-entry counts for every form in one query, keyed by form id.
778 public static function get_entry_counts() {
779 global $wpdb;
780 $results = $wpdb->get_results("SELECT form_id, COUNT(*) AS count FROM {$wpdb->prefix}hashform_entries WHERE status='published' GROUP BY form_id");
781 $counts = array();
782 foreach ($results as $row) {
783 $counts[$row->form_id] = (int) $row->count;
784 }
785 return $counts;
786 }
787
788 /**
789 * The entry before this one, within the same form.
790 *
791 * Both of these used to walk the whole table, so "previous" from an entry
792 * on one form landed on somebody else's form: the reader was stepping
793 * through every submission on the site rather than the list they came
794 * from. A trashed neighbour is skipped either way.
795 *
796 * @param int $entry_id
797 * @param int $form_id Optional. Looked up from the entry when omitted.
798 */
799 public static function get_prev_entry($entry_id, $form_id = 0) {
800 return self::get_adjacent_entry($entry_id, $form_id, 'prev');
801 }
802
803 public static function get_next_entry($entry_id, $form_id = 0) {
804 return self::get_adjacent_entry($entry_id, $form_id, 'next');
805 }
806
807 private static function get_adjacent_entry($entry_id, $form_id, $direction) {
808 global $wpdb;
809
810 $entry_id = absint($entry_id);
811 $form_id = absint($form_id);
812
813 if (!$form_id) {
814 $form_id = (int) $wpdb->get_var($wpdb->prepare("SELECT form_id FROM {$wpdb->prefix}hashform_entries WHERE id = %d", $entry_id));
815 }
816
817 if (!$form_id) {
818 return array();
819 }
820
821 /*
822 * The two queries are written out rather than assembled, because
823 * $wpdb->prepare() has to be handed a literal to be checkable: a
824 * query built in a variable cannot be verified by anything - not the
825 * sniffs, not a reader - as holding only placeholders.
826 */
827 if ('prev' === $direction) {
828 return $wpdb->get_results($wpdb->prepare("SELECT id FROM {$wpdb->prefix}hashform_entries WHERE id < %d AND form_id = %d AND status = 'published' ORDER BY id DESC LIMIT 1", $entry_id, $form_id));
829 }
830
831 return $wpdb->get_results($wpdb->prepare("SELECT id FROM {$wpdb->prefix}hashform_entries WHERE id > %d AND form_id = %d AND status = 'published' ORDER BY id ASC LIMIT 1", $entry_id, $form_id));
832 }
833
834 public static function get_entry_date($entry_id) {
835 global $wpdb;
836 $results = $wpdb->get_var($wpdb->prepare("SELECT created_at FROM {$wpdb->prefix}hashform_entries WHERE id = %d", $entry_id));
837 return HashFormHelper::convert_date_format($results);
838 }
839
840 }
841
842 new HashFormEntry();
843