PluginProbe
wpForo Forum / 3.2.1
wpForo Forum v3.2.1
3.2.1 3.2.0 3.1.7 3.1.6 3.1.5 3.1.4 3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.10 All 141 releases
← All changes | includes/functions.php +108 -0 3.1.4 → 3.2.1 View file →
@@ -2826,8 +2826,89 @@
2826 2826 return false;
2827 2827 }
2828 2828 }
2829 2829
2830 +/**
2831 + * Add a post to the guest's signed ownership cookie.
2832 + * Called when a guest creates a new post or topic.
2833 + *
2834 + * @param int $postid The post ID to add to ownership
2835 + */
2836 +function wpforo_add_guest_ownership( $postid ) {
2837 + // Respect the forum cookie policy, like every other wpForo cookie writer.
2838 + if( ! wpforo_setting( 'legal', 'cookies' ) ) return;
2839 +
2840 + $postid = (int) $postid;
2841 + if( ! $postid ) return;
2842 +
2843 + $owned = wpforo_get_guest_owned_posts();
2844 + $owned[] = $postid;
2845 +
2846 + // Keep the newest N ids only, so the cookie can't grow past browser limits.
2847 + $max = (int) apply_filters( 'wpforo_guest_ownership_max_posts', 100 );
2848 + $owned = array_values( array_unique( $owned ) );
2849 + if( $max > 0 && count( $owned ) > $max ) $owned = array_slice( $owned, - $max );
2850 +
2851 + $data = wp_json_encode( $owned );
2852 + if( ! is_string( $data ) ) return;
2853 +
2854 + $signature = hash_hmac( 'sha256', $data, wp_salt( 'auth' ) );
2855 + $cookie_value = base64_encode( $data ) . '.' . $signature;
2856 +
2857 + $expire = time() + ( 30 * DAY_IN_SECONDS );
2858 + $secure = is_ssl() && 'https' === parse_url( get_option( 'home' ), PHP_URL_SCHEME );
2859 + $path = ( COOKIEPATH != SITECOOKIEPATH ) ? SITECOOKIEPATH : COOKIEPATH;
2860 +
2861 + // Suppressed: output may already have started on some themes/hooks, and a
2862 + // failed cookie must degrade to "cannot edit", never to a PHP warning.
2863 + @setcookie( 'wpforo_guest_ownership', $cookie_value, $expire, $path, COOKIE_DOMAIN, $secure, true );
2864 +
2865 + // Make it readable within the same request.
2866 + $_COOKIE['wpforo_guest_ownership'] = $cookie_value;
2867 +}
2868 +
2869 +/**
2870 + * Get list of post IDs this guest owns (verified via signature).
2871 + * Returns empty array if cookie is missing, corrupted, or signature invalid.
2872 + *
2873 + * @return array List of post IDs
2874 + */
2875 +function wpforo_get_guest_owned_posts() {
2876 + $cookie = isset( $_COOKIE['wpforo_guest_ownership'] ) ? $_COOKIE['wpforo_guest_ownership'] : '';
2877 +
2878 + // The cookie is fully attacker-controlled: it can arrive as an array
2879 + // (wpforo_guest_ownership[]=x), which would fatal on strpos() in PHP 8.
2880 + if( ! is_string( $cookie ) || $cookie === '' ) return [];
2881 +
2882 + $parts = explode( '.', $cookie, 2 );
2883 + if( count( $parts ) !== 2 ) return [];
2884 +
2885 + list( $data_b64, $signature ) = $parts;
2886 + $data = base64_decode( $data_b64, true );
2887 + if( ! is_string( $data ) || $data === '' ) return [];
2888 +
2889 + // Verify HMAC signature - prevents forgery
2890 + $expected = hash_hmac( 'sha256', $data, wp_salt( 'auth' ) );
2891 + if( ! hash_equals( $expected, $signature ) ) return [];
2892 +
2893 + $owned = json_decode( $data, true );
2894 + if( ! is_array( $owned ) ) return [];
2895 +
2896 + return array_values( array_filter( array_map( 'intval', $owned ) ) );
2897 +}
2898 +
2899 +/**
2900 + * Check if the current guest owns a specific post.
2901 + * Uses cryptographically signed cookie - cannot be forged.
2902 + *
2903 + * @param int $postid The post ID to check
2904 + * @return bool True if guest owns this post
2905 + */
2906 +function wpforo_guest_owns_post( $postid ) {
2907 + $owned = wpforo_get_guest_owned_posts();
2908 + return in_array( (int) $postid, $owned, true );
2909 +}
2910 +
2830 2911 function wpforo_extra_html_parser( $extra_html = '', $allowed_html = [] ) {
2831 2912 if( $extra_html ) {
2832 2913 $extra_html = explode( ',', $extra_html );
2833 2914 $extra_html = array_filter( $extra_html );
@@ -2903,8 +2984,35 @@
2903 2984 function wpforo_decode( $data ) {
2904 2985 $data = is_array( $data ) ? array_map( 'wpforo_decode', $data ) : htmlspecialchars_decode( $data, ENT_QUOTES );
2905 2986
2906 2987 return $data;
2988 +}
2989 +
2990 +/**
2991 + * Build a REGEXP literal for searching inside a wp_json_encode() stored column.
2992 + *
2993 + * The haystack (profiles.fields, postmeta.metavalue) is written with wp_json_encode(),
2994 + * which escapes forward slashes ("S/4HANA" is stored as S\/4HANA) and non-ASCII
2995 + * characters (Ľ), so the needle must be encoded the same way or it can never match.
2996 + * Callers must still wrap the result in esc_sql() before putting it in a query.
2997 + *
2998 + * @param string $value Raw needle.
2999 + *
3000 + * @return string Regex-literal needle, without SQL escaping.
3001 + */
3002 +function wpforo_json_regexp_needle( $value ): string {
3003 + $value = (string) $value;
3004 +
3005 + // esc_sql() cannot reliably escape invalid UTF-8 on a utf8mb4 connection and
3006 + // wp_json_encode() returns false for it, so strip it before encoding.
3007 + $clean = wp_check_invalid_utf8( $value, true );
3008 + $json = ( ! is_string( $clean ) || $clean === '' ) ? false : wp_json_encode( $clean );
3009 +
3010 + // Never return an empty needle for a non-empty search term: in a substring
3011 + // pattern that would match every row. \u0000 cannot occur in stored field data.
3012 + if( ! is_string( $json ) || strlen( $json ) < 2 ) return ( $value === '' ) ? '' : preg_quote( '\u0000' );
3013 +
3014 + return preg_quote( substr( $json, 1, -1 ) );
2907 3015 }
2908 3016
2909 3017 function wpforo_trim( $data ) {
2910 3018 $data = is_array( $data ) ? array_map( 'wpforo_trim', $data ) : trim( (string) $data );