PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.8
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.8
2.12.8 2.12.7 2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 All 98 releases
← All changes | inc/helper.php +260 -13 2.12.1 → 2.12.8 View file →
@@ -191,8 +191,16 @@
191 191
192 192 /**
193 193 * Extracts the field label from the dynamic field key ( or field slug ).
194 194 *
195 + * ALWAYS escape the return value at the sink. The label is decoded from the
196 + * submitted key via {@see self::decode()}, so it is submitter-controlled and
197 + * unauthenticated — and this method is strictly more dangerous than decode()
198 + * alone, because it additionally runs html_entity_decode(), which expands
199 + * entities and therefore undoes any htmlspecialchars()-on-store defence.
200 + * When a label's integrity matters, read it from the form's stored block
201 + * definitions by block id instead of from the submitted key.
202 + *
195 203 * @param string $field_key Dynamic field key.
196 204 * @since 1.1.1
197 205 * @return string Extracted field label.
198 206 */
@@ -201,12 +209,14 @@
201 209 return '';
202 210 }
203 211
204 212 $label = explode( '-lbl-', $field_key )[1];
205 - // Getting the encrypted label. we are removing the block slug here.
213 + // Getting the encoded label. we are removing the block slug here.
206 214 $label = explode( '-', $label )[0];
207 215
208 - return $label ? html_entity_decode( self::decrypt( $label ) ) : '';
216 + // The result is submitter-controlled and unauthenticated — escape it for its
217 + // context at the sink (esc_html, escape_csv_formula, ...), never trust it.
218 + return $label ? html_entity_decode( self::decode( $label ) ) : '';
209 219 }
210 220
211 221 /**
212 222 * Extracts the block ID from the dynamic field key ( or field slug ).
@@ -509,15 +519,34 @@
509 519 return is_string( $output ) ? $output : '';
510 520 }
511 521
512 522 /**
513 - * Encrypt data using base64.
523 + * Base64-encode a string for use inside a field key.
514 524 *
515 - * @param string $input The input string which needs to be encrypted.
516 - * @since 0.0.1
517 - * @return string The encrypted string.
525 + * Replaces encrypt(), which is retained as a deprecated alias. The encoding itself
526 + * is unchanged since 0.0.1 — only the name is new.
527 + *
528 + * NOT ENCRYPTION. This is plain, unkeyed base64 (padding stripped) used only to
529 + * carry a label inside a field key. There is no key, no HMAC and no integrity
530 + * protection, so a value round-tripped through decode() is fully attacker-forgeable
531 + * and must never be treated as authentic or trusted as a security boundary. When a
532 + * label's integrity matters, look it up from the form's stored block definitions by
533 + * block id instead of decoding it from the submitted key.
534 + *
535 + * Two behaviours worth knowing before relying on this pair:
536 + *
537 + * - The input is run through wp_strip_all_tags(), so this is not a lossless
538 + * round trip: decode( encode( $x ) ) !== $x whenever $x contains markup.
539 + * That stripping happens on this trusted side only — decode() returns raw
540 + * submitted bytes and does NOT strip anything, so every sink must escape for
541 + * its own context (esc_html(), escape_csv_formula(), ...).
542 + * - Falsy input (including the string '0') returns '', not base64.
543 + *
544 + * @param string $input The input string to encode.
545 + * @since 2.12.3
546 + * @return string The base64-encoded string (padding removed).
518 547 */
519 - public static function encrypt( $input ) {
548 + public static function encode( $input ) {
520 549 // If the input is empty or not a string, then abandon ship.
521 550 if ( empty( $input ) || ! is_string( $input ) ) {
522 551 return '';
523 552 }
@@ -524,32 +553,67 @@
524 553
525 554 // Strip HTML tags to prevent them from being included in IDs and field names.
526 555 $input = wp_strip_all_tags( $input );
527 556
528 - // Encrypt the input and return it.
557 + // Base64-encode the input and return it.
529 558 $base_64 = base64_encode( $input ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
530 559 return rtrim( $base_64, '=' );
531 560 }
532 561
533 562 /**
534 - * Decrypt data using base64.
563 + * Base64-encode a string.
535 564 *
536 - * @param string $input The input string which needs to be decrypted.
565 + * @deprecated 2.12.3 Use {@see self::encode()}. The name wrongly implied a security
566 + * boundary — this is unkeyed base64, not encryption.
567 + *
568 + * @param string $input The input string to encode.
537 569 * @since 0.0.1
538 - * @return string The decrypted string.
570 + * @return string The base64-encoded string.
539 571 */
540 - public static function decrypt( $input ) {
572 + public static function encrypt( $input ) {
573 + return self::encode( $input );
574 + }
575 +
576 + /**
577 + * Base64-decode a string produced by encode().
578 + *
579 + * Replaces decrypt(), which is retained as a deprecated alias. The decoding itself
580 + * is unchanged since 0.0.1 — only the name is new.
581 + *
582 + * NOT DECRYPTION. See {@see self::encode()} — the result is unauthenticated and
583 + * attacker-forgeable; do not trust it where integrity matters. The output is raw
584 + * submitted bytes: no tag stripping, no sanitising. Escape it at the sink.
585 + *
586 + * @param string $input The input string to decode.
587 + * @since 2.12.3
588 + * @return string The decoded string.
589 + */
590 + public static function decode( $input ) {
541 591 // If the input is empty or not a string, then abandon ship.
542 592 if ( empty( $input ) || ! is_string( $input ) ) {
543 593 return '';
544 594 }
545 595
546 - // Decrypt the input and return it.
596 + // Base64-decode the input and return it.
547 597 $base_64 = $input . str_repeat( '=', strlen( $input ) % 4 );
548 598 return base64_decode( $base_64 ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
549 599 }
550 600
551 601 /**
602 + * Base64-decode a string.
603 + *
604 + * @deprecated 2.12.3 Use {@see self::decode()}. The name wrongly implied a security
605 + * boundary — this is unkeyed base64, not decryption.
606 + *
607 + * @param string $input The input string to decode.
608 + * @since 0.0.1
609 + * @return string The decoded string.
610 + */
611 + public static function decrypt( $input ) {
612 + return self::decode( $input );
613 + }
614 +
615 + /**
552 616 * Update an option from the database.
553 617 *
554 618 * @param string $key The option key.
555 619 * @param mixed $value The value to update.
@@ -685,8 +749,43 @@
685 749 );
686 750 }
687 751
688 752 /**
753 + * Resolve the submitting user, surviving REST's nonce-less de-authentication.
754 + *
755 + * The public form endpoints authenticate with the HMAC Submit_Token rather than
756 + * a nonce, because the form markup is page-cacheable and core answers a nonce
757 + * that fails verification with a hard 403 — a value baked into a cached page
758 + * would break submissions once it aged out.
759 + *
760 + * The trade-off is that `rest_cookie_check_errors()` treats a cookie-carrying
761 + * REST request with no nonce as anonymous and calls `wp_set_current_user( 0 )`
762 + * before dispatch. So `get_current_user_id()` returns 0 during a submission even
763 + * when the visitor is signed in, which silently drops entry attribution and
764 + * blanks every `{user_*}` smart tag.
765 + *
766 + * `wp_validate_auth_cookie()` reads the logged-in cookie directly and is
767 + * unaffected by that reset. It verifies the cookie's HMAC, so the identity is
768 + * authenticated, not merely asserted — this is the same check core itself uses
769 + * for cookie auth, and the pattern already used by the Pro login route.
770 + *
771 + * Returns 0 for genuinely anonymous submissions, so callers can keep treating
772 + * falsy as "not logged in".
773 + *
774 + * @since 2.12.6
775 + * @return int User ID, or 0 when the submitter is not signed in.
776 + */
777 + public static function get_submitting_user_id() {
778 + $user_id = get_current_user_id();
779 +
780 + if ( $user_id ) {
781 + return $user_id;
782 + }
783 +
784 + return absint( wp_validate_auth_cookie( '', 'logged_in' ) );
785 + }
786 +
787 + /**
689 788 * Check if the current user has a given capability.
690 789 *
691 790 * @param string $capability The capability to check.
692 791 * @param array<mixed> $args Optional. Additional arguments to pass to the capability check.
@@ -2044,8 +2143,29 @@
2044 2143 return defined( 'SRFM_PRO_VER' );
2045 2144 }
2046 2145
2047 2146 /**
2147 + * Whether SureForms promotional content should be hidden.
2148 + *
2149 + * Covers review requests, cross-sell banners and announcements. The free
2150 + * plugin never hides them on its own; SureForms Pro's Distraction Free mode
2151 + * turns this on through the filter.
2152 + *
2153 + * @since 2.12.8
2154 + * @return bool
2155 + */
2156 + public static function hide_promotions() {
2157 + /**
2158 + * Filter whether SureForms hides its promotional content in wp-admin.
2159 + *
2160 + * @since 2.12.8
2161 + *
2162 + * @param bool $hide Default false.
2163 + */
2164 + return (bool) apply_filters( 'srfm_hide_promotions', false );
2165 + }
2166 +
2167 + /**
2048 2168 * Verifies the request by checking the nonce and user capabilities.
2049 2169 *
2050 2170 * @param string $request_type The type of request, either 'rest' or 'ajax'.
2051 2171 * @param string $nonce_action The action name for the nonce.
@@ -2110,8 +2230,62 @@
2110 2230 return implode( '-', array_slice( explode( '-', explode( '-lbl-', $field_name )[0] ), 0, 2 ) );
2111 2231 }
2112 2232
2113 2233 /**
2234 + * The active caching plugin, if there is one.
2235 + *
2236 + * Caching matters to SureForms because a cached page serves the same HTML to
2237 + * everyone: the submission token is embedded at render time, and an
2238 + * aggressively cached or JS-combining setup can serve a stale token or reorder
2239 + * the scripts a form depends on. This is what surfaces that to the site owner
2240 + * before it turns into "my form stopped working".
2241 + *
2242 + * @since 2.12.6
2243 + * @return string Human-readable plugin name, or '' when none is active.
2244 + */
2245 + public static function get_active_caching_plugin() {
2246 + $entry = self::get_active_caching_plugin_entry();
2247 +
2248 + return null === $entry ? '' : $entry[0];
2249 + }
2250 +
2251 + /**
2252 + * Setup guide for the active caching plugin.
2253 + *
2254 + * Six of the recognised plugins have a guide of their own; the rest, and any
2255 + * site with none detected, get the general one. Sending someone to a page that
2256 + * names the plugin they actually run is the difference between advice they can
2257 + * follow and advice they have to translate.
2258 + *
2259 + * Falls back to the general guide rather than returning nothing, so the notice
2260 + * always has somewhere to send them.
2261 + *
2262 + * @since 2.12.7
2263 + * @param string $medium Placement the link is rendered in, used as utm_medium.
2264 + * Two surfaces show this guide -- the dashboard notice and
2265 + * the onboarding step -- and a shared value would make the
2266 + * two indistinguishable in reporting, which is the whole
2267 + * point of the attribution.
2268 + * @return string Absolute documentation URL.
2269 + */
2270 + public static function get_caching_plugin_doc_url( $medium = 'form_checks_notice' ) {
2271 + $entry = self::get_active_caching_plugin_entry();
2272 + $slug = null === $entry || '' === $entry[1] ? 'how-to-set-up-sureforms-with-caching-plugins' : $entry[1];
2273 +
2274 + // Through the central builder rather than hardcoding the domain, so the
2275 + // link carries the same UTM attribution as every other doc link and a
2276 + // domain change is one edit. utm_content is the slug, so the caller can be
2277 + // told which guide people actually open.
2278 + return self::get_sureforms_website_url(
2279 + 'docs/' . $slug . '/',
2280 + [
2281 + 'utm_medium' => self::get_string_value( $medium ),
2282 + 'utm_content' => $slug,
2283 + ]
2284 + );
2285 + }
2286 +
2287 + /**
2114 2288 * Check if any of the top 10 popular WordPress SMTP plugins is active using array_intersect.
2115 2289 *
2116 2290 * @since 1.9.1
2117 2291 * @return bool True if any SMTP plugin is active, false otherwise.
@@ -2638,7 +2812,80 @@
2638 2812 * @return int
2639 2813 */
2640 2814 private static function get_geo_failure_ttl() {
2641 2815 return self::get_integer_value( apply_filters( 'srfm_geo_failure_ttl', 5 * MINUTE_IN_SECONDS ) );
2816 + }
2817 +
2818 + /**
2819 + * Caching plugins SureForms recognises, and the guide for each.
2820 + *
2821 + * `path => [ display name, doc slug ]`. An empty slug means there is no
2822 + * plugin-specific guide and the general one applies. Name and slug live in one
2823 + * array on purpose: keyed separately they drift, and a doc link that silently
2824 + * degrades to the generic page is the kind of regression nobody reports.
2825 + *
2826 + * Order is precedence: the first active plugin in this list wins. The six with
2827 + * their own guide are listed first on purpose, so a site running two caching
2828 + * plugins is pointed at the specific guide rather than whichever plugin the
2829 + * old alphabetical order happened to reach first. That flips the winner on a
2830 + * few pairs -- WP Fastest Cache over WP Super Cache, SiteGround Optimizer and
2831 + * Autoptimize over their partners -- and in each case the new winner is the
2832 + * one that has something to say. Reordering this array changes which guide a
2833 + * two-plugin site sees.
2834 + *
2835 + * @since 2.12.7
2836 + * @return array<string,array{0:string,1:string}>
2837 + */
2838 + private static function get_known_caching_plugins() {
2839 + return [
2840 + 'litespeed-cache/litespeed-cache.php' => [ 'LiteSpeed Cache', 'how-to-set-up-sureforms-with-litespeed-cache' ],
2841 + 'wp-rocket/wp-rocket.php' => [ 'WP Rocket', 'how-to-set-up-sureforms-with-wp-rocket' ],
2842 + 'w3-total-cache/w3-total-cache.php' => [ 'W3 Total Cache', 'how-to-set-up-sureforms-with-w3-total-cache' ],
2843 + 'wp-fastest-cache/wpFastestCache.php' => [ 'WP Fastest Cache', 'how-to-set-up-sureforms-with-wp-fastest-cache' ],
2844 + 'sg-cachepress/sg-cachepress.php' => [ 'SiteGround Optimizer', 'how-to-set-up-sureforms-with-siteground-optimizer' ],
2845 + 'autoptimize/autoptimize.php' => [ 'Autoptimize', 'how-to-set-up-sureforms-with-autoptimize' ],
2846 + 'wp-super-cache/wp-cache.php' => [ 'WP Super Cache', '' ],
2847 + 'wp-optimize/wp-optimize.php' => [ 'WP-Optimize', '' ],
2848 + 'cache-enabler/cache-enabler.php' => [ 'Cache Enabler', '' ],
2849 + 'comet-cache/comet-cache.php' => [ 'Comet Cache', '' ],
2850 + 'hummingbird-performance/wp-hummingbird.php' => [ 'Hummingbird', '' ],
2851 + 'breeze/breeze.php' => [ 'Breeze', '' ],
2852 + 'nitropack/main.php' => [ 'NitroPack', '' ],
2853 + 'swift-performance-lite/performance.php' => [ 'Swift Performance Lite', '' ],
2854 + 'wp-cloudflare-page-cache/wp-cloudflare-page-cache.php' => [ 'Super Page Cache', '' ],
2855 + 'flying-press/flying-press.php' => [ 'FlyingPress', '' ],
2856 + 'redis-cache/redis-cache.php' => [ 'Redis Object Cache', '' ],
2857 + 'powered-cache/powered-cache.php' => [ 'Powered Cache', '' ],
2858 + 'docket-cache/docket-cache.php' => [ 'Docket Cache', '' ],
2859 + 'seraphinite-accelerator/plugin_root.php' => [ 'Seraphinite Accelerator', '' ],
2860 + ];
2861 + }
2862 +
2863 + /**
2864 + * The active caching plugin's entry, if there is one.
2865 + *
2866 + * Detection is by plugin path, mirroring is_any_smtp_plugin_active(), including
2867 + * the multisite network-active merge. First match in
2868 + * get_known_caching_plugins() wins; that array's order is the precedence.
2869 + *
2870 + * @since 2.12.7
2871 + * @return array{0:string,1:string}|null Name and doc slug, or null when none is active.
2872 + */
2873 + private static function get_active_caching_plugin_entry() {
2874 + $active_plugins = (array) get_option( 'active_plugins', [] );
2875 +
2876 + // For multisite, merge sitewide active plugins.
2877 + if ( is_multisite() ) {
2878 + $network_plugins = (array) get_site_option( 'active_sitewide_plugins', [] );
2879 + $active_plugins = array_merge( $active_plugins, array_keys( $network_plugins ) );
2880 + }
2881 +
2882 + foreach ( self::get_known_caching_plugins() as $path => $entry ) {
2883 + if ( in_array( $path, $active_plugins, true ) ) {
2884 + return $entry;
2885 + }
2886 + }
2887 +
2888 + return null;
2642 2889 }
2643 2890
2644 2891 }