Changeset 3782284
- Timestamp:
- 11/03/2026 04:58:22 PM (67 minutes ago)
- Location:
- submitsignal
- Files:
-
- 9 edited
- 1 copied
-
assets/screenshot-1.png (modified) ( previous)
-
assets/screenshot-2.png (modified) ( previous)
-
assets/screenshot-3.png (modified) ( previous)
-
tags/0.4.0 (copied) (copied from submitsignal/trunk )
-
tags/0.4.0/includes/native-admin.php (modified) (19 diffs)
-
tags/0.4.0/readme.txt (modified) (5 diffs)
-
tags/0.4.0/submitsignal.php (modified) (2 diffs)
-
trunk/includes/native-admin.php (modified) (19 diffs)
-
trunk/readme.txt (modified) (5 diffs)
-
trunk/submitsignal.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
submitsignal/tags/0.4.0/includes/native-admin.php
r3780656 r3782284 13 13 add_action( 'admin_post_submitsignal_native_action', array( __CLASS__, 'handle' ) ); 14 14 add_action( 'admin_post_submitsignal_native_return', array( __CLASS__, 'returned' ) ); 15 add_action( 'admin_init', array( __CLASS__, 'checkout_return' ) ); 15 16 add_action( 'submitsignal_native_expire', array( __CLASS__, 'expire' ) ); 16 17 } … … 131 132 if ( ! is_string( $url ) || strlen( $url ) > 4096 || preg_match( '/[\x00-\x20\x7f\\\\]/', $url ) ) { return false; } 132 133 $parts = wp_parse_url( $url ); 134 // Stripe Checkout can include opaque fragment state. Keep its full URL; 135 // the portal retains its separate host and fragment restrictions. 133 136 return is_array( $parts ) && isset( $parts['scheme'], $parts['host'] ) && 'https' === $parts['scheme'] && 134 137 ( $portal ? 'billing.stripe.com' : 'checkout.stripe.com' ) === $parts['host'] && 135 ! isset( $parts['user'] ) && ! isset( $parts['pass'] ) && ! isset( $parts['port'] ) && ! isset( $parts['fragment']);138 ! isset( $parts['user'] ) && ! isset( $parts['pass'] ) && ! isset( $parts['port'] ) && ( ! $portal || ! isset( $parts['fragment'] ) ); 136 139 } 137 140 public static function page_url( $url ) { 138 141 if ( ! is_string( $url ) || strlen( $url ) > 2048 || preg_match( '/[\x00-\x20\x7f\\\\]/', $url ) ) { return false; } 139 142 $parts = wp_parse_url( $url ); $home = wp_parse_url( home_url( '/' ) ); 140 if ( ! is_array( $parts ) || ! is_array( $home ) || empty( $parts['host'] ) || 'https' !== ( isset( $parts['scheme'] ) ? $parts['scheme'] : '' ) || strtolower( $parts['host'] ) !== strtolower( $home['host']) || isset( $parts['user'] ) || isset( $parts['pass'] ) || isset( $parts['fragment'] ) || isset( $parts['query'] ) || ( isset( $parts['port'] ) ? $parts['port'] : 443 ) !== ( isset( $home['port'] ) ? $home['port'] : 443 ) ) { return false; }143 if ( ! is_array( $parts ) || ! is_array( $home ) || empty( $parts['host'] ) || 'https' !== ( isset( $parts['scheme'] ) ? $parts['scheme'] : '' ) || preg_replace( '/\.$/', '', strtolower( $parts['host'] ) ) !== preg_replace( '/\.$/', '', strtolower( $home['host'] ) ) || isset( $parts['user'] ) || isset( $parts['pass'] ) || isset( $parts['fragment'] ) || isset( $parts['query'] ) || ( isset( $parts['port'] ) ? $parts['port'] : 443 ) !== ( isset( $home['port'] ) ? $home['port'] : 443 ) ) { return false; } 141 144 $base = rtrim( isset( $home['path'] ) ? $home['path'] : '', '/' ); 142 145 $path = isset( $parts['path'] ) ? $parts['path'] : '/'; … … 144 147 if ( preg_match( '#(?:^|/)(?:\.|\.\.)(?:/|$)#', $decoded ) || false !== strpos( $decoded, '\\' ) || preg_match( '/[\x00-\x20\x7f]/', $decoded ) || preg_match( '/%2f|%5c/i', $path ) ) { return false; } 145 148 return '' === $base || $path === $base || 0 === strpos( $path, $base . '/' ); 149 } 150 private static function canonical_page_url( $url ) { 151 // Validate the original first. Match only the service's known origin 152 // normalization; never decode or rewrite the selected page path. 153 if ( ! self::page_url( $url ) ) { return false; } 154 $parts = wp_parse_url( $url ); 155 $host = preg_replace( '/\.$/', '', strtolower( $parts['host'] ) ); 156 $port = isset( $parts['port'] ) && 443 !== $parts['port'] ? ':' . $parts['port'] : ''; 157 return 'https://' . $host . $port . ( ! empty( $parts['path'] ) ? $parts['path'] : '/' ); 146 158 } 147 159 private static function api( $path, $body, $token = '' ) { … … 209 221 if ( get_option( SubmitSignal_Plugin::SETTINGS ) !== $settings ) { return new WP_Error( 'storage', __( 'Approved credentials could not be saved. The private pending request remains for explicit claim retry after WordPress storage is repaired.', 'submitsignal' ) ); } 210 222 delete_option( self::VIEW ); self::clear_pending( self::key() ); SubmitSignal_Plugin::upgrade(); 223 // This single read follows the explicit account approval. A failed read 224 // never repeats the claim or turns a connection into a form test. 225 self::refresh_status( $settings ); 211 226 return true; 227 } 228 private static function connected_notice() { 229 $view = self::cache(); 230 return empty( $view['data'] ) ? __( 'Website connected. Current status is unavailable; use Refresh status. Do not repeat the connection.', 'submitsignal' ) : __( 'Website connected. Choose your form below and authorize its first check.', 'submitsignal' ); 212 231 } 213 232 public static function returned() { … … 218 237 if ( ! empty( $pending['claim_attempted'] ) ) { self::notice( __( 'A claim was already attempted. Use the explicit Retry connection claim button if its outcome is still unknown.', 'submitsignal' ) ); } 219 238 $result = self::claim( $pending ); 220 self::notice( is_wp_error( $result ) ? $result->get_error_message() : __( 'Website connected. Refresh status below, then choose a form and authorize its first check.', 'submitsignal') );239 self::notice( is_wp_error( $result ) ? $result->get_error_message() : self::connected_notice() ); 221 240 } 222 241 private static function cache() { … … 248 267 private static function payload( $operation, $view ) { 249 268 $body = array( 'action' => $operation ); 250 if ( 'create_monitor' === $operation) {269 if ( in_array( $operation, array( 'create_monitor', 'check_form' ), true ) ) { 251 270 $form = self::input( 'form_id' ); $url = self::input( 'page_url' ); $name = sanitize_text_field( self::input( 'name' ) ); 252 if ( '1' !== self::input( 'consent' ) || ! in_array( $form, array_column( SubmitSignal_Plugin::forms(), 'id' ), true ) || ! self::page_url( $url ) || '' === $name || strlen( $name ) > 100 ) { return new WP_Error( 'input', __( 'Choose a compatible form, its HTTPS page on this website, a short name and explicit test permission.', 'submitsignal' ) ); } 253 return array( 'action' => $operation, 'formId' => $form, 'pageUrl' => $url, 'name' => $name, 'consent' => true, 'intervalHours' => 24 ); 271 $forms = SubmitSignal_Plugin::forms(); 272 if ( '' === $name ) { foreach ( $forms as $item ) { if ( $item['id'] === $form ) { $name = substr( sanitize_text_field( $item['name'] ), 0, 100 ); } } } 273 if ( '1' !== self::input( 'consent' ) || ! in_array( $form, array_column( $forms, 'id' ), true ) || ! self::page_url( $url ) || '' === $name || strlen( $name ) > 100 ) { return new WP_Error( 'input', __( 'Choose a compatible form, its HTTPS page on this website and explicit test permission.', 'submitsignal' ) ); } 274 if ( 'check_form' === $operation && empty( $view['data']['capabilities']['browser'] ) ) { return new WP_Error( 'capability', __( 'Browser checks are unavailable. You can create a paused monitor without running it.', 'submitsignal' ) ); } 275 if ( 'check_form' === $operation ) { foreach ( isset( $view['data']['monitors'] ) ? $view['data']['monitors'] : array() as $item ) { if ( isset( $item['formId'] ) && $item['formId'] === $form && ! empty( $item['submissionReview'] ) ) { return new WP_Error( 'consent', __( 'This form has an uncertain submission. Review its existing attempt before any further inquiry.', 'submitsignal' ) ); } } } 276 return array( 'action' => 'create_monitor', 'formId' => $form, 'pageUrl' => self::canonical_page_url( $url ), 'name' => $name, 'consent' => true, 'intervalHours' => 24 ); 254 277 } 255 278 if ( in_array( $operation, array( 'run_monitor', 'pause_monitor', 'review_monitor' ), true ) ) { … … 272 295 if ( 'checkout' === $operation ) { 273 296 $plan = self::input( 'plan' ); 274 if ( ! in_array( $plan, array( 'solo', 'agency' ), true ) || '1' !== self::input( 'commercial_consent' ) || self::TERMS !== self::input( 'terms_version' ) || '1' !== self::input( 'us_business' ) || '1' !== self::input( 'us_billing' ) || '1' !== self::input( 'supported_use' ) ) { return new WP_Error( 'consent', __( 'Choose a plan and explicitly confirm the current terms, US business, US billing and supported use.', 'submitsignal' ) ); } 297 $eligible = '1' === self::input( 'eligibility_consent' ) || ( '1' === self::input( 'us_business' ) && '1' === self::input( 'us_billing' ) && '1' === self::input( 'supported_use' ) ); 298 if ( ! in_array( $plan, array( 'solo', 'agency' ), true ) || '1' !== self::input( 'commercial_consent' ) || self::TERMS !== self::input( 'terms_version' ) || ! $eligible ) { return new WP_Error( 'consent', __( 'Choose a plan and explicitly confirm the current terms, US business, US billing and supported use.', 'submitsignal' ) ); } 275 299 $body['plan'] = $plan; $body['consent'] = array( 'accepted' => true, 'version' => self::TERMS, 'plan' => $plan ); 276 300 $body['eligibility'] = array( 'businessCountry' => 'US', 'billingCountry' => 'US', 'supportedUse' => true ); … … 287 311 public static function handle() { 288 312 self::guard(); $operation = self::input( 'operation' ); 289 $allowed = array( 'start', 'retry_authorization', 'retry_claim', 'cancel_authorization', 'refresh_inventory', 'status', 'create_monitor', ' run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' );313 $allowed = array( 'start', 'retry_authorization', 'retry_claim', 'cancel_authorization', 'refresh_inventory', 'status', 'create_monitor', 'check_form', 'run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' ); 290 314 if ( ! in_array( $operation, $allowed, true ) ) { wp_die( esc_html__( 'Unknown action.', 'submitsignal' ), '', array( 'response' => 400 ) ); } 291 315 check_admin_referer( 'submitsignal_native_' . $operation ); … … 296 320 wp_redirect( $result ); exit; // Exact canonical service path validated above. 297 321 } 298 if ( 'retry_claim' === $operation ) { $pending = self::pending(); $result = $pending ? self::claim( $pending ) : new WP_Error( 'expired', 'The approval expired. Start a new connection deliberately.' ); self::notice( is_wp_error( $result ) ? $result->get_error_message() : 'Website connected. Refresh status to choose a form.'); }322 if ( 'retry_claim' === $operation ) { $pending = self::pending(); $result = $pending ? self::claim( $pending ) : new WP_Error( 'expired', 'The approval expired. Start a new connection deliberately.' ); self::notice( is_wp_error( $result ) ? $result->get_error_message() : self::connected_notice() ); } 299 323 if ( 'refresh_inventory' === $operation ) { 300 324 if ( get_transient( 'submitsignal_native_inventory_lock' ) ) { self::notice( __( 'Wait one minute before refreshing inventory again.', 'submitsignal' ) ); } … … 338 362 return $result; 339 363 } 364 if ( 'check_form' === $operation ) { 365 $monitor = isset( $result['monitor'] ) && is_array( $result['monitor'] ) ? $result['monitor'] : array(); 366 $already_known = false; foreach ( isset( $view['data']['monitors'] ) ? $view['data']['monitors'] : array() as $item ) { if ( isset( $item['id'], $monitor['id'] ) && $item['id'] === $monitor['id'] ) { $already_known = true; } } 367 if ( $already_known || empty( $monitor['id'] ) || ! self::id( $monitor['id'] ) || ! isset( $monitor['siteId'], $monitor['formId'], $monitor['pageUrl'], $monitor['paused'] ) || $monitor['siteId'] !== $settings['site_id'] || $monitor['formId'] !== $body['formId'] || $monitor['pageUrl'] !== $body['pageUrl'] || true !== $monitor['paused'] || ! empty( $monitor['lastRunAt'] ) || ! empty( $monitor['submissionReview'] ) ) { return new WP_Error( 'native_uncertain', __( 'The created monitor could not be verified. No check was requested. Refresh status and review the existing monitor; do not recreate it.', 'submitsignal' ) ); } 368 // The consumed attempt and action lease span both calls. Persist a 369 // second fence before the one authorized run; never resume this 370 // sequence after a timeout, process interruption or rejected create. 371 $view['one_check'] = array( 'monitor_id' => $monitor['id'], 'run_attempted' => true ); 372 update_option( self::VIEW, $view, false ); 373 if ( get_option( self::VIEW ) !== $view ) { return new WP_Error( 'storage', __( 'The paused monitor was created, but its check could not be safely recorded. No check was requested. Refresh status before an explicit decision.', 'submitsignal' ) ); } 374 $result = self::api( '/api/plugin/native/manage', array( 'action' => 'run_monitor', 'monitorId' => $monitor['id'] ), $settings['management_token'] ); 375 if ( is_wp_error( $result ) ) { self::rejected_grant( $result ); return $result; } 376 $run = isset( $result['run'] ) && is_array( $result['run'] ) ? $result['run'] : array(); 377 if ( empty( $run['id'] ) || ! self::id( $run['id'] ) || ! isset( $run['siteId'], $run['monitorId'] ) || $run['siteId'] !== $settings['site_id'] || $run['monitorId'] !== $monitor['id'] ) { return new WP_Error( 'native_uncertain', __( 'The check outcome could not be verified. Refresh status and review this attempt before any further submission.', 'submitsignal' ) ); } 378 } 340 379 if ( in_array( $operation, array( 'checkout', 'billing_portal' ), true ) ) { 341 380 if ( empty( $result['url'] ) || ! self::billing_url( $result['url'], 'billing_portal' === $operation ) ) { $view['site_id'] = $settings['site_id']; $view['uncertain'] = true; update_option( self::VIEW, $view, false ); return new WP_Error( 'destination', __( 'The billing destination could not be verified. Refresh status before another attempt.', 'submitsignal' ) ); } … … 351 390 echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="margin:12px 0"><input type="hidden" name="action" value="submitsignal_native_action"><input type="hidden" name="operation" value="' . esc_attr( $operation ) . '">'; 352 391 wp_nonce_field( 'submitsignal_native_' . $operation ); 353 if ( in_array( $operation, array( 'create_monitor', ' run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' ), true ) ) { self::hidden( 'native_attempt', self::$render_attempt ); }392 if ( in_array( $operation, array( 'create_monitor', 'check_form', 'run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' ), true ) ) { self::hidden( 'native_attempt', self::$render_attempt ); } 354 393 } 355 394 private static function hidden( $name, $value ) { echo '<input type="hidden" name="' . esc_attr( $name ) . '" value="' . esc_attr( $value ) . '">'; } … … 379 418 return $fallback ? 'Other check: ' . $fallback : 'Other check'; 380 419 } 381 private static function evidence( $run ) { 420 private static function evidence( $run, $show_daily_offer = false ) { 421 $passed = isset( $run['status'] ) && 'passed' === $run['status']; 382 422 echo '<p><strong>' . esc_html( isset( $run['status'] ) ? $run['status'] : 'unverified' ) . '</strong> · ' . esc_html( isset( $run['startedAt'] ) ? $run['startedAt'] : '' ) . '</p>'; 423 if ( $passed && $show_daily_offer ) { echo '<p><a class="button button-primary" href="#submitsignal-daily-monitoring">' . esc_html__( 'Review daily monitoring — $19/month plus tax', 'submitsignal' ) . '</a></p>'; } 383 424 if ( ! empty( $run['summary'] ) && is_string( $run['summary'] ) ) { echo '<p>' . esc_html( substr( sanitize_text_field( $run['summary'] ), 0, 1000 ) ) . '</p>'; } 384 425 if ( ! empty( $run['stages'] ) && is_array( $run['stages'] ) ) { 426 $stages = array_slice( $run['stages'], 0, 15 ); 427 if ( $passed ) { 428 $successful = array(); $attention = array(); 429 foreach ( $stages as $stage ) { if ( is_array( $stage ) && isset( $stage['status'] ) && 'passed' === $stage['status'] ) { $successful[] = $stage; } else { $attention[] = $stage; } } 430 if ( $successful ) { echo '<details><summary>' . esc_html__( 'View passed-check evidence', 'submitsignal' ) . '</summary>'; self::evidence_stages( $successful ); echo '</details>'; } 431 // Unverified inbox/CRM results and any contradictory failure 432 // remain visible; a passed browser result never upgrades them. 433 if ( $attention ) { self::evidence_stages( $attention ); } 434 } else { self::evidence_stages( $stages ); } 435 } 436 } 437 private static function evidence_stages( $stages ) { 385 438 echo '<ul>'; 386 foreach ( array_slice( $run['stages'], 0, 15 )as $stage ) {439 foreach ( $stages as $stage ) { 387 440 if ( ! is_array( $stage ) ) { continue; } 388 441 $status = isset( $stage['status'] ) && is_string( $stage['status'] ) && in_array( $stage['status'], array( 'passed', 'failed', 'unverified', 'inconclusive' ), true ) ? $stage['status'] : 'unverified'; … … 392 445 } 393 446 echo '</ul>'; 447 } 448 private static function block_form_ids( $blocks, &$ids, &$remaining, $depth = 0 ) { 449 if ( $depth > 20 ) { return; } 450 foreach ( $blocks as $block ) { 451 if ( --$remaining < 0 ) { return; } 452 if ( isset( $block['blockName'], $block['attrs']['formId'] ) && 'wpforms/form-selector' === $block['blockName'] && is_scalar( $block['attrs']['formId'] ) ) { $ids[] = (string) $block['attrs']['formId']; } 453 if ( ! empty( $block['innerBlocks'] ) ) { self::block_form_ids( $block['innerBlocks'], $ids, $remaining, $depth + 1 ); } 454 } 455 } 456 /** Local suggestions only. They neither fetch a page nor change inventory. */ 457 public static function page_suggestions( $forms ) { 458 $suggestions = array(); foreach ( $forms as $form ) { $suggestions[ $form['id'] ] = array(); } 459 if ( ! $suggestions ) { return $suggestions; } 460 $posts = get_posts( array( 'post_type' => array( 'page', 'post' ), 'post_status' => 'publish', 'has_password' => false, 'numberposts' => 100, 'orderby' => 'ID', 'order' => 'DESC', 'suppress_filters' => true ) ); 461 $pattern = '/' . get_shortcode_regex( array( 'wpforms' ) ) . '/s'; 462 foreach ( $posts as $post ) { 463 if ( 'publish' !== $post->post_status || $post->post_password || ! is_post_publicly_viewable( $post ) || strlen( $post->post_content ) > 262144 ) { continue; } 464 $url = get_permalink( $post ); if ( ! self::page_url( $url ) ) { continue; } 465 $ids = array(); $remaining = 500; 466 self::block_form_ids( parse_blocks( $post->post_content ), $ids, $remaining ); 467 if ( preg_match_all( $pattern, $post->post_content, $matches, PREG_SET_ORDER ) ) { 468 foreach ( $matches as $match ) { 469 if ( ! empty( $match[1] ) || ! empty( $match[6] ) ) { continue; } 470 $attributes = shortcode_parse_atts( $match[3] ); 471 if ( is_array( $attributes ) && isset( $attributes['id'] ) && is_string( $attributes['id'] ) ) { $ids[] = $attributes['id']; } 472 } 473 } 474 foreach ( array_unique( $ids ) as $id ) { 475 if ( isset( $suggestions[ $id ] ) && count( $suggestions[ $id ] ) < 10 ) { $suggestions[ $id ][] = array( 'url' => $url, 'title' => substr( sanitize_text_field( get_the_title( $post ) ), 0, 100 ) ); } 476 } 477 } 478 return $suggestions; 479 } 480 private static function setup_fields( $forms, $pages, $operation ) { 481 $single = 1 === count( $forms ) ? $forms[0] : null; 482 $choices = $single && isset( $pages[ $single['id'] ] ) ? $pages[ $single['id'] ] : array(); 483 $url = 1 === count( $choices ) ? $choices[0]['url'] : ''; 484 $list = 'submitsignal-pages-' . $operation; 485 echo '<div data-ss-setup><p><label>' . esc_html__( 'Contact form', 'submitsignal' ) . ' <select name="form_id" data-ss-form required><option value="">' . esc_html__( 'Choose your form', 'submitsignal' ) . '</option>'; 486 foreach ( $forms as $form ) { echo '<option value="' . esc_attr( $form['id'] ) . '" ' . selected( $single ? $single['id'] : '', $form['id'], false ) . '>' . esc_html( $form['name'] . ' (#' . $form['id'] . ')' ) . '</option>'; } 487 echo '</select></label></p><p><label>' . esc_html__( 'Page containing this form', 'submitsignal' ) . ' <input class="regular-text" data-ss-url name="page_url" type="url" list="' . esc_attr( $list ) . '" value="' . esc_attr( $url ) . '" maxlength="2048" required></label><datalist id="' . esc_attr( $list ) . '">'; 488 foreach ( $choices as $choice ) { echo '<option value="' . esc_attr( $choice['url'] ) . '">' . esc_html( $choice['title'] ) . '</option>'; } 489 echo '</datalist></p><p>' . esc_html__( 'Confirm the suggested public page, choose another suggestion, or paste its HTTPS URL. Theme-built and dynamically embedded forms may need a manual URL. Nothing is submitted until you choose an action below.', 'submitsignal' ) . '</p><details><summary>' . esc_html__( 'Monitor name (optional)', 'submitsignal' ) . '</summary><p><label>' . esc_html__( 'Name', 'submitsignal' ) . ' <input class="regular-text" data-ss-name name="name" maxlength="100" value="' . esc_attr( $single ? $single['name'] : '' ) . '"></label></p></details></div>'; 490 } 491 private static function setup_defaults_script( $forms, $pages ) { 492 $names = array(); foreach ( $forms as $form ) { $names[ $form['id'] ] = $form['name']; } 493 $flags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT; 494 echo '<script>(function(){var names=' . wp_json_encode( $names, $flags ) . ',pages=' . wp_json_encode( $pages, $flags ) . ';document.querySelectorAll("[data-ss-setup]").forEach(function(group){var form=group.querySelector("[data-ss-form]"),url=group.querySelector("[data-ss-url]"),name=group.querySelector("[data-ss-name]"),list=document.getElementById(url.getAttribute("list")),lastName=name.value;form.addEventListener("change",function(){var choices=pages[form.value]||[];if(name.value===lastName||!name.value){name.value=(names[form.value]||"").slice(0,100);}lastName=(names[form.value]||"").slice(0,100);url.value=choices.length===1?choices[0].url:"";while(list.firstChild){list.removeChild(list.firstChild);}choices.forEach(function(choice){var option=document.createElement("option");option.value=choice.url;option.label=choice.title;list.appendChild(option);});});});})();</script>'; 495 } 496 private static function checkout_form( $plan ) { 497 $label = 'agency' === $plan ? 'Agency' : 'Solo'; $price = 'agency' === $plan ? '99' : '19'; 498 self::open_form( 'checkout' ); self::hidden( 'plan', $plan ); self::hidden( 'terms_version', self::TERMS ); 499 self::checkbox( 'eligibility_consent', __( 'This is a US business with US billing. I will use only supported ordinary WPForms Lite contact forms that I own or am authorized to test.', 'submitsignal' ) ); 500 echo '<p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Read the current terms', 'submitsignal' ) . '</a> · ' . esc_html( self::TERMS ) . '</p>'; 501 self::checkbox( 'commercial_consent', sprintf( __( 'I accept the current terms and %1$s at US$%2$s per month plus applicable tax, renewing monthly until canceled. I will review the total and complete payment on Stripe.', 'submitsignal' ), $label, $price ) ); 502 self::close_form( sprintf( __( 'Continue with %1$s — $%2$s/month', 'submitsignal' ), $label, $price ), 'solo' === $plan ); 503 } 504 /** Reconcile a checkout return before WordPress emits administrator headers. */ 505 public static function checkout_return() { 506 if ( ! current_user_can( 'manage_options' ) || ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] || 'submitsignal' !== self::input( 'page', true ) || ! self::management() ) { return; } 507 $checkout_return = self::input( 'checkout', true ); 508 if ( in_array( $checkout_return, array( 'success', 'canceled' ), true ) ) { 509 $result = self::manage( 'status' ); 510 self::notice( is_wp_error( $result ) ? __( 'Returned from checkout, but current status is unavailable. Refresh status before another action. Returning never confirms payment.', 'submitsignal' ) : __( 'Returned from checkout. Your verified plan is shown below; the return link itself is not payment proof. Daily monitoring stays paused until you explicitly enable it.', 'submitsignal' ) ); 394 511 } 395 512 } … … 404 521 } 405 522 $notice = get_transient( 'submitsignal_notice_' . get_current_user_id() ); delete_transient( 'submitsignal_notice_' . get_current_user_id() ); 406 echo '<div class="wrap"><h1>SubmitSignal</h1><p>' . esc_html__( ' Set up and review contact-form checks here in WordPress. Connecting does not run a test or start a paid plan.', 'submitsignal' ) . '</p>';523 echo '<div class="wrap"><h1>SubmitSignal</h1><p>' . esc_html__( 'Connect your account, check one form for free, then choose whether to monitor it daily. No card is needed for your first check.', 'submitsignal' ) . '</p>'; 407 524 if ( is_string( $notice ) && $notice ) { echo '<div class="notice notice-info"><p>' . esc_html( $notice ) . '</p></div>'; } 408 525 if ( $pending ) { … … 412 529 self::open_form( 'cancel_authorization' ); self::close_form( __( 'Cancel local pending approval', 'submitsignal' ) ); 413 530 } elseif ( ! $management ) { 414 echo '<h2>' . esc_html( $settings ? __( ' Enable WordPress controls', 'submitsignal' ) : __( 'Connect your free account', 'submitsignal' ) ) . '</h2>';531 echo '<h2>' . esc_html( $settings ? __( '1. Enable WordPress controls', 'submitsignal' ) : __( '1. Connect your free account', 'submitsignal' ) ) . '</h2>'; 415 532 echo '<p>' . esc_html__( 'Continue to SubmitSignal, sign in, approve this website, then return here. No pairing-code copying is needed. Existing metadata connections require this separate approval before WordPress can manage checks or billing.', 'submitsignal' ) . '</p>'; 416 533 self::open_form( 'start' ); … … 418 535 self::close_form( $settings ? __( 'Enable WordPress controls', 'submitsignal' ) : __( 'Connect SubmitSignal', 'submitsignal' ), true ); 419 536 } else { 420 echo '<h2>' . esc_html__( ' Connected — WordPress controls enabled', 'submitsignal' ) . '</h2><p>' . esc_html__( 'Credentials are private server-side options. This page does not poll, submit forms or purchase anything automatically.', 'submitsignal' ) . '</p>';421 if ( empty( $view['data'] ) ) { echo '<p><strong>' . esc_html__( ' Next: choose Refresh status to load your form controls and current plan.', 'submitsignal' ) . '</strong> ' . esc_html__( 'Then select Form, enter Monitor name and Public HTTPS page containing this form, accept the test permission and choose Create paused monitor. No test runs until you separately authorize Run first check.', 'submitsignal' ) . '</p>'; }537 echo '<h2>' . esc_html__( '1. Connected — WordPress controls enabled', 'submitsignal' ) . '</h2>'; 538 if ( empty( $view['data'] ) ) { echo '<p><strong>' . esc_html__( 'Current status could not be loaded. Choose Refresh status to continue.', 'submitsignal' ) . '</strong> ' . esc_html__( 'Your connection is saved. Do not reconnect or repeat a check to load status.', 'submitsignal' ) . '</p>'; } 422 539 self::open_form( 'status' ); self::close_form( __( 'Refresh status', 'submitsignal' ), true ); 423 540 if ( ! empty( $view['observed_at'] ) ) { echo '<p>' . esc_html( 'Last explicit status refresh: ' . gmdate( 'Y-m-d H:i:s', $view['observed_at'] ) . ' UTC. This is a saved snapshot.' ) . '</p>'; } … … 425 542 if ( ! empty( $view['data'] ) ) { self::controls( $view['data'], $forms, ! empty( $view['uncertain'] ) ); } 426 543 } 427 echo '< h2>' . esc_html__( 'Compatible form inventory', 'submitsignal' ) . '</h2>';544 echo '<details><summary>' . esc_html__( 'Form inventory and advanced settings', 'submitsignal' ) . '</summary><h2>' . esc_html__( 'Compatible form inventory', 'submitsignal' ) . '</h2>'; 428 545 if ( ! function_exists( 'wpforms' ) ) { echo '<p>' . esc_html__( 'WPForms is not active. Activate WPForms Lite, then create and publish an ordinary contact form.', 'submitsignal' ) . '</p>'; } 429 546 elseif ( ! $forms ) { $published = get_posts( array( 'post_type' => 'wpforms', 'post_status' => 'publish', 'numberposts' => 1, 'fields' => 'ids' ) ); echo '<p>' . esc_html( $published ? __( 'Published WPForms forms were found, but their fields or settings are outside supported coverage. Use ordinary name, email, text and paragraph fields; do not remove CAPTCHA or other protections to force a test.', 'submitsignal' ) : __( 'No published WPForms forms were found. Create and publish an ordinary contact form, then refresh inventory.', 'submitsignal' ) ) . '</p>'; } … … 432 549 if ( $settings ) { self::receipt_controls( $settings ); } 433 550 if ( ! $settings && ! $pending ) { self::legacy_form(); } 434 echo '< p>' . esc_html__( 'Launch coverage is simple public WPForms Lite forms. CAPTCHA, custom validation and unsupported fields can make a check inconclusive. WordPress mail handoff does not prove customer-inbox, spam-folder or CRM delivery. Existing email and CRM automations may still run for labeled tests.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/start' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Setup guide', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard?setup=wordpress' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/privacy' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Privacy', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Terms', 'submitsignal' ) . '</a></p></div>';551 echo '</details><p>' . esc_html__( 'Launch coverage is simple public WPForms Lite forms. CAPTCHA, custom validation and unsupported fields can make a check inconclusive. WordPress mail handoff does not prove customer-inbox, spam-folder or CRM delivery. Existing email and CRM automations may still run for labeled tests.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/start' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Setup guide', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard?setup=wordpress' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/privacy' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Privacy', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Terms', 'submitsignal' ) . '</a></p></div>'; 435 552 } 436 553 private static function controls( $data, $forms, $uncertain ) { 437 echo '<p>' . esc_html( 'Plan: ' . ( isset( $data['account']['plan'] ) ? $data['account']['plan'] : 'unknown' ) . ' · Billing: ' . ( isset( $data['account']['billingStatus'] ) ? $data['account']['billingStatus'] : 'unknown' ) . ' · Browser checks: ' . ( ! empty( $data['capabilities']['browser'] ) ? 'available' : 'unavailable' ) . ' · Daily scheduler: ' . ( ! empty( $data['capabilities']['scheduler'] ) ? 'available' : 'unavailable' ) ) . '</p>'; 438 echo '<p>' . esc_html__( 'Free accounts have one website, one monitor and up to three manual checks per day. Daily monitoring and failure emails require a paid plan and available service capabilities. A checkout return alone does not confirm payment; refresh verified status.', 'submitsignal' ) . '</p>'; 554 echo '<section style="max-width:760px;padding:16px;background:#fff;border:1px solid #dcdcde;margin:16px 0"><h2>' . esc_html__( '2. Check one form for free', 'submitsignal' ) . '</h2><p>' . esc_html__( 'Choose the form and public page. One explicit permission creates a paused monitor and requests exactly one labeled check. It does not start daily monitoring or take payment.', 'submitsignal' ) . '</p>'; 555 if ( empty( $data['capabilities']['browser'] ) ) { echo '<p role="status">' . esc_html__( 'Browser checks are currently unavailable. You can save a paused monitor and return later; no inquiry will be submitted now.', 'submitsignal' ) . '</p>'; } 556 $pages = self::page_suggestions( $forms ); 439 557 foreach ( array_slice( $data['monitors'], 0, 100 ) as $monitor ) { 440 558 if ( empty( $monitor['id'] ) || ! self::id( $monitor['id'] ) ) { continue; } 441 559 echo '<hr><h3>' . esc_html( isset( $monitor['name'] ) ? $monitor['name'] : 'Form monitor' ) . '</h3><p>' . esc_html( ! empty( $monitor['paused'] ) ? 'Daily schedule paused' : 'Daily schedule enabled; paid access and scheduler availability still apply' ) . '</p>'; 442 560 $latest = null; foreach ( $data['runs'] as $run ) { if ( isset( $run['monitorId'] ) && $run['monitorId'] === $monitor['id'] && ( ! $latest || strcmp( (string) $run['startedAt'], (string) $latest['startedAt'] ) > 0 ) ) { $latest = $run; } } 443 if ( $latest ) { self::evidence( $latest ); }561 if ( $latest ) { self::evidence( $latest, empty( $data['account']['hasExistingSubscription'] ) ); } 444 562 $review = isset( $monitor['submissionReview'] ) ? $monitor['submissionReview'] : null; 445 563 if ( $review ) { … … 447 565 if ( ! $uncertain && isset( $review['state'], $review['runId'] ) && 'needs_review' === $review['state'] ) { self::open_form( 'review_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::hidden( 'run_id', $review['runId'] ); self::checkbox( 'acknowledgement', __( 'I reviewed this exact attempt and its possible downstream effects. I authorize clearing its hold without submitting another inquiry.', 'submitsignal' ) ); self::close_form( __( 'Acknowledge reviewed attempt', 'submitsignal' ) ); } 448 566 } elseif ( ! $uncertain && ! empty( $data['capabilities']['browser'] ) && ( ! $latest || 'running' !== $latest['status'] ) ) { 567 $repeat_passed = $latest && 'passed' === $latest['status']; 568 if ( $repeat_passed ) { echo '<details><summary>' . esc_html__( 'Run another manual check', 'submitsignal' ) . '</summary>'; } 449 569 self::open_form( 'run_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::checkbox( 'test_permission', __( 'I authorize one labeled synthetic inquiry on this form. Its existing email and CRM automations may run.', 'submitsignal' ) ); self::close_form( $latest ? __( 'Run one check', 'submitsignal' ) : __( 'Run first check', 'submitsignal' ), true ); 570 if ( $repeat_passed ) { echo '</details>'; } 450 571 } 451 if ( ! $uncertain && ( empty( $monitor['paused'] ) || self::may_resume( $data, $monitor ) ) ) { self::open_form( 'pause_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::hidden( 'paused', empty( $monitor['paused'] ) ? '1' : '0' ); if ( ! empty( $monitor['paused'] ) ) { self::checkbox( 'schedule_permission', __( 'I authorize one labeled check each day while paid access and scheduling are available.', 'submitsignal' ) ); } self::close_form( empty( $monitor['paused'] ) ? __( 'Pause daily monitoring', 'submitsignal' ) : __( 'Resume daily monitoring', 'submitsignal' ) ); }452 572 } 453 573 $free_limit_reached = isset( $data['account']['plan'] ) && 'early_access' === $data['account']['plan'] && count( $data['monitors'] ) >= 1; 454 574 if ( ! $uncertain && $free_limit_reached ) { 455 echo '< hr><h2>' . esc_html__( 'Your free monitor is already set up', 'submitsignal' ) . '</h2><p>' . esc_html__( 'The free plan includes one monitor. Use the existing monitor above instead of creating another.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> — ' . esc_html__( 'If you chose the wrong form or page and have not run a check, delete that untested monitor in the dashboard, return here, choose Refresh status, and recreate it correctly.', 'submitsignal' ) . '</p><p>' . esc_html__( 'Submission-review holds survive deletion. Never delete and recreate a monitor to evade a hold or repeat an uncertain inquiry; review the original attempt and its downstream effects first.', 'submitsignal' ) . '</p>';575 echo '<details><summary>' . esc_html__( 'Free monitor options and recovery', 'submitsignal' ) . '</summary><h2>' . esc_html__( 'Your free monitor is already set up', 'submitsignal' ) . '</h2><p>' . esc_html__( 'The free plan includes one monitor. Use the existing monitor above instead of creating another.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> — ' . esc_html__( 'If you chose the wrong form or page and have not run a check, delete that untested monitor in the dashboard, return here, choose Refresh status, and recreate it correctly.', 'submitsignal' ) . '</p><p>' . esc_html__( 'Submission-review holds survive deletion. Never delete and recreate a monitor to evade a hold or repeat an uncertain inquiry; review the original attempt and its downstream effects first.', 'submitsignal' ) . '</p></details>'; 456 576 } elseif ( ! $uncertain && $forms ) { 457 echo '<hr><h2>' . esc_html__( 'Choose a form for its first check', 'submitsignal' ) . '</h2>'; self::open_form( 'create_monitor' ); 458 echo '<p><label>' . esc_html__( 'Form', 'submitsignal' ) . ' <select name="form_id" required><option value="">' . esc_html__( 'Choose a compatible form', 'submitsignal' ) . '</option>'; foreach ( $forms as $form ) { echo '<option value="' . esc_attr( $form['id'] ) . '">' . esc_html( $form['name'] ) . '</option>'; } echo '</select></label></p><p><label>' . esc_html__( 'Monitor name', 'submitsignal' ) . ' <input class="regular-text" name="name" maxlength="100" required></label></p><p><label>' . esc_html__( 'Public HTTPS page containing this form', 'submitsignal' ) . ' <input class="regular-text" name="page_url" type="url" maxlength="2048" required></label></p>'; 459 self::checkbox( 'consent', __( 'I own or administer this website and authorize labeled synthetic tests of this form, including its existing email and CRM automations. The monitor starts paused; running a check and enabling a daily schedule are separate actions.', 'submitsignal' ) ); self::close_form( __( 'Create paused monitor', 'submitsignal' ), true ); 460 } 461 echo '<hr><h2>' . esc_html__( 'Optional daily monitoring', 'submitsignal' ) . '</h2><p>' . esc_html__( 'Solo: $19/month plus tax. Agency: $99/month plus tax. Paid launch enrollment is limited to US businesses with US billing, subject to current capacity. Payment details are entered on Stripe; WordPress never receives them. Returning here does not by itself verify payment.', 'submitsignal' ) . '</p>'; 577 if ( ! empty( $data['capabilities']['browser'] ) ) { 578 self::open_form( 'check_form' ); self::setup_fields( $forms, $pages, 'check_form' ); 579 self::checkbox( 'consent', __( 'I own or administer this website. Create a paused monitor for the form and page shown above and run exactly one labeled test inquiry now. Its existing email and CRM automations may run. Daily checks and payment need separate permission.', 'submitsignal' ) ); self::close_form( __( 'Check this form', 'submitsignal' ), true ); 580 } 581 echo '<details><summary>' . esc_html__( 'Save a paused monitor without running a check', 'submitsignal' ) . '</summary>'; 582 self::open_form( 'create_monitor' ); self::setup_fields( $forms, $pages, 'create_monitor' ); 583 self::checkbox( 'consent', __( 'I own or administer this website and authorize monitoring setup for this selected form. Save it paused without submitting an inquiry. A check or daily schedule requires a later explicit choice.', 'submitsignal' ) ); self::close_form( __( 'Create paused monitor', 'submitsignal' ) ); echo '</details>'; 584 } 585 self::setup_defaults_script( $forms, $pages ); 586 echo '<p>' . esc_html__( 'Free diagnostics include one website, one monitor and up to three manual checks per day. Read browser submission, WordPress acceptance and email handoff separately; they do not prove inbox or CRM delivery.', 'submitsignal' ) . '</p></section>'; 587 echo '<section id="submitsignal-daily-monitoring" style="max-width:760px;padding:16px;background:#fff;border:1px solid #dcdcde;margin:16px 0"><h2>' . esc_html__( '3. Optional daily monitoring', 'submitsignal' ) . '</h2><p>' . esc_html( 'Plan: ' . ( isset( $data['account']['plan'] ) && 'early_access' !== $data['account']['plan'] ? $data['account']['plan'] : 'Free diagnostics' ) . ' · Billing: ' . ( isset( $data['account']['billingStatus'] ) ? $data['account']['billingStatus'] : 'unknown' ) ) . '</p><p>' . esc_html__( 'We recommend reviewing a successful check before paying. Solo is $19/month plus tax for one website; Agency is $99/month plus tax for up to ten. US business and US billing are required, subject to current launch capacity. Stripe handles payment details. Buying a plan does not enable the schedule.', 'submitsignal' ) . '</p>'; 462 588 if ( ! $uncertain && ! empty( $data['account']['publicCheckout'] ) && ! empty( $data['capabilities']['billing'] ) && empty( $data['account']['hasExistingSubscription'] ) ) { 463 self::open_form( 'checkout' ); echo '<p><label>' . esc_html__( 'Plan', 'submitsignal' ) . ' <select name="plan" required><option value="">' . esc_html__( 'Choose a plan', 'submitsignal' ) . '</option><option value="solo">Solo — $19/month plus tax</option><option value="agency">Agency — $99/month plus tax</option></select></label></p>'; 464 self::hidden( 'terms_version', self::TERMS ); self::checkbox( 'us_business', __( 'This is a US business.', 'submitsignal' ) ); self::checkbox( 'us_billing', __( 'The billing country is the United States.', 'submitsignal' ) ); self::checkbox( 'supported_use', __( 'My use is limited to supported ordinary WPForms Lite contact forms and I have permission to run labeled checks.', 'submitsignal' ) ); 465 echo '<p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Read the current commercial terms', 'submitsignal' ) . '</a> · ' . esc_html( self::TERMS ) . '</p>'; self::checkbox( 'commercial_consent', __( 'I accept the current commercial terms and authorize the selected monthly subscription plus tax, renewing until canceled. I will review the total and complete payment on Stripe.', 'submitsignal' ) ); self::close_form( __( 'Continue to secure checkout', 'submitsignal' ), true ); 589 self::checkout_form( 'solo' ); 590 echo '<details><summary>' . esc_html__( 'Need up to ten websites? Choose Agency — $99/month plus tax', 'submitsignal' ) . '</summary>'; self::checkout_form( 'agency' ); echo '</details>'; 466 591 } 467 592 if ( ! $uncertain && ! empty( $data['account']['canManageBilling'] ) ) { self::open_form( 'billing_portal' ); self::close_form( __( 'Manage billing on Stripe', 'submitsignal' ) ); } 468 if ( ! $uncertain ) { echo '<hr><h2>' . esc_html__( 'Disconnect WordPress controls', 'submitsignal' ) . '</h2>'; self::open_form( 'revoke' ); self::checkbox( 'acknowledgement', __( 'Pause this website’s monitors, revoke these WordPress controls and remove local credentials and queued metadata. Billing and hosted history remain unchanged; this does not cancel a subscription.', 'submitsignal' ) ); self::close_form( __( 'Pause and disconnect this website', 'submitsignal' ) ); } 593 if ( ! $uncertain ) { foreach ( $data['monitors'] as $monitor ) { if ( ! empty( $monitor['id'] ) && self::id( $monitor['id'] ) && ( empty( $monitor['paused'] ) || self::may_resume( $data, $monitor ) ) ) { echo '<p><strong>' . esc_html( isset( $monitor['name'] ) ? $monitor['name'] : 'Form monitor' ) . '</strong></p>'; self::open_form( 'pause_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::hidden( 'paused', empty( $monitor['paused'] ) ? '1' : '0' ); if ( ! empty( $monitor['paused'] ) ) { self::checkbox( 'schedule_permission', __( 'I authorize one labeled check each day while paid access and scheduling are available.', 'submitsignal' ) ); } self::close_form( empty( $monitor['paused'] ) ? __( 'Pause daily monitoring', 'submitsignal' ) : __( 'Resume daily monitoring', 'submitsignal' ) ); } } } 594 echo '</section>'; 595 if ( ! $uncertain ) { echo '<details><summary>' . esc_html__( 'Pause and disconnect WordPress controls', 'submitsignal' ) . '</summary>'; self::open_form( 'revoke' ); self::checkbox( 'acknowledgement', __( 'Pause this website’s monitors, revoke these WordPress controls and remove local credentials and queued metadata. Billing and hosted history remain unchanged; this does not cancel a subscription.', 'submitsignal' ) ); self::close_form( __( 'Pause and disconnect this website', 'submitsignal' ) ); echo '</details>'; } 469 596 } 470 597 private static function receipt_controls( $settings ) { -
submitsignal/tags/0.4.0/readme.txt
r3780656 r3782284 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0. 3.17 Stable tag: 0.4.0 8 8 License: GPL-2.0-or-later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 C onnect ordinary WPForms Lite contact forms to SubmitSignal reliability checks.11 Check a WPForms Lite contact form free from WordPress. Choose optional daily monitoring after reviewing its result. 12 12 13 13 == Description == 14 14 15 Choose a form, review its public page and authorize one clearly labeled inquiry. SubmitSignal shows separate browser submission, WordPress acceptance and email-handoff evidence in WordPress. Start free with one website, one form and up to three manual checks per day. No card is required. 16 17 Version 0.4.0 shortens setup: connected form controls load automatically, known public form pages are suggested locally, monitor names are supplied, and one explicit Check this form action can prepare a paused monitor and run its first authorized check. Daily monitoring and payment remain separate choices. 18 15 19 SubmitSignal connects an administrator-authorized WordPress website to the hosted SubmitSignal monitoring service. It reports metadata about successful WPForms processing and WordPress mail-transport outcomes. The service can submit visibly labeled synthetic contact inquiries to forms the account owner explicitly selects. 16 20 17 This is the official SubmitSignal WordPress connector. SubmitSignal, this plugin and the hosted monitoring service are owned and operated by Axtrel AI LLC. Visit the [official service website](https://getsubmitsignal.com) or [setup guide](https://getsubmitsignal.com/start). This connector uses the canonical service API at https://getsubmitsignal.com. Version 0.3.0 adds administrator-only setup, explicit manual checks, evidence, submission-review controls and billing handoffinside Settings > SubmitSignal.21 This is the official SubmitSignal WordPress connector. SubmitSignal, this plugin and the hosted monitoring service are owned and operated by Axtrel AI LLC. Visit the [official service website](https://getsubmitsignal.com) or [setup guide](https://getsubmitsignal.com/start). This connector uses the canonical service API at https://getsubmitsignal.com. Setup, individually authorized checks, evidence, submission-review controls and the billing handoff are available inside Settings > SubmitSignal. 18 22 19 23 Supported launch coverage is ordinary WPForms Lite contact forms. WPForms Pro is not included in supported launch coverage until acceptance testing with a licensed Pro installation is complete. … … 21 25 The free plugin requires a SubmitSignal service account. The free service tier includes manual diagnostics for one website and one monitor, with up to three checks per day. Scheduled daily monitoring and automated email failure alerts require an active Solo or Agency subscription. Current availability and subscription pricing are shown on the service website. 22 26 23 Paid launch enrollment is limited to US businesses with US billing. Solo monitoring is $19/month plus tax; Agency monitoring is $99/month plus tax. Start with a [free form review](https://getsubmitsignal.com/compatibility). This questionnaire evaluates your answers; it does not visit or submityour form. An actual test requires the website owner's authorization.27 Paid launch enrollment is limited to US businesses with US billing. Solo monitoring is $19/month plus tax; Agency monitoring is $99/month plus tax. Install free and authorize your first check in WordPress. If you are unsure about fit, the optional [free form review](https://getsubmitsignal.com/compatibility) evaluates your answers without visiting or submitting your form. An actual test requires the website owner's authorization. 24 28 25 29 Browser checks require the service's browser runner to be configured, and scheduled monitoring and email alerts also depend on the hosted scheduler and email service. The dashboard shows current availability. Installing this plugin alone does not start synthetic submissions or promise active scheduled checks. … … 47 51 The administrator signs in to SubmitSignal, approves this website and returns to WordPress. Private metadata and management tokens are stored only in non-autoloaded server options; they never appear in HTML, JavaScript or URLs. Existing older connections need a separate new approval to enable WordPress controls. An uncertain response retains the original private request for an explicit retry. 48 52 49 Native management requests are sent only after an administrator action protected by a WordPress nonce. Loading settings makes no service request. Status is a saved snapshot until explicitly refreshed; an accepted management action may perform one bounded status refresh. New native monitors start paused. Creating a monitor, running one labeled check, acknowledging an uncertain prior attempt, resuming daily monitoring and starting paid checkout each require explicit choices. There is no anonymous WordPress telemetry or automatic polling.53 Native management changes require an administrator action protected by a WordPress nonce and single-use action token. After the administrator-approved connection callback, an accepted action, or a checkout return, the plugin may perform one bounded read of verified service status. Ordinary settings visits do not poll or submit forms. A checkout return never proves payment. Public form-page suggestions are read locally from published, non-password-protected WordPress content; only the page selected for monitoring is sent with the authorized setup action. New monitors start paused. Check this form explicitly authorizes preparing that monitor and submitting one labeled inquiry. Acknowledging an uncertain attempt, enabling daily monitoring and starting paid checkout remain separate choices. There is no anonymous WordPress telemetry. 50 54 51 55 While connected, the plugin sends compatible form inventories, heartbeat timestamps, random event IDs, form IDs, form-processing outcomes, mail-transport outcomes and synthetic run references. This default metadata stream excludes entered field values, visitor names, email addresses, recipients, message content, visitor IP addresses and cookies. Separately enabled synthetic notification copies are processed by Resend as described above. The service records only the receiving event ID, test/run identifiers, unique test destination and receipt timestamps; it does not fetch the received message body. The hosted browser checks visit publicly accessible form pages; the hosted service's privacy policy separately describes that processing. … … 57 61 == Installation == 58 62 59 1. Download the current [official connector ZIP](https://getsubmitsignal.com/downloads/submitsignal.zip) and use WordPress > Plugins > Add New Plugin > Upload Plugin. The [WordPress.org listing](https://wordpress.org/plugins/submitsignal/) also offers the latest directory-published release; confirm its version before installing. Check the installed version after updating. 60 2. Activate SubmitSignal. Install and activate WPForms Lite and create an ordinary contact form if needed. WPForms Pro is not yet part of supported launch coverage. 61 3. Open Settings > SubmitSignal, or choose Settings beside SubmitSignal on the Plugins page. Read and accept the connection disclosure, then choose Connect SubmitSignal. Sign in to the service and approve this website; you return to the same WordPress administrator session without copying a code. A separate legacy code option remains available for hosted-dashboard-only control. 62 4. Choose Refresh status to load the controls. Select Form, enter Monitor name and Public HTTPS page containing this form, explicitly authorize labeled tests, then choose Create paused monitor. Creation does not submit the form. The free plan includes one monitor; if you chose the wrong form or page and have not run a check, use Open dashboard to delete that untested monitor, then return, refresh status and recreate it correctly. Submission-review holds survive deletion; never delete and recreate a monitor to evade a hold or repeat an uncertain inquiry. 63 5. Choose Run first check and explicitly authorize one synthetic inquiry. Review browser, WordPress acceptance and mail-handoff evidence. Inspect your real inbox and existing CRM separately. If an attempt may already have submitted, review its downstream effects before acknowledging the hold; acknowledgment does not rerun it. 64 6. Free manual diagnostics cover one website, one monitor and up to three checks per day. For daily monitoring, choose Solo ($19/month plus tax) or Agency ($99/month plus tax), confirm US business/billing and supported use, accept the current commercial terms, and continue to Stripe. Payment details never enter WordPress. Refresh verified service status after returning; the return alone is not proof of payment. Enable a daily schedule only after a useful first check and explicit schedule permission. 65 7. Use Pause or Resume for daily monitoring, and Manage billing when available. Billing and hosted history remain separate from disconnecting WordPress controls. The [setup guide](https://getsubmitsignal.com/start) and [hosted dashboard](https://getsubmitsignal.com/dashboard?setup=wordpress) remain available. 63 1. Install and activate SubmitSignal and WPForms Lite. Open Settings > SubmitSignal, accept the connection disclosure and choose Connect SubmitSignal. Sign in, review the website and account shown, approve and return to the same WordPress administrator session. No pairing code is needed and the form controls load automatically. The [official website ZIP](https://getsubmitsignal.com/downloads/submitsignal.zip) can be installed with Plugins > Add New Plugin > Upload Plugin. 64 2. Review the form and its public HTTPS page. WordPress suggests published pages containing a recognized embed; dynamic or theme embeds may require entering the URL yourself. Authorize one labeled inquiry and choose Check this form. It creates a paused monitor and requests one check; existing email and CRM automations may run. Review each evidence stage. An uncertain submission must be reviewed before any later check; deleting or recreating the monitor does not bypass a hold. 65 3. After a useful first check, choose optional daily monitoring: Solo $19/month plus applicable tax for one website, or Agency $99/month plus applicable tax for up to ten. Confirm US business/billing eligibility and supported use, accept the selected monthly terms, and complete payment securely on Stripe/Link. WordPress reads verified status on return. Choose to enable daily checks separately when paid access is confirmed. Cancel through Manage billing; uninstalling or disconnecting does not cancel a subscription. 66 67 Free diagnostics include one website, one form and up to three manual checks per day. Daily monitoring and automated failure emails require an active paid plan and operational services. See the [short setup guide](https://getsubmitsignal.com/start) or ask for free setup help there. Existing older connections and subscriptions are preserved. Manual pairing remains an advanced option. 66 68 67 69 WordPress must be reachable over HTTPS and expose its public REST API. Install per website on multisite; network-wide configuration is not provided. … … 86 88 == Screenshots == 87 89 88 1. After account approval, refresh status in WordPress. Local demonstration with simulated service responses.89 2. Choose a supported form and public page, then create a paused monitor. Local demonstration; no form submission occurs.90 3. Review separate browser, WordPress and mail-handoff evidence . Local demonstration with simulated results; customer inbox and CRM delivery remain unverified.90 1. Connect from WordPress and approve the account connection. Local demonstration with simulated service responses. 91 2. Review a suggested form page and authorize one first check. Local demonstration with simulated service responses. 92 3. Review separate browser, WordPress and mail-handoff evidence, then choose optional daily monitoring. Local demonstration with simulated results; customer inbox and CRM delivery remain unverified. 91 93 92 94 == Changelog == 95 96 = 0.4.0 = 97 * Shorten setup to connect, check one form and choose optional daily monitoring. 98 * Load verified form controls after approval and read verified status on checkout return, without automatic form tests or purchases. 99 * Suggest recognized public form pages locally and supply monitor names; preserve manual page entry when an embed is unknown. 100 * Combine paused monitor preparation and one explicitly authorized first check, preserving single-use attempts and uncertainty holds. 101 * Simplify billing declarations while retaining US eligibility, supported-use limits, exact-plan monthly terms and separate schedule permission. 102 * Preserve complete Stripe Checkout destinations, including opaque fragment state, without weakening host validation. 93 103 94 104 = 0.3.1 = -
submitsignal/tags/0.4.0/submitsignal.php
r3780656 r3782284 3 3 * Plugin Name: SubmitSignal 4 4 * Description: Connect ordinary WPForms contact forms to SubmitSignal reliability monitoring, with explicit administrator consent. 5 * Version: 0. 3.15 * Version: 0.4.0 6 6 * Requires at least: 6.2 7 7 * Requires PHP: 7.4 … … 16 16 17 17 final class SubmitSignal_Plugin { 18 const VERSION = '0. 3.1';18 const VERSION = '0.4.0'; 19 19 const SERVICE = 'https://getsubmitsignal.com'; 20 20 const SETTINGS = 'submitsignal_settings'; -
submitsignal/trunk/includes/native-admin.php
r3780656 r3782284 13 13 add_action( 'admin_post_submitsignal_native_action', array( __CLASS__, 'handle' ) ); 14 14 add_action( 'admin_post_submitsignal_native_return', array( __CLASS__, 'returned' ) ); 15 add_action( 'admin_init', array( __CLASS__, 'checkout_return' ) ); 15 16 add_action( 'submitsignal_native_expire', array( __CLASS__, 'expire' ) ); 16 17 } … … 131 132 if ( ! is_string( $url ) || strlen( $url ) > 4096 || preg_match( '/[\x00-\x20\x7f\\\\]/', $url ) ) { return false; } 132 133 $parts = wp_parse_url( $url ); 134 // Stripe Checkout can include opaque fragment state. Keep its full URL; 135 // the portal retains its separate host and fragment restrictions. 133 136 return is_array( $parts ) && isset( $parts['scheme'], $parts['host'] ) && 'https' === $parts['scheme'] && 134 137 ( $portal ? 'billing.stripe.com' : 'checkout.stripe.com' ) === $parts['host'] && 135 ! isset( $parts['user'] ) && ! isset( $parts['pass'] ) && ! isset( $parts['port'] ) && ! isset( $parts['fragment']);138 ! isset( $parts['user'] ) && ! isset( $parts['pass'] ) && ! isset( $parts['port'] ) && ( ! $portal || ! isset( $parts['fragment'] ) ); 136 139 } 137 140 public static function page_url( $url ) { 138 141 if ( ! is_string( $url ) || strlen( $url ) > 2048 || preg_match( '/[\x00-\x20\x7f\\\\]/', $url ) ) { return false; } 139 142 $parts = wp_parse_url( $url ); $home = wp_parse_url( home_url( '/' ) ); 140 if ( ! is_array( $parts ) || ! is_array( $home ) || empty( $parts['host'] ) || 'https' !== ( isset( $parts['scheme'] ) ? $parts['scheme'] : '' ) || strtolower( $parts['host'] ) !== strtolower( $home['host']) || isset( $parts['user'] ) || isset( $parts['pass'] ) || isset( $parts['fragment'] ) || isset( $parts['query'] ) || ( isset( $parts['port'] ) ? $parts['port'] : 443 ) !== ( isset( $home['port'] ) ? $home['port'] : 443 ) ) { return false; }143 if ( ! is_array( $parts ) || ! is_array( $home ) || empty( $parts['host'] ) || 'https' !== ( isset( $parts['scheme'] ) ? $parts['scheme'] : '' ) || preg_replace( '/\.$/', '', strtolower( $parts['host'] ) ) !== preg_replace( '/\.$/', '', strtolower( $home['host'] ) ) || isset( $parts['user'] ) || isset( $parts['pass'] ) || isset( $parts['fragment'] ) || isset( $parts['query'] ) || ( isset( $parts['port'] ) ? $parts['port'] : 443 ) !== ( isset( $home['port'] ) ? $home['port'] : 443 ) ) { return false; } 141 144 $base = rtrim( isset( $home['path'] ) ? $home['path'] : '', '/' ); 142 145 $path = isset( $parts['path'] ) ? $parts['path'] : '/'; … … 144 147 if ( preg_match( '#(?:^|/)(?:\.|\.\.)(?:/|$)#', $decoded ) || false !== strpos( $decoded, '\\' ) || preg_match( '/[\x00-\x20\x7f]/', $decoded ) || preg_match( '/%2f|%5c/i', $path ) ) { return false; } 145 148 return '' === $base || $path === $base || 0 === strpos( $path, $base . '/' ); 149 } 150 private static function canonical_page_url( $url ) { 151 // Validate the original first. Match only the service's known origin 152 // normalization; never decode or rewrite the selected page path. 153 if ( ! self::page_url( $url ) ) { return false; } 154 $parts = wp_parse_url( $url ); 155 $host = preg_replace( '/\.$/', '', strtolower( $parts['host'] ) ); 156 $port = isset( $parts['port'] ) && 443 !== $parts['port'] ? ':' . $parts['port'] : ''; 157 return 'https://' . $host . $port . ( ! empty( $parts['path'] ) ? $parts['path'] : '/' ); 146 158 } 147 159 private static function api( $path, $body, $token = '' ) { … … 209 221 if ( get_option( SubmitSignal_Plugin::SETTINGS ) !== $settings ) { return new WP_Error( 'storage', __( 'Approved credentials could not be saved. The private pending request remains for explicit claim retry after WordPress storage is repaired.', 'submitsignal' ) ); } 210 222 delete_option( self::VIEW ); self::clear_pending( self::key() ); SubmitSignal_Plugin::upgrade(); 223 // This single read follows the explicit account approval. A failed read 224 // never repeats the claim or turns a connection into a form test. 225 self::refresh_status( $settings ); 211 226 return true; 227 } 228 private static function connected_notice() { 229 $view = self::cache(); 230 return empty( $view['data'] ) ? __( 'Website connected. Current status is unavailable; use Refresh status. Do not repeat the connection.', 'submitsignal' ) : __( 'Website connected. Choose your form below and authorize its first check.', 'submitsignal' ); 212 231 } 213 232 public static function returned() { … … 218 237 if ( ! empty( $pending['claim_attempted'] ) ) { self::notice( __( 'A claim was already attempted. Use the explicit Retry connection claim button if its outcome is still unknown.', 'submitsignal' ) ); } 219 238 $result = self::claim( $pending ); 220 self::notice( is_wp_error( $result ) ? $result->get_error_message() : __( 'Website connected. Refresh status below, then choose a form and authorize its first check.', 'submitsignal') );239 self::notice( is_wp_error( $result ) ? $result->get_error_message() : self::connected_notice() ); 221 240 } 222 241 private static function cache() { … … 248 267 private static function payload( $operation, $view ) { 249 268 $body = array( 'action' => $operation ); 250 if ( 'create_monitor' === $operation) {269 if ( in_array( $operation, array( 'create_monitor', 'check_form' ), true ) ) { 251 270 $form = self::input( 'form_id' ); $url = self::input( 'page_url' ); $name = sanitize_text_field( self::input( 'name' ) ); 252 if ( '1' !== self::input( 'consent' ) || ! in_array( $form, array_column( SubmitSignal_Plugin::forms(), 'id' ), true ) || ! self::page_url( $url ) || '' === $name || strlen( $name ) > 100 ) { return new WP_Error( 'input', __( 'Choose a compatible form, its HTTPS page on this website, a short name and explicit test permission.', 'submitsignal' ) ); } 253 return array( 'action' => $operation, 'formId' => $form, 'pageUrl' => $url, 'name' => $name, 'consent' => true, 'intervalHours' => 24 ); 271 $forms = SubmitSignal_Plugin::forms(); 272 if ( '' === $name ) { foreach ( $forms as $item ) { if ( $item['id'] === $form ) { $name = substr( sanitize_text_field( $item['name'] ), 0, 100 ); } } } 273 if ( '1' !== self::input( 'consent' ) || ! in_array( $form, array_column( $forms, 'id' ), true ) || ! self::page_url( $url ) || '' === $name || strlen( $name ) > 100 ) { return new WP_Error( 'input', __( 'Choose a compatible form, its HTTPS page on this website and explicit test permission.', 'submitsignal' ) ); } 274 if ( 'check_form' === $operation && empty( $view['data']['capabilities']['browser'] ) ) { return new WP_Error( 'capability', __( 'Browser checks are unavailable. You can create a paused monitor without running it.', 'submitsignal' ) ); } 275 if ( 'check_form' === $operation ) { foreach ( isset( $view['data']['monitors'] ) ? $view['data']['monitors'] : array() as $item ) { if ( isset( $item['formId'] ) && $item['formId'] === $form && ! empty( $item['submissionReview'] ) ) { return new WP_Error( 'consent', __( 'This form has an uncertain submission. Review its existing attempt before any further inquiry.', 'submitsignal' ) ); } } } 276 return array( 'action' => 'create_monitor', 'formId' => $form, 'pageUrl' => self::canonical_page_url( $url ), 'name' => $name, 'consent' => true, 'intervalHours' => 24 ); 254 277 } 255 278 if ( in_array( $operation, array( 'run_monitor', 'pause_monitor', 'review_monitor' ), true ) ) { … … 272 295 if ( 'checkout' === $operation ) { 273 296 $plan = self::input( 'plan' ); 274 if ( ! in_array( $plan, array( 'solo', 'agency' ), true ) || '1' !== self::input( 'commercial_consent' ) || self::TERMS !== self::input( 'terms_version' ) || '1' !== self::input( 'us_business' ) || '1' !== self::input( 'us_billing' ) || '1' !== self::input( 'supported_use' ) ) { return new WP_Error( 'consent', __( 'Choose a plan and explicitly confirm the current terms, US business, US billing and supported use.', 'submitsignal' ) ); } 297 $eligible = '1' === self::input( 'eligibility_consent' ) || ( '1' === self::input( 'us_business' ) && '1' === self::input( 'us_billing' ) && '1' === self::input( 'supported_use' ) ); 298 if ( ! in_array( $plan, array( 'solo', 'agency' ), true ) || '1' !== self::input( 'commercial_consent' ) || self::TERMS !== self::input( 'terms_version' ) || ! $eligible ) { return new WP_Error( 'consent', __( 'Choose a plan and explicitly confirm the current terms, US business, US billing and supported use.', 'submitsignal' ) ); } 275 299 $body['plan'] = $plan; $body['consent'] = array( 'accepted' => true, 'version' => self::TERMS, 'plan' => $plan ); 276 300 $body['eligibility'] = array( 'businessCountry' => 'US', 'billingCountry' => 'US', 'supportedUse' => true ); … … 287 311 public static function handle() { 288 312 self::guard(); $operation = self::input( 'operation' ); 289 $allowed = array( 'start', 'retry_authorization', 'retry_claim', 'cancel_authorization', 'refresh_inventory', 'status', 'create_monitor', ' run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' );313 $allowed = array( 'start', 'retry_authorization', 'retry_claim', 'cancel_authorization', 'refresh_inventory', 'status', 'create_monitor', 'check_form', 'run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' ); 290 314 if ( ! in_array( $operation, $allowed, true ) ) { wp_die( esc_html__( 'Unknown action.', 'submitsignal' ), '', array( 'response' => 400 ) ); } 291 315 check_admin_referer( 'submitsignal_native_' . $operation ); … … 296 320 wp_redirect( $result ); exit; // Exact canonical service path validated above. 297 321 } 298 if ( 'retry_claim' === $operation ) { $pending = self::pending(); $result = $pending ? self::claim( $pending ) : new WP_Error( 'expired', 'The approval expired. Start a new connection deliberately.' ); self::notice( is_wp_error( $result ) ? $result->get_error_message() : 'Website connected. Refresh status to choose a form.'); }322 if ( 'retry_claim' === $operation ) { $pending = self::pending(); $result = $pending ? self::claim( $pending ) : new WP_Error( 'expired', 'The approval expired. Start a new connection deliberately.' ); self::notice( is_wp_error( $result ) ? $result->get_error_message() : self::connected_notice() ); } 299 323 if ( 'refresh_inventory' === $operation ) { 300 324 if ( get_transient( 'submitsignal_native_inventory_lock' ) ) { self::notice( __( 'Wait one minute before refreshing inventory again.', 'submitsignal' ) ); } … … 338 362 return $result; 339 363 } 364 if ( 'check_form' === $operation ) { 365 $monitor = isset( $result['monitor'] ) && is_array( $result['monitor'] ) ? $result['monitor'] : array(); 366 $already_known = false; foreach ( isset( $view['data']['monitors'] ) ? $view['data']['monitors'] : array() as $item ) { if ( isset( $item['id'], $monitor['id'] ) && $item['id'] === $monitor['id'] ) { $already_known = true; } } 367 if ( $already_known || empty( $monitor['id'] ) || ! self::id( $monitor['id'] ) || ! isset( $monitor['siteId'], $monitor['formId'], $monitor['pageUrl'], $monitor['paused'] ) || $monitor['siteId'] !== $settings['site_id'] || $monitor['formId'] !== $body['formId'] || $monitor['pageUrl'] !== $body['pageUrl'] || true !== $monitor['paused'] || ! empty( $monitor['lastRunAt'] ) || ! empty( $monitor['submissionReview'] ) ) { return new WP_Error( 'native_uncertain', __( 'The created monitor could not be verified. No check was requested. Refresh status and review the existing monitor; do not recreate it.', 'submitsignal' ) ); } 368 // The consumed attempt and action lease span both calls. Persist a 369 // second fence before the one authorized run; never resume this 370 // sequence after a timeout, process interruption or rejected create. 371 $view['one_check'] = array( 'monitor_id' => $monitor['id'], 'run_attempted' => true ); 372 update_option( self::VIEW, $view, false ); 373 if ( get_option( self::VIEW ) !== $view ) { return new WP_Error( 'storage', __( 'The paused monitor was created, but its check could not be safely recorded. No check was requested. Refresh status before an explicit decision.', 'submitsignal' ) ); } 374 $result = self::api( '/api/plugin/native/manage', array( 'action' => 'run_monitor', 'monitorId' => $monitor['id'] ), $settings['management_token'] ); 375 if ( is_wp_error( $result ) ) { self::rejected_grant( $result ); return $result; } 376 $run = isset( $result['run'] ) && is_array( $result['run'] ) ? $result['run'] : array(); 377 if ( empty( $run['id'] ) || ! self::id( $run['id'] ) || ! isset( $run['siteId'], $run['monitorId'] ) || $run['siteId'] !== $settings['site_id'] || $run['monitorId'] !== $monitor['id'] ) { return new WP_Error( 'native_uncertain', __( 'The check outcome could not be verified. Refresh status and review this attempt before any further submission.', 'submitsignal' ) ); } 378 } 340 379 if ( in_array( $operation, array( 'checkout', 'billing_portal' ), true ) ) { 341 380 if ( empty( $result['url'] ) || ! self::billing_url( $result['url'], 'billing_portal' === $operation ) ) { $view['site_id'] = $settings['site_id']; $view['uncertain'] = true; update_option( self::VIEW, $view, false ); return new WP_Error( 'destination', __( 'The billing destination could not be verified. Refresh status before another attempt.', 'submitsignal' ) ); } … … 351 390 echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="margin:12px 0"><input type="hidden" name="action" value="submitsignal_native_action"><input type="hidden" name="operation" value="' . esc_attr( $operation ) . '">'; 352 391 wp_nonce_field( 'submitsignal_native_' . $operation ); 353 if ( in_array( $operation, array( 'create_monitor', ' run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' ), true ) ) { self::hidden( 'native_attempt', self::$render_attempt ); }392 if ( in_array( $operation, array( 'create_monitor', 'check_form', 'run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' ), true ) ) { self::hidden( 'native_attempt', self::$render_attempt ); } 354 393 } 355 394 private static function hidden( $name, $value ) { echo '<input type="hidden" name="' . esc_attr( $name ) . '" value="' . esc_attr( $value ) . '">'; } … … 379 418 return $fallback ? 'Other check: ' . $fallback : 'Other check'; 380 419 } 381 private static function evidence( $run ) { 420 private static function evidence( $run, $show_daily_offer = false ) { 421 $passed = isset( $run['status'] ) && 'passed' === $run['status']; 382 422 echo '<p><strong>' . esc_html( isset( $run['status'] ) ? $run['status'] : 'unverified' ) . '</strong> · ' . esc_html( isset( $run['startedAt'] ) ? $run['startedAt'] : '' ) . '</p>'; 423 if ( $passed && $show_daily_offer ) { echo '<p><a class="button button-primary" href="#submitsignal-daily-monitoring">' . esc_html__( 'Review daily monitoring — $19/month plus tax', 'submitsignal' ) . '</a></p>'; } 383 424 if ( ! empty( $run['summary'] ) && is_string( $run['summary'] ) ) { echo '<p>' . esc_html( substr( sanitize_text_field( $run['summary'] ), 0, 1000 ) ) . '</p>'; } 384 425 if ( ! empty( $run['stages'] ) && is_array( $run['stages'] ) ) { 426 $stages = array_slice( $run['stages'], 0, 15 ); 427 if ( $passed ) { 428 $successful = array(); $attention = array(); 429 foreach ( $stages as $stage ) { if ( is_array( $stage ) && isset( $stage['status'] ) && 'passed' === $stage['status'] ) { $successful[] = $stage; } else { $attention[] = $stage; } } 430 if ( $successful ) { echo '<details><summary>' . esc_html__( 'View passed-check evidence', 'submitsignal' ) . '</summary>'; self::evidence_stages( $successful ); echo '</details>'; } 431 // Unverified inbox/CRM results and any contradictory failure 432 // remain visible; a passed browser result never upgrades them. 433 if ( $attention ) { self::evidence_stages( $attention ); } 434 } else { self::evidence_stages( $stages ); } 435 } 436 } 437 private static function evidence_stages( $stages ) { 385 438 echo '<ul>'; 386 foreach ( array_slice( $run['stages'], 0, 15 )as $stage ) {439 foreach ( $stages as $stage ) { 387 440 if ( ! is_array( $stage ) ) { continue; } 388 441 $status = isset( $stage['status'] ) && is_string( $stage['status'] ) && in_array( $stage['status'], array( 'passed', 'failed', 'unverified', 'inconclusive' ), true ) ? $stage['status'] : 'unverified'; … … 392 445 } 393 446 echo '</ul>'; 447 } 448 private static function block_form_ids( $blocks, &$ids, &$remaining, $depth = 0 ) { 449 if ( $depth > 20 ) { return; } 450 foreach ( $blocks as $block ) { 451 if ( --$remaining < 0 ) { return; } 452 if ( isset( $block['blockName'], $block['attrs']['formId'] ) && 'wpforms/form-selector' === $block['blockName'] && is_scalar( $block['attrs']['formId'] ) ) { $ids[] = (string) $block['attrs']['formId']; } 453 if ( ! empty( $block['innerBlocks'] ) ) { self::block_form_ids( $block['innerBlocks'], $ids, $remaining, $depth + 1 ); } 454 } 455 } 456 /** Local suggestions only. They neither fetch a page nor change inventory. */ 457 public static function page_suggestions( $forms ) { 458 $suggestions = array(); foreach ( $forms as $form ) { $suggestions[ $form['id'] ] = array(); } 459 if ( ! $suggestions ) { return $suggestions; } 460 $posts = get_posts( array( 'post_type' => array( 'page', 'post' ), 'post_status' => 'publish', 'has_password' => false, 'numberposts' => 100, 'orderby' => 'ID', 'order' => 'DESC', 'suppress_filters' => true ) ); 461 $pattern = '/' . get_shortcode_regex( array( 'wpforms' ) ) . '/s'; 462 foreach ( $posts as $post ) { 463 if ( 'publish' !== $post->post_status || $post->post_password || ! is_post_publicly_viewable( $post ) || strlen( $post->post_content ) > 262144 ) { continue; } 464 $url = get_permalink( $post ); if ( ! self::page_url( $url ) ) { continue; } 465 $ids = array(); $remaining = 500; 466 self::block_form_ids( parse_blocks( $post->post_content ), $ids, $remaining ); 467 if ( preg_match_all( $pattern, $post->post_content, $matches, PREG_SET_ORDER ) ) { 468 foreach ( $matches as $match ) { 469 if ( ! empty( $match[1] ) || ! empty( $match[6] ) ) { continue; } 470 $attributes = shortcode_parse_atts( $match[3] ); 471 if ( is_array( $attributes ) && isset( $attributes['id'] ) && is_string( $attributes['id'] ) ) { $ids[] = $attributes['id']; } 472 } 473 } 474 foreach ( array_unique( $ids ) as $id ) { 475 if ( isset( $suggestions[ $id ] ) && count( $suggestions[ $id ] ) < 10 ) { $suggestions[ $id ][] = array( 'url' => $url, 'title' => substr( sanitize_text_field( get_the_title( $post ) ), 0, 100 ) ); } 476 } 477 } 478 return $suggestions; 479 } 480 private static function setup_fields( $forms, $pages, $operation ) { 481 $single = 1 === count( $forms ) ? $forms[0] : null; 482 $choices = $single && isset( $pages[ $single['id'] ] ) ? $pages[ $single['id'] ] : array(); 483 $url = 1 === count( $choices ) ? $choices[0]['url'] : ''; 484 $list = 'submitsignal-pages-' . $operation; 485 echo '<div data-ss-setup><p><label>' . esc_html__( 'Contact form', 'submitsignal' ) . ' <select name="form_id" data-ss-form required><option value="">' . esc_html__( 'Choose your form', 'submitsignal' ) . '</option>'; 486 foreach ( $forms as $form ) { echo '<option value="' . esc_attr( $form['id'] ) . '" ' . selected( $single ? $single['id'] : '', $form['id'], false ) . '>' . esc_html( $form['name'] . ' (#' . $form['id'] . ')' ) . '</option>'; } 487 echo '</select></label></p><p><label>' . esc_html__( 'Page containing this form', 'submitsignal' ) . ' <input class="regular-text" data-ss-url name="page_url" type="url" list="' . esc_attr( $list ) . '" value="' . esc_attr( $url ) . '" maxlength="2048" required></label><datalist id="' . esc_attr( $list ) . '">'; 488 foreach ( $choices as $choice ) { echo '<option value="' . esc_attr( $choice['url'] ) . '">' . esc_html( $choice['title'] ) . '</option>'; } 489 echo '</datalist></p><p>' . esc_html__( 'Confirm the suggested public page, choose another suggestion, or paste its HTTPS URL. Theme-built and dynamically embedded forms may need a manual URL. Nothing is submitted until you choose an action below.', 'submitsignal' ) . '</p><details><summary>' . esc_html__( 'Monitor name (optional)', 'submitsignal' ) . '</summary><p><label>' . esc_html__( 'Name', 'submitsignal' ) . ' <input class="regular-text" data-ss-name name="name" maxlength="100" value="' . esc_attr( $single ? $single['name'] : '' ) . '"></label></p></details></div>'; 490 } 491 private static function setup_defaults_script( $forms, $pages ) { 492 $names = array(); foreach ( $forms as $form ) { $names[ $form['id'] ] = $form['name']; } 493 $flags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT; 494 echo '<script>(function(){var names=' . wp_json_encode( $names, $flags ) . ',pages=' . wp_json_encode( $pages, $flags ) . ';document.querySelectorAll("[data-ss-setup]").forEach(function(group){var form=group.querySelector("[data-ss-form]"),url=group.querySelector("[data-ss-url]"),name=group.querySelector("[data-ss-name]"),list=document.getElementById(url.getAttribute("list")),lastName=name.value;form.addEventListener("change",function(){var choices=pages[form.value]||[];if(name.value===lastName||!name.value){name.value=(names[form.value]||"").slice(0,100);}lastName=(names[form.value]||"").slice(0,100);url.value=choices.length===1?choices[0].url:"";while(list.firstChild){list.removeChild(list.firstChild);}choices.forEach(function(choice){var option=document.createElement("option");option.value=choice.url;option.label=choice.title;list.appendChild(option);});});});})();</script>'; 495 } 496 private static function checkout_form( $plan ) { 497 $label = 'agency' === $plan ? 'Agency' : 'Solo'; $price = 'agency' === $plan ? '99' : '19'; 498 self::open_form( 'checkout' ); self::hidden( 'plan', $plan ); self::hidden( 'terms_version', self::TERMS ); 499 self::checkbox( 'eligibility_consent', __( 'This is a US business with US billing. I will use only supported ordinary WPForms Lite contact forms that I own or am authorized to test.', 'submitsignal' ) ); 500 echo '<p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Read the current terms', 'submitsignal' ) . '</a> · ' . esc_html( self::TERMS ) . '</p>'; 501 self::checkbox( 'commercial_consent', sprintf( __( 'I accept the current terms and %1$s at US$%2$s per month plus applicable tax, renewing monthly until canceled. I will review the total and complete payment on Stripe.', 'submitsignal' ), $label, $price ) ); 502 self::close_form( sprintf( __( 'Continue with %1$s — $%2$s/month', 'submitsignal' ), $label, $price ), 'solo' === $plan ); 503 } 504 /** Reconcile a checkout return before WordPress emits administrator headers. */ 505 public static function checkout_return() { 506 if ( ! current_user_can( 'manage_options' ) || ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] || 'submitsignal' !== self::input( 'page', true ) || ! self::management() ) { return; } 507 $checkout_return = self::input( 'checkout', true ); 508 if ( in_array( $checkout_return, array( 'success', 'canceled' ), true ) ) { 509 $result = self::manage( 'status' ); 510 self::notice( is_wp_error( $result ) ? __( 'Returned from checkout, but current status is unavailable. Refresh status before another action. Returning never confirms payment.', 'submitsignal' ) : __( 'Returned from checkout. Your verified plan is shown below; the return link itself is not payment proof. Daily monitoring stays paused until you explicitly enable it.', 'submitsignal' ) ); 394 511 } 395 512 } … … 404 521 } 405 522 $notice = get_transient( 'submitsignal_notice_' . get_current_user_id() ); delete_transient( 'submitsignal_notice_' . get_current_user_id() ); 406 echo '<div class="wrap"><h1>SubmitSignal</h1><p>' . esc_html__( ' Set up and review contact-form checks here in WordPress. Connecting does not run a test or start a paid plan.', 'submitsignal' ) . '</p>';523 echo '<div class="wrap"><h1>SubmitSignal</h1><p>' . esc_html__( 'Connect your account, check one form for free, then choose whether to monitor it daily. No card is needed for your first check.', 'submitsignal' ) . '</p>'; 407 524 if ( is_string( $notice ) && $notice ) { echo '<div class="notice notice-info"><p>' . esc_html( $notice ) . '</p></div>'; } 408 525 if ( $pending ) { … … 412 529 self::open_form( 'cancel_authorization' ); self::close_form( __( 'Cancel local pending approval', 'submitsignal' ) ); 413 530 } elseif ( ! $management ) { 414 echo '<h2>' . esc_html( $settings ? __( ' Enable WordPress controls', 'submitsignal' ) : __( 'Connect your free account', 'submitsignal' ) ) . '</h2>';531 echo '<h2>' . esc_html( $settings ? __( '1. Enable WordPress controls', 'submitsignal' ) : __( '1. Connect your free account', 'submitsignal' ) ) . '</h2>'; 415 532 echo '<p>' . esc_html__( 'Continue to SubmitSignal, sign in, approve this website, then return here. No pairing-code copying is needed. Existing metadata connections require this separate approval before WordPress can manage checks or billing.', 'submitsignal' ) . '</p>'; 416 533 self::open_form( 'start' ); … … 418 535 self::close_form( $settings ? __( 'Enable WordPress controls', 'submitsignal' ) : __( 'Connect SubmitSignal', 'submitsignal' ), true ); 419 536 } else { 420 echo '<h2>' . esc_html__( ' Connected — WordPress controls enabled', 'submitsignal' ) . '</h2><p>' . esc_html__( 'Credentials are private server-side options. This page does not poll, submit forms or purchase anything automatically.', 'submitsignal' ) . '</p>';421 if ( empty( $view['data'] ) ) { echo '<p><strong>' . esc_html__( ' Next: choose Refresh status to load your form controls and current plan.', 'submitsignal' ) . '</strong> ' . esc_html__( 'Then select Form, enter Monitor name and Public HTTPS page containing this form, accept the test permission and choose Create paused monitor. No test runs until you separately authorize Run first check.', 'submitsignal' ) . '</p>'; }537 echo '<h2>' . esc_html__( '1. Connected — WordPress controls enabled', 'submitsignal' ) . '</h2>'; 538 if ( empty( $view['data'] ) ) { echo '<p><strong>' . esc_html__( 'Current status could not be loaded. Choose Refresh status to continue.', 'submitsignal' ) . '</strong> ' . esc_html__( 'Your connection is saved. Do not reconnect or repeat a check to load status.', 'submitsignal' ) . '</p>'; } 422 539 self::open_form( 'status' ); self::close_form( __( 'Refresh status', 'submitsignal' ), true ); 423 540 if ( ! empty( $view['observed_at'] ) ) { echo '<p>' . esc_html( 'Last explicit status refresh: ' . gmdate( 'Y-m-d H:i:s', $view['observed_at'] ) . ' UTC. This is a saved snapshot.' ) . '</p>'; } … … 425 542 if ( ! empty( $view['data'] ) ) { self::controls( $view['data'], $forms, ! empty( $view['uncertain'] ) ); } 426 543 } 427 echo '< h2>' . esc_html__( 'Compatible form inventory', 'submitsignal' ) . '</h2>';544 echo '<details><summary>' . esc_html__( 'Form inventory and advanced settings', 'submitsignal' ) . '</summary><h2>' . esc_html__( 'Compatible form inventory', 'submitsignal' ) . '</h2>'; 428 545 if ( ! function_exists( 'wpforms' ) ) { echo '<p>' . esc_html__( 'WPForms is not active. Activate WPForms Lite, then create and publish an ordinary contact form.', 'submitsignal' ) . '</p>'; } 429 546 elseif ( ! $forms ) { $published = get_posts( array( 'post_type' => 'wpforms', 'post_status' => 'publish', 'numberposts' => 1, 'fields' => 'ids' ) ); echo '<p>' . esc_html( $published ? __( 'Published WPForms forms were found, but their fields or settings are outside supported coverage. Use ordinary name, email, text and paragraph fields; do not remove CAPTCHA or other protections to force a test.', 'submitsignal' ) : __( 'No published WPForms forms were found. Create and publish an ordinary contact form, then refresh inventory.', 'submitsignal' ) ) . '</p>'; } … … 432 549 if ( $settings ) { self::receipt_controls( $settings ); } 433 550 if ( ! $settings && ! $pending ) { self::legacy_form(); } 434 echo '< p>' . esc_html__( 'Launch coverage is simple public WPForms Lite forms. CAPTCHA, custom validation and unsupported fields can make a check inconclusive. WordPress mail handoff does not prove customer-inbox, spam-folder or CRM delivery. Existing email and CRM automations may still run for labeled tests.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/start' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Setup guide', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard?setup=wordpress' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/privacy' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Privacy', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Terms', 'submitsignal' ) . '</a></p></div>';551 echo '</details><p>' . esc_html__( 'Launch coverage is simple public WPForms Lite forms. CAPTCHA, custom validation and unsupported fields can make a check inconclusive. WordPress mail handoff does not prove customer-inbox, spam-folder or CRM delivery. Existing email and CRM automations may still run for labeled tests.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/start' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Setup guide', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard?setup=wordpress' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/privacy' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Privacy', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Terms', 'submitsignal' ) . '</a></p></div>'; 435 552 } 436 553 private static function controls( $data, $forms, $uncertain ) { 437 echo '<p>' . esc_html( 'Plan: ' . ( isset( $data['account']['plan'] ) ? $data['account']['plan'] : 'unknown' ) . ' · Billing: ' . ( isset( $data['account']['billingStatus'] ) ? $data['account']['billingStatus'] : 'unknown' ) . ' · Browser checks: ' . ( ! empty( $data['capabilities']['browser'] ) ? 'available' : 'unavailable' ) . ' · Daily scheduler: ' . ( ! empty( $data['capabilities']['scheduler'] ) ? 'available' : 'unavailable' ) ) . '</p>'; 438 echo '<p>' . esc_html__( 'Free accounts have one website, one monitor and up to three manual checks per day. Daily monitoring and failure emails require a paid plan and available service capabilities. A checkout return alone does not confirm payment; refresh verified status.', 'submitsignal' ) . '</p>'; 554 echo '<section style="max-width:760px;padding:16px;background:#fff;border:1px solid #dcdcde;margin:16px 0"><h2>' . esc_html__( '2. Check one form for free', 'submitsignal' ) . '</h2><p>' . esc_html__( 'Choose the form and public page. One explicit permission creates a paused monitor and requests exactly one labeled check. It does not start daily monitoring or take payment.', 'submitsignal' ) . '</p>'; 555 if ( empty( $data['capabilities']['browser'] ) ) { echo '<p role="status">' . esc_html__( 'Browser checks are currently unavailable. You can save a paused monitor and return later; no inquiry will be submitted now.', 'submitsignal' ) . '</p>'; } 556 $pages = self::page_suggestions( $forms ); 439 557 foreach ( array_slice( $data['monitors'], 0, 100 ) as $monitor ) { 440 558 if ( empty( $monitor['id'] ) || ! self::id( $monitor['id'] ) ) { continue; } 441 559 echo '<hr><h3>' . esc_html( isset( $monitor['name'] ) ? $monitor['name'] : 'Form monitor' ) . '</h3><p>' . esc_html( ! empty( $monitor['paused'] ) ? 'Daily schedule paused' : 'Daily schedule enabled; paid access and scheduler availability still apply' ) . '</p>'; 442 560 $latest = null; foreach ( $data['runs'] as $run ) { if ( isset( $run['monitorId'] ) && $run['monitorId'] === $monitor['id'] && ( ! $latest || strcmp( (string) $run['startedAt'], (string) $latest['startedAt'] ) > 0 ) ) { $latest = $run; } } 443 if ( $latest ) { self::evidence( $latest ); }561 if ( $latest ) { self::evidence( $latest, empty( $data['account']['hasExistingSubscription'] ) ); } 444 562 $review = isset( $monitor['submissionReview'] ) ? $monitor['submissionReview'] : null; 445 563 if ( $review ) { … … 447 565 if ( ! $uncertain && isset( $review['state'], $review['runId'] ) && 'needs_review' === $review['state'] ) { self::open_form( 'review_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::hidden( 'run_id', $review['runId'] ); self::checkbox( 'acknowledgement', __( 'I reviewed this exact attempt and its possible downstream effects. I authorize clearing its hold without submitting another inquiry.', 'submitsignal' ) ); self::close_form( __( 'Acknowledge reviewed attempt', 'submitsignal' ) ); } 448 566 } elseif ( ! $uncertain && ! empty( $data['capabilities']['browser'] ) && ( ! $latest || 'running' !== $latest['status'] ) ) { 567 $repeat_passed = $latest && 'passed' === $latest['status']; 568 if ( $repeat_passed ) { echo '<details><summary>' . esc_html__( 'Run another manual check', 'submitsignal' ) . '</summary>'; } 449 569 self::open_form( 'run_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::checkbox( 'test_permission', __( 'I authorize one labeled synthetic inquiry on this form. Its existing email and CRM automations may run.', 'submitsignal' ) ); self::close_form( $latest ? __( 'Run one check', 'submitsignal' ) : __( 'Run first check', 'submitsignal' ), true ); 570 if ( $repeat_passed ) { echo '</details>'; } 450 571 } 451 if ( ! $uncertain && ( empty( $monitor['paused'] ) || self::may_resume( $data, $monitor ) ) ) { self::open_form( 'pause_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::hidden( 'paused', empty( $monitor['paused'] ) ? '1' : '0' ); if ( ! empty( $monitor['paused'] ) ) { self::checkbox( 'schedule_permission', __( 'I authorize one labeled check each day while paid access and scheduling are available.', 'submitsignal' ) ); } self::close_form( empty( $monitor['paused'] ) ? __( 'Pause daily monitoring', 'submitsignal' ) : __( 'Resume daily monitoring', 'submitsignal' ) ); }452 572 } 453 573 $free_limit_reached = isset( $data['account']['plan'] ) && 'early_access' === $data['account']['plan'] && count( $data['monitors'] ) >= 1; 454 574 if ( ! $uncertain && $free_limit_reached ) { 455 echo '< hr><h2>' . esc_html__( 'Your free monitor is already set up', 'submitsignal' ) . '</h2><p>' . esc_html__( 'The free plan includes one monitor. Use the existing monitor above instead of creating another.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> — ' . esc_html__( 'If you chose the wrong form or page and have not run a check, delete that untested monitor in the dashboard, return here, choose Refresh status, and recreate it correctly.', 'submitsignal' ) . '</p><p>' . esc_html__( 'Submission-review holds survive deletion. Never delete and recreate a monitor to evade a hold or repeat an uncertain inquiry; review the original attempt and its downstream effects first.', 'submitsignal' ) . '</p>';575 echo '<details><summary>' . esc_html__( 'Free monitor options and recovery', 'submitsignal' ) . '</summary><h2>' . esc_html__( 'Your free monitor is already set up', 'submitsignal' ) . '</h2><p>' . esc_html__( 'The free plan includes one monitor. Use the existing monitor above instead of creating another.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> — ' . esc_html__( 'If you chose the wrong form or page and have not run a check, delete that untested monitor in the dashboard, return here, choose Refresh status, and recreate it correctly.', 'submitsignal' ) . '</p><p>' . esc_html__( 'Submission-review holds survive deletion. Never delete and recreate a monitor to evade a hold or repeat an uncertain inquiry; review the original attempt and its downstream effects first.', 'submitsignal' ) . '</p></details>'; 456 576 } elseif ( ! $uncertain && $forms ) { 457 echo '<hr><h2>' . esc_html__( 'Choose a form for its first check', 'submitsignal' ) . '</h2>'; self::open_form( 'create_monitor' ); 458 echo '<p><label>' . esc_html__( 'Form', 'submitsignal' ) . ' <select name="form_id" required><option value="">' . esc_html__( 'Choose a compatible form', 'submitsignal' ) . '</option>'; foreach ( $forms as $form ) { echo '<option value="' . esc_attr( $form['id'] ) . '">' . esc_html( $form['name'] ) . '</option>'; } echo '</select></label></p><p><label>' . esc_html__( 'Monitor name', 'submitsignal' ) . ' <input class="regular-text" name="name" maxlength="100" required></label></p><p><label>' . esc_html__( 'Public HTTPS page containing this form', 'submitsignal' ) . ' <input class="regular-text" name="page_url" type="url" maxlength="2048" required></label></p>'; 459 self::checkbox( 'consent', __( 'I own or administer this website and authorize labeled synthetic tests of this form, including its existing email and CRM automations. The monitor starts paused; running a check and enabling a daily schedule are separate actions.', 'submitsignal' ) ); self::close_form( __( 'Create paused monitor', 'submitsignal' ), true ); 460 } 461 echo '<hr><h2>' . esc_html__( 'Optional daily monitoring', 'submitsignal' ) . '</h2><p>' . esc_html__( 'Solo: $19/month plus tax. Agency: $99/month plus tax. Paid launch enrollment is limited to US businesses with US billing, subject to current capacity. Payment details are entered on Stripe; WordPress never receives them. Returning here does not by itself verify payment.', 'submitsignal' ) . '</p>'; 577 if ( ! empty( $data['capabilities']['browser'] ) ) { 578 self::open_form( 'check_form' ); self::setup_fields( $forms, $pages, 'check_form' ); 579 self::checkbox( 'consent', __( 'I own or administer this website. Create a paused monitor for the form and page shown above and run exactly one labeled test inquiry now. Its existing email and CRM automations may run. Daily checks and payment need separate permission.', 'submitsignal' ) ); self::close_form( __( 'Check this form', 'submitsignal' ), true ); 580 } 581 echo '<details><summary>' . esc_html__( 'Save a paused monitor without running a check', 'submitsignal' ) . '</summary>'; 582 self::open_form( 'create_monitor' ); self::setup_fields( $forms, $pages, 'create_monitor' ); 583 self::checkbox( 'consent', __( 'I own or administer this website and authorize monitoring setup for this selected form. Save it paused without submitting an inquiry. A check or daily schedule requires a later explicit choice.', 'submitsignal' ) ); self::close_form( __( 'Create paused monitor', 'submitsignal' ) ); echo '</details>'; 584 } 585 self::setup_defaults_script( $forms, $pages ); 586 echo '<p>' . esc_html__( 'Free diagnostics include one website, one monitor and up to three manual checks per day. Read browser submission, WordPress acceptance and email handoff separately; they do not prove inbox or CRM delivery.', 'submitsignal' ) . '</p></section>'; 587 echo '<section id="submitsignal-daily-monitoring" style="max-width:760px;padding:16px;background:#fff;border:1px solid #dcdcde;margin:16px 0"><h2>' . esc_html__( '3. Optional daily monitoring', 'submitsignal' ) . '</h2><p>' . esc_html( 'Plan: ' . ( isset( $data['account']['plan'] ) && 'early_access' !== $data['account']['plan'] ? $data['account']['plan'] : 'Free diagnostics' ) . ' · Billing: ' . ( isset( $data['account']['billingStatus'] ) ? $data['account']['billingStatus'] : 'unknown' ) ) . '</p><p>' . esc_html__( 'We recommend reviewing a successful check before paying. Solo is $19/month plus tax for one website; Agency is $99/month plus tax for up to ten. US business and US billing are required, subject to current launch capacity. Stripe handles payment details. Buying a plan does not enable the schedule.', 'submitsignal' ) . '</p>'; 462 588 if ( ! $uncertain && ! empty( $data['account']['publicCheckout'] ) && ! empty( $data['capabilities']['billing'] ) && empty( $data['account']['hasExistingSubscription'] ) ) { 463 self::open_form( 'checkout' ); echo '<p><label>' . esc_html__( 'Plan', 'submitsignal' ) . ' <select name="plan" required><option value="">' . esc_html__( 'Choose a plan', 'submitsignal' ) . '</option><option value="solo">Solo — $19/month plus tax</option><option value="agency">Agency — $99/month plus tax</option></select></label></p>'; 464 self::hidden( 'terms_version', self::TERMS ); self::checkbox( 'us_business', __( 'This is a US business.', 'submitsignal' ) ); self::checkbox( 'us_billing', __( 'The billing country is the United States.', 'submitsignal' ) ); self::checkbox( 'supported_use', __( 'My use is limited to supported ordinary WPForms Lite contact forms and I have permission to run labeled checks.', 'submitsignal' ) ); 465 echo '<p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Read the current commercial terms', 'submitsignal' ) . '</a> · ' . esc_html( self::TERMS ) . '</p>'; self::checkbox( 'commercial_consent', __( 'I accept the current commercial terms and authorize the selected monthly subscription plus tax, renewing until canceled. I will review the total and complete payment on Stripe.', 'submitsignal' ) ); self::close_form( __( 'Continue to secure checkout', 'submitsignal' ), true ); 589 self::checkout_form( 'solo' ); 590 echo '<details><summary>' . esc_html__( 'Need up to ten websites? Choose Agency — $99/month plus tax', 'submitsignal' ) . '</summary>'; self::checkout_form( 'agency' ); echo '</details>'; 466 591 } 467 592 if ( ! $uncertain && ! empty( $data['account']['canManageBilling'] ) ) { self::open_form( 'billing_portal' ); self::close_form( __( 'Manage billing on Stripe', 'submitsignal' ) ); } 468 if ( ! $uncertain ) { echo '<hr><h2>' . esc_html__( 'Disconnect WordPress controls', 'submitsignal' ) . '</h2>'; self::open_form( 'revoke' ); self::checkbox( 'acknowledgement', __( 'Pause this website’s monitors, revoke these WordPress controls and remove local credentials and queued metadata. Billing and hosted history remain unchanged; this does not cancel a subscription.', 'submitsignal' ) ); self::close_form( __( 'Pause and disconnect this website', 'submitsignal' ) ); } 593 if ( ! $uncertain ) { foreach ( $data['monitors'] as $monitor ) { if ( ! empty( $monitor['id'] ) && self::id( $monitor['id'] ) && ( empty( $monitor['paused'] ) || self::may_resume( $data, $monitor ) ) ) { echo '<p><strong>' . esc_html( isset( $monitor['name'] ) ? $monitor['name'] : 'Form monitor' ) . '</strong></p>'; self::open_form( 'pause_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::hidden( 'paused', empty( $monitor['paused'] ) ? '1' : '0' ); if ( ! empty( $monitor['paused'] ) ) { self::checkbox( 'schedule_permission', __( 'I authorize one labeled check each day while paid access and scheduling are available.', 'submitsignal' ) ); } self::close_form( empty( $monitor['paused'] ) ? __( 'Pause daily monitoring', 'submitsignal' ) : __( 'Resume daily monitoring', 'submitsignal' ) ); } } } 594 echo '</section>'; 595 if ( ! $uncertain ) { echo '<details><summary>' . esc_html__( 'Pause and disconnect WordPress controls', 'submitsignal' ) . '</summary>'; self::open_form( 'revoke' ); self::checkbox( 'acknowledgement', __( 'Pause this website’s monitors, revoke these WordPress controls and remove local credentials and queued metadata. Billing and hosted history remain unchanged; this does not cancel a subscription.', 'submitsignal' ) ); self::close_form( __( 'Pause and disconnect this website', 'submitsignal' ) ); echo '</details>'; } 469 596 } 470 597 private static function receipt_controls( $settings ) { -
submitsignal/trunk/readme.txt
r3780656 r3782284 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0. 3.17 Stable tag: 0.4.0 8 8 License: GPL-2.0-or-later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 C onnect ordinary WPForms Lite contact forms to SubmitSignal reliability checks.11 Check a WPForms Lite contact form free from WordPress. Choose optional daily monitoring after reviewing its result. 12 12 13 13 == Description == 14 14 15 Choose a form, review its public page and authorize one clearly labeled inquiry. SubmitSignal shows separate browser submission, WordPress acceptance and email-handoff evidence in WordPress. Start free with one website, one form and up to three manual checks per day. No card is required. 16 17 Version 0.4.0 shortens setup: connected form controls load automatically, known public form pages are suggested locally, monitor names are supplied, and one explicit Check this form action can prepare a paused monitor and run its first authorized check. Daily monitoring and payment remain separate choices. 18 15 19 SubmitSignal connects an administrator-authorized WordPress website to the hosted SubmitSignal monitoring service. It reports metadata about successful WPForms processing and WordPress mail-transport outcomes. The service can submit visibly labeled synthetic contact inquiries to forms the account owner explicitly selects. 16 20 17 This is the official SubmitSignal WordPress connector. SubmitSignal, this plugin and the hosted monitoring service are owned and operated by Axtrel AI LLC. Visit the [official service website](https://getsubmitsignal.com) or [setup guide](https://getsubmitsignal.com/start). This connector uses the canonical service API at https://getsubmitsignal.com. Version 0.3.0 adds administrator-only setup, explicit manual checks, evidence, submission-review controls and billing handoffinside Settings > SubmitSignal.21 This is the official SubmitSignal WordPress connector. SubmitSignal, this plugin and the hosted monitoring service are owned and operated by Axtrel AI LLC. Visit the [official service website](https://getsubmitsignal.com) or [setup guide](https://getsubmitsignal.com/start). This connector uses the canonical service API at https://getsubmitsignal.com. Setup, individually authorized checks, evidence, submission-review controls and the billing handoff are available inside Settings > SubmitSignal. 18 22 19 23 Supported launch coverage is ordinary WPForms Lite contact forms. WPForms Pro is not included in supported launch coverage until acceptance testing with a licensed Pro installation is complete. … … 21 25 The free plugin requires a SubmitSignal service account. The free service tier includes manual diagnostics for one website and one monitor, with up to three checks per day. Scheduled daily monitoring and automated email failure alerts require an active Solo or Agency subscription. Current availability and subscription pricing are shown on the service website. 22 26 23 Paid launch enrollment is limited to US businesses with US billing. Solo monitoring is $19/month plus tax; Agency monitoring is $99/month plus tax. Start with a [free form review](https://getsubmitsignal.com/compatibility). This questionnaire evaluates your answers; it does not visit or submityour form. An actual test requires the website owner's authorization.27 Paid launch enrollment is limited to US businesses with US billing. Solo monitoring is $19/month plus tax; Agency monitoring is $99/month plus tax. Install free and authorize your first check in WordPress. If you are unsure about fit, the optional [free form review](https://getsubmitsignal.com/compatibility) evaluates your answers without visiting or submitting your form. An actual test requires the website owner's authorization. 24 28 25 29 Browser checks require the service's browser runner to be configured, and scheduled monitoring and email alerts also depend on the hosted scheduler and email service. The dashboard shows current availability. Installing this plugin alone does not start synthetic submissions or promise active scheduled checks. … … 47 51 The administrator signs in to SubmitSignal, approves this website and returns to WordPress. Private metadata and management tokens are stored only in non-autoloaded server options; they never appear in HTML, JavaScript or URLs. Existing older connections need a separate new approval to enable WordPress controls. An uncertain response retains the original private request for an explicit retry. 48 52 49 Native management requests are sent only after an administrator action protected by a WordPress nonce. Loading settings makes no service request. Status is a saved snapshot until explicitly refreshed; an accepted management action may perform one bounded status refresh. New native monitors start paused. Creating a monitor, running one labeled check, acknowledging an uncertain prior attempt, resuming daily monitoring and starting paid checkout each require explicit choices. There is no anonymous WordPress telemetry or automatic polling.53 Native management changes require an administrator action protected by a WordPress nonce and single-use action token. After the administrator-approved connection callback, an accepted action, or a checkout return, the plugin may perform one bounded read of verified service status. Ordinary settings visits do not poll or submit forms. A checkout return never proves payment. Public form-page suggestions are read locally from published, non-password-protected WordPress content; only the page selected for monitoring is sent with the authorized setup action. New monitors start paused. Check this form explicitly authorizes preparing that monitor and submitting one labeled inquiry. Acknowledging an uncertain attempt, enabling daily monitoring and starting paid checkout remain separate choices. There is no anonymous WordPress telemetry. 50 54 51 55 While connected, the plugin sends compatible form inventories, heartbeat timestamps, random event IDs, form IDs, form-processing outcomes, mail-transport outcomes and synthetic run references. This default metadata stream excludes entered field values, visitor names, email addresses, recipients, message content, visitor IP addresses and cookies. Separately enabled synthetic notification copies are processed by Resend as described above. The service records only the receiving event ID, test/run identifiers, unique test destination and receipt timestamps; it does not fetch the received message body. The hosted browser checks visit publicly accessible form pages; the hosted service's privacy policy separately describes that processing. … … 57 61 == Installation == 58 62 59 1. Download the current [official connector ZIP](https://getsubmitsignal.com/downloads/submitsignal.zip) and use WordPress > Plugins > Add New Plugin > Upload Plugin. The [WordPress.org listing](https://wordpress.org/plugins/submitsignal/) also offers the latest directory-published release; confirm its version before installing. Check the installed version after updating. 60 2. Activate SubmitSignal. Install and activate WPForms Lite and create an ordinary contact form if needed. WPForms Pro is not yet part of supported launch coverage. 61 3. Open Settings > SubmitSignal, or choose Settings beside SubmitSignal on the Plugins page. Read and accept the connection disclosure, then choose Connect SubmitSignal. Sign in to the service and approve this website; you return to the same WordPress administrator session without copying a code. A separate legacy code option remains available for hosted-dashboard-only control. 62 4. Choose Refresh status to load the controls. Select Form, enter Monitor name and Public HTTPS page containing this form, explicitly authorize labeled tests, then choose Create paused monitor. Creation does not submit the form. The free plan includes one monitor; if you chose the wrong form or page and have not run a check, use Open dashboard to delete that untested monitor, then return, refresh status and recreate it correctly. Submission-review holds survive deletion; never delete and recreate a monitor to evade a hold or repeat an uncertain inquiry. 63 5. Choose Run first check and explicitly authorize one synthetic inquiry. Review browser, WordPress acceptance and mail-handoff evidence. Inspect your real inbox and existing CRM separately. If an attempt may already have submitted, review its downstream effects before acknowledging the hold; acknowledgment does not rerun it. 64 6. Free manual diagnostics cover one website, one monitor and up to three checks per day. For daily monitoring, choose Solo ($19/month plus tax) or Agency ($99/month plus tax), confirm US business/billing and supported use, accept the current commercial terms, and continue to Stripe. Payment details never enter WordPress. Refresh verified service status after returning; the return alone is not proof of payment. Enable a daily schedule only after a useful first check and explicit schedule permission. 65 7. Use Pause or Resume for daily monitoring, and Manage billing when available. Billing and hosted history remain separate from disconnecting WordPress controls. The [setup guide](https://getsubmitsignal.com/start) and [hosted dashboard](https://getsubmitsignal.com/dashboard?setup=wordpress) remain available. 63 1. Install and activate SubmitSignal and WPForms Lite. Open Settings > SubmitSignal, accept the connection disclosure and choose Connect SubmitSignal. Sign in, review the website and account shown, approve and return to the same WordPress administrator session. No pairing code is needed and the form controls load automatically. The [official website ZIP](https://getsubmitsignal.com/downloads/submitsignal.zip) can be installed with Plugins > Add New Plugin > Upload Plugin. 64 2. Review the form and its public HTTPS page. WordPress suggests published pages containing a recognized embed; dynamic or theme embeds may require entering the URL yourself. Authorize one labeled inquiry and choose Check this form. It creates a paused monitor and requests one check; existing email and CRM automations may run. Review each evidence stage. An uncertain submission must be reviewed before any later check; deleting or recreating the monitor does not bypass a hold. 65 3. After a useful first check, choose optional daily monitoring: Solo $19/month plus applicable tax for one website, or Agency $99/month plus applicable tax for up to ten. Confirm US business/billing eligibility and supported use, accept the selected monthly terms, and complete payment securely on Stripe/Link. WordPress reads verified status on return. Choose to enable daily checks separately when paid access is confirmed. Cancel through Manage billing; uninstalling or disconnecting does not cancel a subscription. 66 67 Free diagnostics include one website, one form and up to three manual checks per day. Daily monitoring and automated failure emails require an active paid plan and operational services. See the [short setup guide](https://getsubmitsignal.com/start) or ask for free setup help there. Existing older connections and subscriptions are preserved. Manual pairing remains an advanced option. 66 68 67 69 WordPress must be reachable over HTTPS and expose its public REST API. Install per website on multisite; network-wide configuration is not provided. … … 86 88 == Screenshots == 87 89 88 1. After account approval, refresh status in WordPress. Local demonstration with simulated service responses.89 2. Choose a supported form and public page, then create a paused monitor. Local demonstration; no form submission occurs.90 3. Review separate browser, WordPress and mail-handoff evidence . Local demonstration with simulated results; customer inbox and CRM delivery remain unverified.90 1. Connect from WordPress and approve the account connection. Local demonstration with simulated service responses. 91 2. Review a suggested form page and authorize one first check. Local demonstration with simulated service responses. 92 3. Review separate browser, WordPress and mail-handoff evidence, then choose optional daily monitoring. Local demonstration with simulated results; customer inbox and CRM delivery remain unverified. 91 93 92 94 == Changelog == 95 96 = 0.4.0 = 97 * Shorten setup to connect, check one form and choose optional daily monitoring. 98 * Load verified form controls after approval and read verified status on checkout return, without automatic form tests or purchases. 99 * Suggest recognized public form pages locally and supply monitor names; preserve manual page entry when an embed is unknown. 100 * Combine paused monitor preparation and one explicitly authorized first check, preserving single-use attempts and uncertainty holds. 101 * Simplify billing declarations while retaining US eligibility, supported-use limits, exact-plan monthly terms and separate schedule permission. 102 * Preserve complete Stripe Checkout destinations, including opaque fragment state, without weakening host validation. 93 103 94 104 = 0.3.1 = -
submitsignal/trunk/submitsignal.php
r3780656 r3782284 3 3 * Plugin Name: SubmitSignal 4 4 * Description: Connect ordinary WPForms contact forms to SubmitSignal reliability monitoring, with explicit administrator consent. 5 * Version: 0. 3.15 * Version: 0.4.0 6 6 * Requires at least: 6.2 7 7 * Requires PHP: 7.4 … … 16 16 17 17 final class SubmitSignal_Plugin { 18 const VERSION = '0. 3.1';18 const VERSION = '0.4.0'; 19 19 const SERVICE = 'https://getsubmitsignal.com'; 20 20 const SETTINGS = 'submitsignal_settings';
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)