PluginProbe ʕ •ᴥ•ʔ
JetFormBuilder — Dynamic Blocks Form Builder / trunk
JetFormBuilder — Dynamic Blocks Form Builder vtrunk
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 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.1.0 2.1.1 2.1.10 2.1.11 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 3.0.0 3.0.0.1 3.0.0.2 3.0.0.3 3.0.1 3.0.1.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.0.1 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.3.2 3.3.3 3.3.3.1 3.3.4 3.3.4.1 3.3.4.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.4.5 3.4.5.1 3.4.5.2 3.4.6 3.4.7 3.4.7.1 3.5.0 3.5.1 3.5.1.1 3.5.1.2 3.5.2 3.5.2.1 3.5.3 3.5.4 3.5.5 3.5.6 3.5.6.1 3.5.6.2 3.5.6.3 3.6.0
jetformbuilder / modules / gateways / secure-price-notice.php
jetformbuilder / modules / gateways Last commit date
actions-abstract 2 years ago assets 20 hours ago db-models 2 years ago export 2 years ago legacy 2 years ago meta-boxes 2 years ago pages 2 years ago paypal 20 hours ago query-views 2 years ago rest-api 8 months ago scenarios-abstract 2 years ago tab-handlers 2 years ago table-views 20 hours ago base-gateway-action.php 1 year ago base-gateway.php 3 months ago base-scenario-gateway.php 2 years ago gateways-editor-data.php 20 hours ago legacy-base-gateway.php 20 hours ago migrate-legacy-data.php 2 years ago module.php 20 hours ago scenario-item.php 2 years ago secure-price-notice.php 20 hours ago trusted-price-resolver-expression-parser.php 20 hours ago trusted-price-resolver.php 20 hours ago
secure-price-notice.php
353 lines
1 <?php
2
3 namespace JFB_Modules\Gateways;
4
5 use JFB_Modules\Post_Type\Module as Post_Type_Module;
6
7 if ( ! defined( 'WPINC' ) ) {
8 die;
9 }
10
11 class Secure_Price_Notice {
12
13 const SCAN_VERSION_OPTION = 'jet_fb_secure_price_scan_version';
14 const NOTICE_OPTION = 'jet_fb_secure_price_notice';
15 const DISMISS_META_KEY = 'jet_fb_secure_price_notice_dismissed';
16 const NOTICE_QUERY_ARG = 'jet_fb_dismiss_secure_price_notice';
17 const SCAN_SCHEMA_VERSION = '1';
18 const SCAN_BATCH_SIZE = 30;
19
20 public function init_hooks() {
21 add_action( 'save_post_' . Post_Type_Module::SLUG, array( $this, 'invalidate_scan_cache' ) );
22
23 if ( ! is_admin() ) {
24 return;
25 }
26
27 add_action( 'admin_init', array( $this, 'maybe_scan_affected_forms' ) );
28 add_action( 'admin_init', array( $this, 'maybe_dismiss_notice' ) );
29 add_action( 'admin_notices', array( $this, 'render_affected_forms_notice' ) );
30 }
31
32 public function remove_hooks() {
33 remove_action( 'save_post_' . Post_Type_Module::SLUG, array( $this, 'invalidate_scan_cache' ) );
34 remove_action( 'admin_init', array( $this, 'maybe_scan_affected_forms' ) );
35 remove_action( 'admin_init', array( $this, 'maybe_dismiss_notice' ) );
36 remove_action( 'admin_notices', array( $this, 'render_affected_forms_notice' ) );
37 }
38
39 public function invalidate_scan_cache() {
40 delete_option( self::SCAN_VERSION_OPTION );
41 }
42
43 public function maybe_scan_affected_forms() {
44 if ( ! current_user_can( 'manage_options' ) ) {
45 return;
46 }
47
48 if (
49 wp_doing_ajax() ||
50 wp_doing_cron() ||
51 ( defined( 'REST_REQUEST' ) && REST_REQUEST )
52 ) {
53 return;
54 }
55
56 $version = $this->get_scan_version();
57 $notice = get_option( self::NOTICE_OPTION, array() );
58
59 if ( get_option( self::SCAN_VERSION_OPTION, '' ) === $version ) {
60 return;
61 }
62
63 $tracked_ids = array_values(
64 array_unique(
65 array_filter(
66 array_map( 'intval', $notice['tracked_ids'] ?? array() )
67 )
68 )
69 );
70
71 if ( array_key_exists( 'tracked_ids', $notice ) ) {
72 $forms = empty( $tracked_ids )
73 ? array()
74 : $this->scan_specific_forms( $tracked_ids );
75
76 $this->save_scan( $version, $tracked_ids, $forms );
77
78 return;
79 }
80
81 $forms = $this->scan_affected_forms();
82 $tracked_ids = array_values( array_unique( array_map( 'intval', wp_list_pluck( $forms, 'id' ) ) ) );
83
84 $this->save_scan( $version, $tracked_ids, $forms );
85 }
86
87 public function maybe_dismiss_notice() {
88 if ( ! current_user_can( 'manage_options' ) ) {
89 return;
90 }
91
92 if ( empty( $_GET[ self::NOTICE_QUERY_ARG ] ) ) {
93 return;
94 }
95
96 check_admin_referer( self::NOTICE_QUERY_ARG );
97
98 update_user_meta(
99 get_current_user_id(),
100 self::DISMISS_META_KEY,
101 $this->get_dismiss_version()
102 );
103
104 wp_safe_redirect(
105 remove_query_arg(
106 array(
107 self::NOTICE_QUERY_ARG,
108 '_wpnonce',
109 )
110 )
111 );
112 exit;
113 }
114
115 public function render_affected_forms_notice() {
116 if ( ! current_user_can( 'manage_options' ) ) {
117 return;
118 }
119
120 $notice = get_option( self::NOTICE_OPTION, array() );
121
122 if ( empty( $notice['forms'] ) || empty( $notice['version'] ) ) {
123 return;
124 }
125
126 if ( $this->get_scan_version() !== $notice['version'] || $this->is_notice_dismissed( $notice ) ) {
127 return;
128 }
129
130 $forms = array_slice( $notice['forms'], 0, 5 );
131 $more_forms = count( $notice['forms'] ) - count( $forms );
132 $dismiss_url = wp_nonce_url(
133 add_query_arg( self::NOTICE_QUERY_ARG, '1' ),
134 self::NOTICE_QUERY_ARG
135 );
136
137 ?>
138 <div class="notice notice-warning">
139 <p>
140 <strong><?php esc_html_e( 'JetFormBuilder: enable Secure payment amount for existing payment forms.', 'jet-form-builder' ); ?></strong>
141 </p>
142 <p>
143 <?php esc_html_e( 'This update adds server-side payment amount validation, but it is disabled for existing forms to preserve compatibility. The forms below still trust the submitted amount, which visitors may be able to modify.', 'jet-form-builder' ); ?>
144 </p>
145 <p>
146 <?php esc_html_e( 'Open each form, enable Secure payment amount in Gateways Settings, verify that its price source is supported, and test the payment flow. Unsupported client-controlled price sources will be rejected after protection is enabled.', 'jet-form-builder' ); ?>
147 </p>
148 <ul style="list-style: disc; margin-left: 1.5em;">
149 <?php foreach ( $forms as $form ) : ?>
150 <li>
151 <a href="<?php echo esc_url( $form['edit_link'] ); ?>">
152 <?php echo esc_html( $form['title'] ); ?>
153 </a>
154 <?php echo esc_html( ' (#' . $form['id'] . ')' ); ?>:
155 <?php
156 echo esc_html(
157 sprintf(
158 /* translators: %s: comma-separated payment gateway IDs */
159 __( 'payment gateway(s): %s', 'jet-form-builder' ),
160 implode( ', ', $form['gateways'] )
161 )
162 );
163 ?>
164 </li>
165 <?php endforeach; ?>
166 </ul>
167 <?php if ( $more_forms > 0 ) : ?>
168 <p>
169 <?php
170 echo esc_html(
171 sprintf(
172 /* translators: %d: number of hidden forms */
173 __( 'Plus %d more form(s) that should be reviewed.', 'jet-form-builder' ),
174 $more_forms
175 )
176 );
177 ?>
178 </p>
179 <?php endif; ?>
180 <p>
181 <a href="<?php echo esc_url( admin_url( 'edit.php?post_type=' . Post_Type_Module::SLUG ) ); ?>" class="button button-primary">
182 <?php esc_html_e( 'Review Forms', 'jet-form-builder' ); ?>
183 </a>
184 <a href="<?php echo esc_url( $dismiss_url ); ?>" class="button button-secondary">
185 <?php esc_html_e( 'Dismiss', 'jet-form-builder' ); ?>
186 </a>
187 </p>
188 </div>
189 <?php
190 }
191
192 private function save_scan( string $version, array $tracked_ids, array $forms ) {
193 update_option( self::SCAN_VERSION_OPTION, $version, false );
194 update_option(
195 self::NOTICE_OPTION,
196 array(
197 'version' => $version,
198 'notice_token' => wp_generate_uuid4(),
199 'tracked_ids' => $tracked_ids,
200 'forms' => $forms,
201 ),
202 false
203 );
204 }
205
206 private function scan_affected_forms(): array {
207 $affected_forms = array();
208 $offset = 0;
209
210 while ( true ) {
211 $form_ids = get_posts(
212 array(
213 'post_type' => Post_Type_Module::SLUG,
214 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ),
215 'posts_per_page' => self::SCAN_BATCH_SIZE,
216 'offset' => $offset,
217 'fields' => 'ids',
218 'orderby' => 'ID',
219 'order' => 'ASC',
220 'no_found_rows' => true,
221 'update_post_meta_cache' => false,
222 'update_post_term_cache' => false,
223 )
224 );
225
226 if ( empty( $form_ids ) ) {
227 break;
228 }
229
230 foreach ( $form_ids as $form_id ) {
231 $form = $this->get_affected_form( (int) $form_id );
232
233 if ( $form ) {
234 $affected_forms[] = $form;
235 }
236 }
237
238 if ( count( $form_ids ) < self::SCAN_BATCH_SIZE ) {
239 break;
240 }
241
242 $offset += self::SCAN_BATCH_SIZE;
243 }
244
245 return $affected_forms;
246 }
247
248 private function scan_specific_forms( array $form_ids ): array {
249 $affected_forms = array();
250
251 foreach ( $form_ids as $form_id ) {
252 $form_id = (int) $form_id;
253
254 if ( $form_id <= 0 || Post_Type_Module::SLUG !== get_post_type( $form_id ) ) {
255 continue;
256 }
257
258 $form = $this->get_affected_form( $form_id );
259
260 if ( $form ) {
261 $affected_forms[] = $form;
262 }
263 }
264
265 return $affected_forms;
266 }
267
268 private function get_affected_form( int $form_id ): array {
269 $settings = jet_form_builder()->post_type->get_gateways( $form_id );
270 $gateways = $this->get_active_gateways( $settings );
271
272 if ( empty( $gateways ) || $this->is_price_protection_enabled( $settings ) ) {
273 return array();
274 }
275
276 return array(
277 'id' => $form_id,
278 'title' => get_the_title( $form_id ) ?: sprintf(
279 /* translators: %d: form ID */
280 __( 'Form #%d', 'jet-form-builder' ),
281 $form_id
282 ),
283 'edit_link' => get_edit_post_link( $form_id, 'raw' ) ?: admin_url( 'post.php?post=' . absint( $form_id ) . '&action=edit' ),
284 'gateways' => $gateways,
285 );
286 }
287
288 private function get_active_gateways( array $settings ): array {
289 $gateways = array();
290
291 if ( 'manual' === ( $settings['mode'] ?? 'single' ) ) {
292 foreach ( $settings as $gateway_id => $gateway_settings ) {
293 if (
294 ! is_string( $gateway_id ) ||
295 ! is_array( $gateway_settings ) ||
296 ! filter_var( $gateway_settings['show_on_front'] ?? false, FILTER_VALIDATE_BOOLEAN )
297 ) {
298 continue;
299 }
300
301 $gateways[] = sanitize_key( $gateway_id );
302 }
303 } else {
304 $gateway_id = is_string( $settings['gateway'] ?? null )
305 ? sanitize_key( $settings['gateway'] )
306 : '';
307
308 if ( $gateway_id && ! in_array( $gateway_id, array( 'none', 'manual' ), true ) ) {
309 $gateways[] = $gateway_id;
310 }
311 }
312
313 return array_values( array_unique( array_filter( $gateways ) ) );
314 }
315
316 private function is_price_protection_enabled( array $settings ): bool {
317 return filter_var( $settings['protect_price_field'] ?? false, FILTER_VALIDATE_BOOLEAN );
318 }
319
320 private function get_scan_version(): string {
321 return jet_form_builder()->get_version() . ':' . self::SCAN_SCHEMA_VERSION;
322 }
323
324 private function get_dismiss_version(): string {
325 return 'schema:' . self::SCAN_SCHEMA_VERSION;
326 }
327
328 private function is_notice_dismissed( array $notice ): bool {
329 $dismissed = (string) get_user_meta( get_current_user_id(), self::DISMISS_META_KEY, true );
330
331 if ( '' === $dismissed ) {
332 return false;
333 }
334
335 if (
336 $dismissed === $this->get_dismiss_version() ||
337 $dismissed === $this->get_notice_dismiss_token( $notice )
338 ) {
339 return true;
340 }
341
342 // Older builds stored "<plugin-version>:<schema>" in user meta.
343 $legacy_suffix = ':' . self::SCAN_SCHEMA_VERSION;
344
345 return strlen( $dismissed ) > strlen( $legacy_suffix )
346 && substr( $dismissed, -strlen( $legacy_suffix ) ) === $legacy_suffix;
347 }
348
349 private function get_notice_dismiss_token( array $notice ): string {
350 return (string) ( $notice['notice_token'] ?? $notice['version'] ?? '' );
351 }
352 }
353