PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.83
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / widgets / Form_Builder / helpers / Partial_Entries.php

Partial_Entries.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.83, at includes/widgets/Form_Builder/helpers/Partial_Entries.php

284 lines 8.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Partial entries for Form Builder.
4 *
5 * Saves what a visitor has typed before they submit, so an abandoned form still
6 * leaves something behind. Entries land in the same list as completed
7 * submissions, as drafts.
8 *
9 * @package King_Addons
10 */
11
12 namespace King_Addons;
13
14 if (!defined('ABSPATH')) {
15 exit;
16 }
17
18 /**
19 * Stores and clears in-progress submissions.
20 */
21 class Form_Partial_Entries
22 {
23 /**
24 * Post type shared with completed submissions.
25 */
26 public const POST_TYPE = 'king-addons-fb-sub';
27
28 /**
29 * Meta flag marking a row as still in progress.
30 */
31 public const META_PARTIAL = 'king_addons_partial';
32
33 /**
34 * Meta holding the browser's key for one visit.
35 */
36 public const META_KEY = 'king_addons_partial_key';
37
38 /**
39 * Option prefix for the per-form switch.
40 */
41 private const OPTION_PREFIX = 'king_addons_partial_entries_';
42
43 /**
44 * Cron hook that clears old drafts.
45 */
46 private const CRON_HOOK = 'king_addons_form_partial_cleanup';
47
48 /**
49 * How long an unfinished entry is kept, in days.
50 */
51 private const RETENTION_DAYS = 30;
52
53 /**
54 * Register the endpoint and the cleanup schedule.
55 */
56 public function __construct()
57 {
58 add_action('wp_ajax_king_addons_form_builder_partial', [self::class, 'handle']);
59 add_action('wp_ajax_nopriv_king_addons_form_builder_partial', [self::class, 'handle']);
60
61 add_action(self::CRON_HOOK, [self::class, 'cleanup']);
62
63 if (!wp_next_scheduled(self::CRON_HOOK)) {
64 wp_schedule_event(time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK);
65 }
66 }
67
68 /**
69 * Remember whether a form saves partial entries.
70 *
71 * @param string $form_id Form element id.
72 * @param bool $enabled Whether it is switched on.
73 *
74 * @return void
75 */
76 public static function save_setting(string $form_id, bool $enabled): void
77 {
78 update_option(self::OPTION_PREFIX . $form_id, $enabled ? '1' : '0', false);
79 }
80
81 /**
82 * Whether a form saves partial entries.
83 *
84 * @param string $form_id Form element id.
85 *
86 * @return bool
87 */
88 public static function is_enabled(string $form_id): bool
89 {
90 return '1' === (string) get_option(self::OPTION_PREFIX . $form_id, '0');
91 }
92
93 /**
94 * Store, update or clear one in-progress entry.
95 *
96 * @return void
97 */
98 public static function handle(): void
99 {
100 $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
101 if (!wp_verify_nonce($nonce, 'king-addons-js')) {
102 wp_send_json_error(['message' => 'invalid-nonce']);
103 }
104
105 $form_id = isset($_POST['king_addons_form_id'])
106 ? sanitize_text_field(wp_unslash($_POST['king_addons_form_id']))
107 : '';
108
109 if ('' === $form_id || !self::is_enabled($form_id)) {
110 wp_send_json_error(['message' => 'not-enabled']);
111 }
112
113 $page_id = isset($_POST['form_page_id']) ? absint(wp_unslash($_POST['form_page_id'])) : 0;
114 if (!Form_Builder_Security::is_valid_submission_page($page_id)) {
115 wp_send_json_error(['message' => 'bad-page']);
116 }
117
118 $key = isset($_POST['entry_key']) ? sanitize_key(wp_unslash($_POST['entry_key'])) : '';
119 if ('' === $key || strlen($key) < 8 || strlen($key) > 64) {
120 wp_send_json_error(['message' => 'bad-key']);
121 }
122
123 $existing = self::find($form_id, $key);
124
125 // The visitor got to the end: the completed submission is the record now.
126 if (!empty($_POST['finalise'])) {
127 if ($existing) {
128 wp_delete_post($existing, true);
129 }
130
131 wp_send_json_success(['message' => 'cleared']);
132 }
133
134 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- checked above.
135 $raw = isset($_POST['form_content']) && is_array($_POST['form_content']) ? wp_unslash($_POST['form_content']) : [];
136 if (empty($raw)) {
137 wp_send_json_error(['message' => 'empty']);
138 }
139
140 $post_id = $existing;
141
142 if (!$post_id) {
143 // One row per visit; a burst of new keys from one address would
144 // otherwise be an easy way to fill the table.
145 if (self::count_recent() > 200) {
146 wp_send_json_error(['message' => 'too-many']);
147 }
148
149 $post_id = wp_insert_post([
150 'post_type' => self::POST_TYPE,
151 'post_status' => 'draft',
152 'post_title' => sprintf(
153 /* translators: %s: date and time. */
154 esc_html__('In progress - %s', 'king-addons'),
155 current_time('mysql')
156 ),
157 ]);
158
159 if (is_wp_error($post_id) || !$post_id) {
160 wp_send_json_error(['message' => 'insert-failed']);
161 }
162
163 update_post_meta($post_id, self::META_PARTIAL, '1');
164 update_post_meta($post_id, self::META_KEY, $key);
165 update_post_meta($post_id, 'king_addons_form_id', $form_id);
166 update_post_meta($post_id, 'king_addons_form_page_id', $page_id);
167 update_post_meta($post_id, 'king_addons_user_ip', class_exists('King_Addons\\Core') ? Core::getClientIP() : '');
168 }
169
170 self::store_fields((int) $post_id, $raw);
171
172 wp_send_json_success(['message' => 'saved', 'entry' => (int) $post_id]);
173 }
174
175 /**
176 * Write the submitted values onto the draft.
177 *
178 * @param int $post_id Draft id.
179 * @param array<mixed> $raw Form content from the browser.
180 *
181 * @return void
182 */
183 private static function store_fields(int $post_id, array $raw): void
184 {
185 $stored = [];
186
187 foreach ($raw as $key => $value) {
188 if (!is_array($value) || count($value) < 2) {
189 continue;
190 }
191
192 $meta_key = sanitize_key((string) $key);
193 if ('' === $meta_key) {
194 continue;
195 }
196
197 $stored[] = $meta_key;
198
199 update_post_meta($post_id, $meta_key, [
200 sanitize_text_field((string) $value[0]),
201 is_array($value[1]) ? array_map('sanitize_text_field', $value[1]) : sanitize_text_field((string) $value[1]),
202 isset($value[2]) ? sanitize_text_field((string) $value[2]) : '',
203 ]);
204 }
205
206 // A field the visitor cleared, or one hidden by conditional logic, must
207 // not linger from an earlier save.
208 $previous = (array) get_post_meta($post_id, 'king_addons_partial_fields', true);
209 foreach (array_diff($previous, $stored) as $gone) {
210 delete_post_meta($post_id, (string) $gone);
211 }
212
213 update_post_meta($post_id, 'king_addons_partial_fields', $stored);
214 update_post_meta($post_id, 'king_addons_partial_updated', current_time('mysql'));
215 }
216
217 /**
218 * The draft belonging to one visit, if there is one.
219 *
220 * @param string $form_id Form element id.
221 * @param string $key Entry key.
222 *
223 * @return int Post id, or 0.
224 */
225 private static function find(string $form_id, string $key): int
226 {
227 $found = get_posts([
228 'post_type' => self::POST_TYPE,
229 'post_status' => 'draft',
230 'posts_per_page' => 1,
231 'fields' => 'ids',
232 'no_found_rows' => true,
233 'meta_query' => [
234 ['key' => self::META_KEY, 'value' => $key],
235 ['key' => 'king_addons_form_id', 'value' => $form_id],
236 ],
237 ]);
238
239 return $found ? (int) $found[0] : 0;
240 }
241
242 /**
243 * How many drafts were started in the last hour.
244 *
245 * @return int
246 */
247 private static function count_recent(): int
248 {
249 $recent = get_posts([
250 'post_type' => self::POST_TYPE,
251 'post_status' => 'draft',
252 'posts_per_page' => 201,
253 'fields' => 'ids',
254 'no_found_rows' => true,
255 'date_query' => [['after' => '1 hour ago']],
256 'meta_query' => [['key' => self::META_PARTIAL, 'value' => '1']],
257 ]);
258
259 return count($recent);
260 }
261
262 /**
263 * Drop drafts nobody came back to finish.
264 *
265 * @return void
266 */
267 public static function cleanup(): void
268 {
269 $old = get_posts([
270 'post_type' => self::POST_TYPE,
271 'post_status' => 'draft',
272 'posts_per_page' => 200,
273 'fields' => 'ids',
274 'no_found_rows' => true,
275 'date_query' => [['before' => self::RETENTION_DAYS . ' days ago']],
276 'meta_query' => [['key' => self::META_PARTIAL, 'value' => '1']],
277 ]);
278
279 foreach ($old as $post_id) {
280 wp_delete_post((int) $post_id, true);
281 }
282 }
283 }
284