PluginProbe
JetFormBuilder — Dynamic Blocks Form Builder / 3.6.5.4
JetFormBuilder — Dynamic Blocks Form Builder v3.6.5.4
3.6.6.1 3.6.6 3.6.5.4 3.6.5.3 3.6.5.2 3.6.5.1 3.6.5 3.6.4.2 3.6.4.1 3.6.4 3.6.3.1 3.6.3 3.6.2.2 3.6.2.1 3.6.2 3.6.1.1 3.6.1 3.6.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 All 134 releases
jetformbuilder / modules / validation / ssr / ssr-blocked-callback-usages.php

ssr-blocked-callback-usages.php in JetFormBuilder — Dynamic Blocks Form Builder 3.6.5.4, at modules/validation/ssr/ssr-blocked-callback-usages.php

224 lines 7.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 namespace JFB_Modules\Validation\Ssr;
5
6 use JFB_Modules\Validation\Advanced_Rules\Server_Side_Rule;
7
8 // If this file is called directly, abort.
9 if ( ! defined( 'WPINC' ) ) {
10 die;
11 }
12
13 /**
14 * Read-only record of existing forms whose saved "Server-Side callback" rule names are on
15 * the `Server_Side_Rule::NOT_ALLOWED` denylist, populated once by
16 * `Migrations\Versions\Version_3_6_5_3` and kept live afterwards by `save_post_jet-form-
17 * builder` (see `Module::refresh_blocked_callback_usages()`). A denylisted name is never
18 * imported into `Ssr_Callback_Registry` and can never be approved — this is informational
19 * only, so a `manage_options` admin can find and manually fix the affected forms instead of
20 * only discovering the break when a visitor reports a form that stopped validating.
21 *
22 * Unlike the pending-callback registry, entries here have no approve/reject action: the
23 * only "fix" is editing the form to remove or replace the blocked rule, which is why each
24 * entry carries a form ID (for an edit-form link) and field name rather than just a
25 * function name.
26 *
27 * Storage: one post meta entry per form (`META_KEY`), not one site-wide option. An earlier
28 * revision of this class stored every form's entries together in a single option, keyed by
29 * `form_id` inside the array — `replace_for_form()` then had to read-modify-write that
30 * whole option on every single form save (or reusable-block save, since a blocked usage can
31 * come from inside a referenced reusable block). Two forms saved close enough together could
32 * both read the option before either wrote back, so whichever write landed second silently
33 * discarded the other form's entry (review finding, issues-tracker #20361 follow-up) — and
34 * an "optimistic concurrency" re-read-before-write fix for that turned out not to actually
35 * detect the conflict, since `get_option()` is cached for the lifetime of the PHP request
36 * (even without a persistent object cache backend) so both the snapshot and the "did it
37 * change" re-read return the same cached value regardless of what another process wrote to
38 * the DB in between (review finding, issues-tracker #20361 follow-up). Per-form post meta
39 * removes the conflict at its root instead of working around it: two forms writing their own
40 * meta touch two different `wp_postmeta` rows, so there is nothing left to race over.
41 *
42 * @since 3.6.5.3
43 */
44 class Ssr_Blocked_Callback_Usages {
45
46 const META_KEY = '_jfb_ssr_blocked_callback_usages';
47
48 /**
49 * Walks the form's blocks (including reusable blocks) collecting SSR rules whose
50 * configured name is on the `Server_Side_Rule::NOT_ALLOWED` denylist. Shared between
51 * the one-time migration scan and the live `save_post_jet-form-builder` refresh so the
52 * two can never drift on what counts as "blocked."
53 *
54 * @since 3.6.5.3
55 *
56 * @param string $post_content
57 *
58 * @return array{field: string, name: string}[]
59 */
60 public static function collect_from_content( string $post_content ): array {
61 if ( '' === trim( $post_content ) ) {
62 return array();
63 }
64
65 $found = array();
66 $blocks = \Jet_Form_Builder\Blocks\Block_Helper::get_blocks_from_content( $post_content );
67
68 foreach ( \Jet_Form_Builder\Blocks\Block_Helper::generate_blocks_in_space( $blocks ) as $block ) {
69 $rules = $block['attrs']['validation']['rules'] ?? array();
70
71 foreach ( $rules as $rule ) {
72 if ( 'ssr' !== ( $rule['type'] ?? '' ) ) {
73 continue;
74 }
75
76 $name = Server_Side_Rule::sanitize_callback_name( (string) ( $rule['value'] ?? '' ) );
77
78 if ( '' === $name || ! in_array( $name, Server_Side_Rule::NOT_ALLOWED, true ) ) {
79 continue;
80 }
81
82 $found[] = array(
83 'field' => (string) ( $block['attrs']['name'] ?? '' ),
84 'name' => $name,
85 );
86 }
87 }
88
89 return $found;
90 }
91
92 /**
93 * Replaces the given form's own blocked-usage entries. A plain `update_post_meta()`/
94 * `delete_post_meta()` on this one form's own meta row — no read-modify-write of shared
95 * state, so no concurrent-save conflict is possible between different forms (see the
96 * class docblock). Passing an empty `$usages` removes the meta entirely, which is
97 * exactly what should happen once the form's blocked rule is fixed or removed.
98 *
99 * @since 3.6.5.3
100 *
101 * @param int $form_id
102 * @param array{field: string, name: string}[] $usages Entries for this form only (no
103 * `form_id` key needed).
104 */
105 public static function replace_for_form( int $form_id, array $usages ) {
106 $clean = self::sanitize_entries( $usages );
107
108 if ( empty( $clean ) ) {
109 delete_post_meta( $form_id, self::META_KEY );
110
111 return;
112 }
113
114 update_post_meta( $form_id, self::META_KEY, $clean );
115 }
116
117 /**
118 * @since 3.6.5.3
119 *
120 * @return array{field: string, name: string}[]
121 */
122 public static function get_usages_for_form( int $form_id ): array {
123 $stored = get_post_meta( $form_id, self::META_KEY, true );
124
125 return is_array( $stored ) ? self::sanitize_entries( $stored ) : array();
126 }
127
128 /**
129 * Every form currently carrying at least one blocked-usage entry. Used by the settings
130 * tab to build the "Forms Using Blocked Functions" list — a `WP_Query` `meta_key`
131 * existence lookup across `jet-form-builder` posts, rather than reading one shared
132 * option, since the data itself now lives per-form (see the class docblock).
133 *
134 * @since 3.6.5.3
135 *
136 * @return array{form_id: int, field: string, name: string}[]
137 */
138 public static function get_usages(): array {
139 $query = new \WP_Query(
140 array(
141 'post_type' => 'jet-form-builder',
142 // Every status a form can meaningfully be in, matching the migration scan's
143 // own `post_status` filter (`Version_3_6_5_3::up()`) — a trashed form can
144 // still be restored and must not silently drop off this list in the meantime.
145 'post_status' => array( 'publish', 'draft', 'pending', 'private', 'future', 'trash' ),
146 'meta_key' => self::META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
147 'fields' => 'ids',
148 'posts_per_page' => -1,
149 'no_found_rows' => true,
150 'orderby' => 'ID',
151 'order' => 'ASC',
152 )
153 );
154
155 $usages = array();
156
157 foreach ( $query->posts as $form_id ) {
158 $form_id = (int) $form_id;
159
160 foreach ( self::get_usages_for_form( $form_id ) as $entry ) {
161 $usages[] = array(
162 'form_id' => $form_id,
163 'field' => $entry['field'],
164 'name' => $entry['name'],
165 );
166 }
167 }
168
169 return $usages;
170 }
171
172 /**
173 * Removes a form's blocked-usage meta entirely. Used when a form is permanently deleted
174 * (`Module::remove_blocked_callback_usages_for_deleted_form()`); redundant with
175 * WordPress's own post meta cleanup on `wp_delete_post()` in the common case, but this
176 * project's `remove_blocked_callback_usages_for_deleted_form()` predates that assumption
177 * and calling it explicitly costs nothing.
178 *
179 * @since 3.6.5.3
180 */
181 public static function clear_for_form( int $form_id ) {
182 delete_post_meta( $form_id, self::META_KEY );
183 }
184
185 /**
186 * Removes every form's blocked-usage meta. Used by `Version_3_6_5_3::down()` to fully
187 * reverse the migration.
188 *
189 * @since 3.6.5.3
190 */
191 public static function clear() {
192 global $wpdb;
193
194 $wpdb->delete( $wpdb->postmeta, array( 'meta_key' => self::META_KEY ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.SlowDBQuery.slow_db_query_meta_key
195
196 wp_cache_delete( 'last_changed', 'posts' );
197 }
198
199 /**
200 * @param array<int, array<string, mixed>> $usages
201 *
202 * @return array{field: string, name: string}[]
203 */
204 private static function sanitize_entries( array $usages ): array {
205 $clean = array();
206
207 foreach ( $usages as $usage ) {
208 $name = isset( $usage['name'] ) ? (string) $usage['name'] : '';
209
210 if ( '' === $name ) {
211 continue;
212 }
213
214 $clean[] = array(
215 'field' => isset( $usage['field'] ) ? (string) $usage['field'] : '',
216 'name' => $name,
217 );
218 }
219
220 return $clean;
221 }
222
223 }
224