PluginProbe ʕ •ᴥ•ʔ
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More / 4.0.0
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More v4.0.0
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 trunk 1.0.0 2.0.0 2.0.1 2.0.2 2.0.3 3.0 3.0.1 3.0.2 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.2.0 3.2.1 3.2.2 3.2.4 3.2.5 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.4.5 3.4.6 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.6 3.5.7 3.5.8 3.5.9 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1
superb-blocks / src / gutenberg / form / class-form-registry.php
superb-blocks / src / gutenberg / form Last commit date
class-form-access-control.php 2 months ago class-form-captcha-handler.php 2 months ago class-form-controller.php 2 months ago class-form-email-config-check.php 2 months ago class-form-email-handler.php 2 months ago class-form-encryption.php 2 months ago class-form-exporter.php 2 months ago class-form-field-validator.php 2 months ago class-form-file-handler.php 2 months ago class-form-google-auth.php 2 months ago class-form-integration-handler.php 2 months ago class-form-math-parser.php 2 months ago class-form-permissions.php 2 months ago class-form-registry.php 2 months ago class-form-settings.php 2 months ago class-form-submission-cpt.php 2 months ago class-form-submission-handler.php 2 months ago
class-form-registry.php
526 lines
1 <?php
2
3 namespace SuperbAddons\Gutenberg\Form;
4
5 defined('ABSPATH') || exit();
6
7 class FormRegistry
8 {
9 const OPTION_KEY = 'spb_form_registry';
10 const CONFIG_PREFIX = 'spb_form_cfg_';
11 const BLOCK_NAME = 'superb-addons/form';
12 const MULTISTEP_BLOCK_NAME = 'superb-addons/multistep-form';
13 const STEP_BLOCK_NAME = 'superb-addons/form-step';
14 const FIELD_BLOCK_NAME = 'superb-addons/form-field';
15
16 private static $supported_post_types = array('post', 'page', 'wp_template', 'wp_template_part', 'wp_block');
17
18 /**
19 * Get the list of post types that can contain form blocks.
20 */
21 public static function GetSupportedPostTypes()
22 {
23 return self::$supported_post_types;
24 }
25
26 public static function Initialize()
27 {
28 add_action('save_post', array(__CLASS__, 'OnSavePost'), 10, 2);
29 add_action('before_delete_post', array(__CLASS__, 'OnDeletePost'), 10, 2);
30 }
31
32 /**
33 * Hook: save_post — scan content for form blocks and update registry + configs.
34 */
35 public static function OnSavePost($post_id, $post)
36 {
37 // Bail on autosave, revisions, or unsupported post types
38 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
39 return;
40 }
41 if (wp_is_post_revision($post_id)) {
42 return;
43 }
44 if (!in_array($post->post_type, self::$supported_post_types, true)) {
45 return;
46 }
47
48 $registry = self::GetAll();
49 $changed = false;
50
51 // Find all form blocks in the saved content
52 $found_form_ids = array();
53 $parsed_blocks = parse_blocks($post->post_content);
54
55 foreach ($parsed_blocks as $block) {
56 self::CollectFormBlocks($block, $found_form_ids, $post_id, $post->post_type, $registry, $changed);
57 }
58
59 // Cleanup: remove forms that were previously on this post but are no longer
60 $forms_with_submissions = null; // lazy-loaded
61 foreach ($registry as $fid => $data) {
62 if (isset($data['source_post_id']) && intval($data['source_post_id']) === intval($post_id) && !in_array($fid, $found_form_ids, true)) {
63 // Form was on this post but no longer — check for submissions before removing
64 if ($forms_with_submissions === null) {
65 $forms_with_submissions = FormSubmissionHandler::GetDistinctFormIds();
66 }
67 if (in_array($fid, $forms_with_submissions, true)) {
68 // Has submissions: flag for deletion once submissions are cleared
69 $registry[$fid]['source_post_id'] = null;
70 $registry[$fid]['source_post_type'] = null;
71 $registry[$fid]['pending_delete'] = true;
72 $registry[$fid]['updated'] = time();
73 } else {
74 unset($registry[$fid]);
75 }
76 $changed = true;
77 // Delete the stored config for removed forms
78 delete_option(self::CONFIG_PREFIX . sanitize_key($fid));
79 }
80 }
81
82 if ($changed) {
83 update_option(self::OPTION_KEY, $registry, false);
84 }
85 }
86
87 /**
88 * Recursively collect form blocks, update registry entries, and store configs.
89 */
90 private static function CollectFormBlocks($block, &$found_form_ids, $post_id, $post_type, &$registry, &$changed)
91 {
92 $is_form = isset($block['blockName']) && $block['blockName'] === self::BLOCK_NAME && !empty($block['attrs']['formId']);
93 $is_multistep = isset($block['blockName']) && $block['blockName'] === self::MULTISTEP_BLOCK_NAME && !empty($block['attrs']['formId']);
94
95 if ($is_form || $is_multistep) {
96 $form_id = sanitize_key($block['attrs']['formId']);
97 $form_name = isset($block['attrs']['formName']) ? sanitize_text_field($block['attrs']['formName']) : '';
98 $found_form_ids[] = $form_id;
99
100 $entry = isset($registry[$form_id]) ? $registry[$form_id] : null;
101 if (
102 !$entry
103 || $entry['name'] !== $form_name
104 || $entry['source_post_id'] !== $post_id
105 || $entry['source_post_type'] !== $post_type
106 || !empty($entry['pending_delete'])
107 ) {
108 $registry[$form_id] = array(
109 'name' => $form_name,
110 'source_post_id' => $post_id,
111 'source_post_type' => $post_type,
112 'updated' => time(),
113 );
114 $changed = true;
115 }
116
117 // Store form config (attributes + inner field blocks)
118 $attrs = isset($block['attrs']) ? $block['attrs'] : array();
119 $form_fields = self::ExtractFieldBlocks($block, $is_multistep);
120 $attrs['formFields'] = $form_fields;
121
122 // Extract webhook secret to encrypted storage and remove from config
123 if (isset($attrs['webhookSecret']) && $attrs['webhookSecret'] !== '') {
124 FormSettings::SetWebhookSecret($form_id, $attrs['webhookSecret']);
125 }
126 unset($attrs['webhookSecret']);
127
128 update_option(self::CONFIG_PREFIX . $form_id, $attrs, false);
129 }
130
131 // Recurse into inner blocks
132 if (!empty($block['innerBlocks'])) {
133 foreach ($block['innerBlocks'] as $inner) {
134 self::CollectFormBlocks($inner, $found_form_ids, $post_id, $post_type, $registry, $changed);
135 }
136 }
137 }
138
139 /**
140 * Extract form-field blocks from a form or multistep-form block.
141 * For multistep forms, recurses through form-step inner blocks.
142 */
143 private static function ExtractFieldBlocks($block, $is_multistep = false)
144 {
145 $form_fields = array();
146 if (empty($block['innerBlocks'])) {
147 return $form_fields;
148 }
149
150 foreach ($block['innerBlocks'] as $inner) {
151 if ($is_multistep && isset($inner['blockName']) && $inner['blockName'] === self::STEP_BLOCK_NAME) {
152 // Recurse into form-step to find form-field blocks
153 if (!empty($inner['innerBlocks'])) {
154 foreach ($inner['innerBlocks'] as $step_inner) {
155 if (
156 isset($step_inner['blockName']) && $step_inner['blockName'] === self::FIELD_BLOCK_NAME
157 && !empty($step_inner['attrs']['fieldId'])
158 ) {
159 $form_fields[] = $step_inner['attrs'];
160 }
161 }
162 }
163 } elseif (
164 isset($inner['blockName']) && $inner['blockName'] === self::FIELD_BLOCK_NAME
165 && !empty($inner['attrs']['fieldId'])
166 ) {
167 $form_fields[] = $inner['attrs'];
168 }
169 }
170
171 return $form_fields;
172 }
173
174 /**
175 * Get the full registry array.
176 */
177 public static function GetAll()
178 {
179 $registry = get_option(self::OPTION_KEY, array());
180 return is_array($registry) ? $registry : array();
181 }
182
183 /**
184 * Get a single registry entry.
185 */
186 public static function Get($form_id)
187 {
188 $registry = self::GetAll();
189 return isset($registry[$form_id]) ? $registry[$form_id] : null;
190 }
191
192 /**
193 * Check if a form is flagged as pending deletion.
194 */
195 public static function IsPendingDelete($form_id)
196 {
197 $entry = self::Get($form_id);
198 return $entry && !empty($entry['pending_delete']);
199 }
200
201 /**
202 * Get display name for a form, with fallback.
203 */
204 public static function GetName($form_id)
205 {
206 $entry = self::Get($form_id);
207 if ($entry && !empty($entry['name'])) {
208 return $entry['name'];
209 }
210 /* translators: %s: form ID */
211 return sprintf(__('Unnamed Form (%s)', 'superb-blocks'), $form_id);
212 }
213
214 /**
215 * Remove a form from the registry.
216 */
217 public static function Remove($form_id)
218 {
219 $registry = self::GetAll();
220 if (isset($registry[$form_id])) {
221 unset($registry[$form_id]);
222 update_option(self::OPTION_KEY, $registry, false);
223 }
224 }
225
226 /**
227 * Remove a form block from its source post content.
228 * Returns true if the block was found and removed, false otherwise.
229 */
230 public static function RemoveFormBlock($form_id)
231 {
232 $entry = self::Get($form_id);
233 if (!$entry || empty($entry['source_post_id'])) {
234 return false;
235 }
236
237 $post = get_post($entry['source_post_id']);
238 if (!$post || empty($post->post_content)) {
239 return false;
240 }
241
242 $blocks = parse_blocks($post->post_content);
243 $filtered = self::FilterOutFormBlock($form_id, $blocks);
244
245 if ($filtered === null) {
246 return false;
247 }
248
249 $new_content = serialize_blocks($filtered);
250
251 // Use wp_update_post to trigger save_post hooks (which will update the registry)
252 return wp_update_post(array(
253 'ID' => $post->ID,
254 'post_content' => $new_content,
255 )) !== 0;
256 }
257
258 /**
259 * Recursively filter out a form block with a specific formId.
260 * Returns the filtered blocks array, or null if the block was not found.
261 */
262 private static function FilterOutFormBlock($form_id, $blocks)
263 {
264 $found = false;
265 $result = array();
266
267 foreach ($blocks as $block) {
268 if (
269 isset($block['blockName']) && ($block['blockName'] === self::BLOCK_NAME || $block['blockName'] === self::MULTISTEP_BLOCK_NAME)
270 && isset($block['attrs']['formId']) && sanitize_key($block['attrs']['formId']) === $form_id
271 ) {
272 $found = true;
273 continue;
274 }
275
276 // Recurse into inner blocks
277 if (!empty($block['innerBlocks'])) {
278 $inner_result = self::FilterOutFormBlock($form_id, $block['innerBlocks']);
279 if ($inner_result !== null) {
280 $found = true;
281 $block['innerBlocks'] = $inner_result;
282 }
283 }
284
285 $result[] = $block;
286 }
287
288 return $found ? $result : null;
289 }
290
291 /**
292 * Hook: before_delete_post — handles permanent post deletion (trash emptying).
293 * save_post does NOT fire when a trashed post is permanently deleted.
294 */
295 public static function OnDeletePost($post_id, $post)
296 {
297 if (!in_array($post->post_type, self::$supported_post_types, true)) {
298 return;
299 }
300
301 $registry = self::GetAll();
302 $changed = false;
303 $forms_with_submissions = null;
304
305 foreach ($registry as $fid => $data) {
306 if (isset($data['source_post_id']) && intval($data['source_post_id']) === intval($post_id)) {
307 if ($forms_with_submissions === null) {
308 $forms_with_submissions = FormSubmissionHandler::GetDistinctFormIds();
309 }
310 if (in_array($fid, $forms_with_submissions, true)) {
311 $registry[$fid]['source_post_id'] = null;
312 $registry[$fid]['source_post_type'] = null;
313 $registry[$fid]['pending_delete'] = true;
314 $registry[$fid]['updated'] = time();
315 } else {
316 unset($registry[$fid]);
317 delete_option(self::CONFIG_PREFIX . sanitize_key($fid));
318 }
319 $changed = true;
320 }
321 }
322
323 if ($changed) {
324 update_option(self::OPTION_KEY, $registry, false);
325 }
326 }
327
328 /**
329 * Clean up registry entries flagged for deletion once their submissions are gone.
330 * Call this after deleting submissions for a specific form.
331 */
332 public static function CleanupAfterSubmissionDelete($form_id)
333 {
334 $registry = self::GetAll();
335 $entry = isset($registry[$form_id]) ? $registry[$form_id] : null;
336
337 if (!$entry || empty($entry['pending_delete'])) {
338 return;
339 }
340
341 // Check if this form still has any submissions
342 $count = FormSubmissionHandler::GetCount($form_id);
343 if (intval($count['total']) === 0) {
344 unset($registry[$form_id]);
345 update_option(self::OPTION_KEY, $registry, false);
346 delete_option(self::CONFIG_PREFIX . sanitize_key($form_id));
347 }
348 }
349
350 /**
351 * Get stored form config, rebuilding from source post if missing.
352 * Returns the config array or null if form cannot be found.
353 */
354 public static function GetConfig($form_id)
355 {
356 $config = get_option(self::CONFIG_PREFIX . sanitize_key($form_id));
357 if (!empty($config) && is_array($config)) {
358 return $config;
359 }
360
361 return self::RebuildConfig($form_id);
362 }
363
364 /**
365 * Rebuild the form config from source post content and store as option.
366 * Returns the config array or null if form cannot be found.
367 */
368 public static function RebuildConfig($form_id)
369 {
370 $entry = self::Get($form_id);
371 $source_post_id = ($entry && isset($entry['source_post_id'])) ? $entry['source_post_id'] : null;
372
373 $post = null;
374 if ($source_post_id !== null) {
375 $post = get_post($source_post_id);
376 }
377
378 // If source post is gone or unknown, try a broad search
379 if (!$post || !self::PostContainsForm($form_id, $post)) {
380 $found = self::FindFormInPosts($form_id);
381 if (!$found) {
382 return null;
383 }
384 $post = get_post($found['post_id']);
385 if (!$post) {
386 return null;
387 }
388 // Update registry with discovered source
389 $registry = self::GetAll();
390 if (isset($registry[$form_id])) {
391 $registry[$form_id]['source_post_id'] = $found['post_id'];
392 $registry[$form_id]['source_post_type'] = $found['post_type'];
393 $registry[$form_id]['updated'] = time();
394 update_option(self::OPTION_KEY, $registry, false);
395 }
396 }
397
398 $form_data = self::ExtractFormFromPost($form_id, $post);
399 if (!$form_data) {
400 return null;
401 }
402
403 update_option(self::CONFIG_PREFIX . sanitize_key($form_id), $form_data, false);
404
405 return $form_data;
406 }
407
408 /**
409 * Check if a post contains a form block with a specific formId.
410 */
411 private static function PostContainsForm($form_id, $post)
412 {
413 if (!has_block(self::BLOCK_NAME, $post) && !has_block(self::MULTISTEP_BLOCK_NAME, $post)) {
414 return false;
415 }
416
417 $parsed_blocks = parse_blocks($post->post_content);
418 $flattened = function_exists('_flatten_blocks') ? _flatten_blocks($parsed_blocks) : self::FlattenBlocks($parsed_blocks);
419
420 foreach ($flattened as $block) {
421 $is_match = ($block['blockName'] === self::BLOCK_NAME || $block['blockName'] === self::MULTISTEP_BLOCK_NAME)
422 && isset($block['attrs']['formId']) && sanitize_key($block['attrs']['formId']) === $form_id;
423 if ($is_match) {
424 return true;
425 }
426 }
427
428 return false;
429 }
430
431 /**
432 * Search across post types for a form block with a specific formId.
433 * Returns array('post_id' => int, 'post_type' => string) or null.
434 */
435 private static function FindFormInPosts($form_id)
436 {
437 $posts = get_posts(array(
438 'post_type' => self::$supported_post_types,
439 'post_status' => array('publish', 'draft', 'private', 'pending', 'future'),
440 'posts_per_page' => -1,
441 'fields' => 'ids',
442 'no_found_rows' => true,
443 'update_post_meta_cache' => false,
444 'update_post_term_cache' => false,
445 ));
446
447 foreach ($posts as $pid) {
448 $post = get_post($pid);
449 if (!$post || empty($post->post_content)) {
450 continue;
451 }
452 // Fast check: does the content even mention our block?
453 if (!has_block(self::BLOCK_NAME, $post) && !has_block(self::MULTISTEP_BLOCK_NAME, $post)) {
454 continue;
455 }
456 if (self::PostContainsForm($form_id, $post)) {
457 return array(
458 'post_id' => $post->ID,
459 'post_type' => $post->post_type,
460 );
461 }
462 }
463
464 return null;
465 }
466
467 /**
468 * Extract a form block's full attributes (including inner field blocks) from a post.
469 * Returns the attributes array or null if not found.
470 */
471 private static function ExtractFormFromPost($form_id, $post)
472 {
473 $parsed_blocks = parse_blocks($post->post_content);
474 $form_block = self::FindFormBlock($form_id, $parsed_blocks);
475
476 if (!$form_block) {
477 return null;
478 }
479
480 $attrs = isset($form_block['attrs']) ? $form_block['attrs'] : array();
481
482 // Extract inner form-field blocks (handles both flat and multistep nesting)
483 $is_multistep = isset($form_block['blockName']) && $form_block['blockName'] === self::MULTISTEP_BLOCK_NAME;
484 $attrs['formFields'] = self::ExtractFieldBlocks($form_block, $is_multistep);
485
486 return $attrs;
487 }
488
489 /**
490 * Recursively find a form block with a specific formId in parsed blocks.
491 */
492 private static function FindFormBlock($form_id, $blocks)
493 {
494 foreach ($blocks as $block) {
495 if (
496 isset($block['blockName']) && ($block['blockName'] === self::BLOCK_NAME || $block['blockName'] === self::MULTISTEP_BLOCK_NAME)
497 && isset($block['attrs']['formId']) && sanitize_key($block['attrs']['formId']) === $form_id
498 ) {
499 return $block;
500 }
501 if (!empty($block['innerBlocks'])) {
502 $found = self::FindFormBlock($form_id, $block['innerBlocks']);
503 if ($found) {
504 return $found;
505 }
506 }
507 }
508 return null;
509 }
510
511 /**
512 * Fallback flatten for WP versions without _flatten_blocks().
513 */
514 private static function FlattenBlocks($blocks)
515 {
516 $result = array();
517 foreach ($blocks as $block) {
518 $result[] = $block;
519 if (!empty($block['innerBlocks'])) {
520 $result = array_merge($result, self::FlattenBlocks($block['innerBlocks']));
521 }
522 }
523 return $result;
524 }
525 }
526