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-access-control.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-access-control.php
452 lines
1 <?php
2
3 namespace SuperbAddons\Gutenberg\Form;
4
5 defined('ABSPATH') || exit();
6
7 class FormAccessControl
8 {
9 const OPTION_ENABLED = 'superbaddons_form_access_control_enabled';
10
11 /**
12 * Sensitive form-level attributes that require the 'configure' capability.
13 * Everything else is considered "safe" and requires the 'edit' capability.
14 */
15 private static $sensitive_attrs = array(
16 // Email routing
17 'emailEnabled',
18 'emailTo',
19 'emailSubject',
20 'emailReplyTo',
21 'emailCC',
22 'emailBCC',
23 // User confirmation
24 'sendConfirmation',
25 'confirmationSubject',
26 'confirmationMessage',
27 'confirmationEmailField',
28 // Anti-spam
29 'captchaType',
30 'honeypotKey',
31 // Data storage
32 'storeEnabled',
33 'storeSpamEnabled',
34 // Integrations
35 'mailchimpEnabled',
36 'mailchimpListIds',
37 'brevoEnabled',
38 'brevoListIds',
39 // Webhook (secret stored separately in encrypted option, not as block attribute)
40 'webhookEnabled',
41 'webhookUrl',
42 'webhookMethod',
43 'webhookHeaders',
44 // Google Sheets
45 'googleSheetsEnabled',
46 'googleSheetsSpreadsheetUrl',
47 'googleSheetsSheetName',
48 // Slack
49 'slackEnabled',
50 'slackWebhookUrl',
51 // Redirects
52 'redirectUrl',
53 'successBehavior',
54 );
55
56 /**
57 * Block names that represent form containers.
58 */
59 private static $form_block_names = array(
60 'superb-addons/form',
61 'superb-addons/multistep-form',
62 );
63
64 public static function Initialize()
65 {
66 add_filter('wp_insert_post_data', array(__CLASS__, 'OnInsertPostData'), 10, 2);
67 }
68
69 /**
70 * Whether form access control restrictions are enabled.
71 */
72 public static function IsEnabled()
73 {
74 return (bool) get_option(self::OPTION_ENABLED, false);
75 }
76
77 /**
78 * Get the list of sensitive attribute keys.
79 */
80 public static function GetSensitiveAttrs()
81 {
82 return self::$sensitive_attrs;
83 }
84
85 /**
86 * Filter: wp_insert_post_data — enforce form access control before content is saved.
87 *
88 * @param array $data Sanitized post data about to be inserted.
89 * @param array $postarr Raw post data array (includes ID for updates).
90 * @return array Possibly corrected $data.
91 */
92 public static function OnInsertPostData($data, $postarr)
93 {
94 // Feature must be enabled
95 if (!self::IsEnabled()) {
96 return $data;
97 }
98
99 // Skip autosave and revisions
100 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
101 return $data;
102 }
103 $post_type = isset($data['post_type']) ? $data['post_type'] : '';
104 if ($post_type === 'revision') {
105 return $data;
106 }
107
108 // Only process supported post types
109 if (!in_array($post_type, FormRegistry::GetSupportedPostTypes(), true)) {
110 return $data;
111 }
112
113 // Admins always bypass
114 if (current_user_can('manage_options')) {
115 return $data;
116 }
117
118 $content = isset($data['post_content']) ? $data['post_content'] : '';
119
120 // Fast bail: no form blocks in the content
121 if (strpos($content, 'superb-addons/form') === false && strpos($content, 'superb-addons/multistep-form') === false) {
122 return $data;
123 }
124
125 // Resolve capabilities
126 $can_edit = FormPermissions::Can('edit');
127 $can_configure = FormPermissions::Can('configure');
128 $can_create = FormPermissions::Can('create');
129
130 // If user has all capabilities, nothing to enforce
131 if ($can_edit && $can_configure && $can_create) {
132 return $data;
133 }
134
135 // Load old content for comparison (updates only)
136 $post_id = isset($postarr['ID']) ? intval($postarr['ID']) : 0;
137 $old_blocks = array();
138 if ($post_id > 0) {
139 $old_post = get_post($post_id);
140 if ($old_post && !empty($old_post->post_content)) {
141 $old_blocks = parse_blocks($old_post->post_content);
142 }
143 }
144
145 // Build lookup of old form blocks by formId for fast access
146 $old_form_blocks = self::IndexFormBlocksByFormId($old_blocks);
147
148 // Parse and enforce new content
149 $new_blocks = parse_blocks($content);
150 $enforced = self::EnforceFormBlocks($new_blocks, $old_form_blocks, $can_edit, $can_configure, $can_create);
151 $data['post_content'] = serialize_blocks($enforced);
152
153 return $data;
154 }
155
156 /**
157 * Build a flat lookup of form blocks indexed by formId from a parsed block tree.
158 *
159 * @param array $blocks Parsed blocks.
160 * @return array formId => block (full block array including innerBlocks/innerContent).
161 */
162 private static function IndexFormBlocksByFormId($blocks)
163 {
164 $index = array();
165 foreach ($blocks as $block) {
166 if (self::IsFormBlock($block) && !empty($block['attrs']['formId'])) {
167 $index[sanitize_key($block['attrs']['formId'])] = $block;
168 }
169 if (!empty($block['innerBlocks'])) {
170 $inner_index = self::IndexFormBlocksByFormId($block['innerBlocks']);
171 foreach ($inner_index as $fid => $fblock) {
172 $index[$fid] = $fblock;
173 }
174 }
175 }
176 return $index;
177 }
178
179 /**
180 * Check if a parsed block is one of our form container blocks.
181 */
182 private static function IsFormBlock($block)
183 {
184 return isset($block['blockName']) && in_array($block['blockName'], self::$form_block_names, true);
185 }
186
187 /**
188 * Recursively walk blocks, enforcing access control on form blocks.
189 *
190 * @param array $blocks Parsed new blocks.
191 * @param array $old_form_blocks Indexed old form blocks (formId => block).
192 * @param bool $can_edit User has 'edit' capability.
193 * @param bool $can_configure User has 'configure' capability.
194 * @param bool $can_create User has 'create' capability.
195 * @return array Enforced blocks.
196 */
197 private static function EnforceFormBlocks($blocks, $old_form_blocks, $can_edit, $can_configure, $can_create)
198 {
199 $result = array();
200
201 foreach ($blocks as $block) {
202 if (self::IsFormBlock($block)) {
203 $form_id = isset($block['attrs']['formId']) ? sanitize_key($block['attrs']['formId']) : '';
204
205 // Determine if this is an existing or new form
206 $is_existing = !empty($form_id) && (
207 isset($old_form_blocks[$form_id]) || FormRegistry::Get($form_id) !== null
208 );
209
210 if ($is_existing) {
211 // Existing form — enforce based on edit/configure capabilities
212 $old_block = isset($old_form_blocks[$form_id]) ? $old_form_blocks[$form_id] : null;
213
214 // If no old block in content (e.g. form was moved from another post),
215 // try rebuilding reference from stored config
216 if ($old_block === null) {
217 $old_block = self::BuildReferenceBlockFromConfig($form_id);
218 }
219
220 if ($old_block !== null) {
221 $block = self::EnforceExistingFormBlock($block, $old_block, $can_edit, $can_configure);
222 }
223 // If we can't find old block data at all, allow through (graceful degradation)
224 } else {
225 // New form — requires 'create' capability
226 if (!$can_create) {
227 // Strip this form block entirely
228 continue;
229 }
230 // User can create: allow the block through as-is
231 }
232
233 $result[] = $block;
234 } else {
235 // Not a form block — recurse into inner blocks
236 if (!empty($block['innerBlocks'])) {
237 $block['innerBlocks'] = self::EnforceFormBlocks(
238 $block['innerBlocks'],
239 $old_form_blocks,
240 $can_edit,
241 $can_configure,
242 $can_create
243 );
244 }
245 $result[] = $block;
246 }
247 }
248
249 return $result;
250 }
251
252 /**
253 * Enforce access control on an existing form block.
254 *
255 * @param array $new_block The incoming (new) form block.
256 * @param array $old_block The previous (old) form block.
257 * @param bool $can_edit User has 'edit' capability.
258 * @param bool $can_configure User has 'configure' capability.
259 * @return array The enforced block.
260 */
261 private static function EnforceExistingFormBlock($new_block, $old_block, $can_edit, $can_configure)
262 {
263 if (!$can_edit && !$can_configure) {
264 // Fully read-only: restore entire block from old content
265 return $old_block;
266 }
267
268 $new_attrs = isset($new_block['attrs']) ? $new_block['attrs'] : array();
269 $old_attrs = isset($old_block['attrs']) ? $old_block['attrs'] : array();
270
271 if ($can_edit && !$can_configure) {
272 // Can edit safe attrs + inner blocks, but sensitive attrs must be restored from old
273 $new_block['attrs'] = self::RestoreSensitiveAttrs($new_attrs, $old_attrs);
274 // Protect the 'sensitive' attribute on inner form-field blocks
275 $sensitive_map = self::BuildFieldSensitiveMap($old_block);
276 if (!empty($sensitive_map)) {
277 $new_inner = isset($new_block['innerBlocks']) ? $new_block['innerBlocks'] : array();
278 $new_block['innerBlocks'] = self::EnforceFieldSensitiveAttr($new_inner, $sensitive_map);
279 }
280 } elseif ($can_configure && !$can_edit) {
281 // Can change sensitive attrs, but safe attrs + inner blocks must be restored from old
282 $new_block['attrs'] = self::RestoreSafeAttrs($new_attrs, $old_attrs);
283 // Restore inner blocks (field structure)
284 $new_block['innerBlocks'] = isset($old_block['innerBlocks']) ? $old_block['innerBlocks'] : array();
285 $new_block['innerContent'] = isset($old_block['innerContent']) ? $old_block['innerContent'] : array();
286 $new_block['innerHTML'] = isset($old_block['innerHTML']) ? $old_block['innerHTML'] : '';
287 }
288 // If both can_edit and can_configure: allow everything (shouldn't reach here due to early bail)
289
290 // When user has 'edit' but not 'configure', inner blocks can change freely except
291 // the 'sensitive' attribute on form-field blocks (protected above via BuildFieldSensitiveMap).
292 // When user lacks 'edit', inner blocks are restored above.
293
294 return $new_block;
295 }
296
297 /**
298 * Restore sensitive attrs from old block, keeping safe attrs from new block.
299 *
300 * @param array $new_attrs Incoming attributes.
301 * @param array $old_attrs Previous attributes.
302 * @return array Merged attributes.
303 */
304 private static function RestoreSensitiveAttrs($new_attrs, $old_attrs)
305 {
306 foreach (self::$sensitive_attrs as $key) {
307 if (array_key_exists($key, $old_attrs)) {
308 $new_attrs[$key] = $old_attrs[$key];
309 } elseif (array_key_exists($key, $new_attrs)) {
310 // Sensitive attr was added that didn't exist before — remove it
311 unset($new_attrs[$key]);
312 }
313 }
314 return $new_attrs;
315 }
316
317 /**
318 * Restore safe (non-sensitive) attrs from old block, keeping sensitive attrs from new block.
319 *
320 * @param array $new_attrs Incoming attributes.
321 * @param array $old_attrs Previous attributes.
322 * @return array Merged attributes.
323 */
324 private static function RestoreSafeAttrs($new_attrs, $old_attrs)
325 {
326 // Start with old attrs (all safe attrs preserved), then overlay sensitive attrs from new
327 $merged = $old_attrs;
328 foreach (self::$sensitive_attrs as $key) {
329 if (array_key_exists($key, $new_attrs)) {
330 $merged[$key] = $new_attrs[$key];
331 } elseif (isset($merged[$key])) {
332 // If new doesn't have it but old does, keep old (no removal of sensitive attrs)
333 }
334 }
335 return $merged;
336 }
337
338 /**
339 * Build a flat map of fieldId => sensitive value from a form block's inner blocks.
340 * Walks recursively to handle both direct form-field children (form block)
341 * and form-field blocks nested inside form-step blocks (multistep-form block).
342 *
343 * Falls back to the formFields config array when innerBlocks is empty
344 * (e.g. when old_block comes from BuildReferenceBlockFromConfig).
345 *
346 * @param array $block The old form block.
347 * @return array fieldId => bool (sensitive value).
348 */
349 private static function BuildFieldSensitiveMap($block)
350 {
351 $map = array();
352
353 // Primary path: walk innerBlocks recursively
354 if (!empty($block['innerBlocks'])) {
355 self::CollectFieldSensitiveValues($block['innerBlocks'], $map);
356 return $map;
357 }
358
359 // Fallback: use formFields from stored config (flat array of field attrs)
360 $attrs = isset($block['attrs']) ? $block['attrs'] : array();
361 if (!empty($attrs['formFields']) && is_array($attrs['formFields'])) {
362 foreach ($attrs['formFields'] as $field_attrs) {
363 if (!empty($field_attrs['fieldId'])) {
364 $map[$field_attrs['fieldId']] = !empty($field_attrs['sensitive']);
365 }
366 }
367 }
368
369 return $map;
370 }
371
372 /**
373 * Recursively collect fieldId => sensitive values from a list of inner blocks.
374 *
375 * @param array $blocks Inner blocks to walk.
376 * @param array &$map Map to populate (fieldId => bool).
377 */
378 private static function CollectFieldSensitiveValues($blocks, &$map)
379 {
380 foreach ($blocks as $block) {
381 if (
382 isset($block['blockName']) && $block['blockName'] === 'superb-addons/form-field'
383 && !empty($block['attrs']['fieldId'])
384 ) {
385 $map[$block['attrs']['fieldId']] = !empty($block['attrs']['sensitive']);
386 }
387 // Recurse into form-step or any other container
388 if (!empty($block['innerBlocks'])) {
389 self::CollectFieldSensitiveValues($block['innerBlocks'], $map);
390 }
391 }
392 }
393
394 /**
395 * Recursively walk inner blocks and restore the 'sensitive' attribute on
396 * form-field blocks from the old sensitive map. New fields (no match in map)
397 * pass through as-is.
398 *
399 * @param array $blocks New inner blocks.
400 * @param array $sensitive_map Old fieldId => bool map.
401 * @return array Enforced inner blocks.
402 */
403 private static function EnforceFieldSensitiveAttr($blocks, $sensitive_map)
404 {
405 $result = array();
406 foreach ($blocks as $block) {
407 if (
408 isset($block['blockName']) && $block['blockName'] === 'superb-addons/form-field'
409 && !empty($block['attrs']['fieldId'])
410 ) {
411 $field_id = $block['attrs']['fieldId'];
412 if (isset($sensitive_map[$field_id])) {
413 $block['attrs']['sensitive'] = $sensitive_map[$field_id];
414 }
415 // New fields (not in map) pass through unchanged
416 }
417 // Recurse into form-step or any other container
418 if (!empty($block['innerBlocks'])) {
419 $block['innerBlocks'] = self::EnforceFieldSensitiveAttr($block['innerBlocks'], $sensitive_map);
420 }
421 $result[] = $block;
422 }
423 return $result;
424 }
425
426 /**
427 * Build a minimal reference block from stored config when old post content isn't available.
428 * This handles cases where a form exists in the registry but the block was moved from another post.
429 *
430 * @param string $form_id The form ID.
431 * @return array|null A block-like array with attrs, or null if config not found.
432 */
433 private static function BuildReferenceBlockFromConfig($form_id)
434 {
435 $config = FormRegistry::GetConfig($form_id);
436 if (empty($config) || !is_array($config)) {
437 return null;
438 }
439
440 // We can restore attrs from config, but innerBlocks/innerContent cannot be
441 // faithfully reconstructed from the flat config. Return a minimal structure
442 // that allows attribute enforcement.
443 return array(
444 'blockName' => FormRegistry::BLOCK_NAME,
445 'attrs' => $config,
446 'innerBlocks' => array(),
447 'innerContent' => array(),
448 'innerHTML' => '',
449 );
450 }
451 }
452