PluginProbe
wpForo Forum / 3.1.6
wpForo Forum v3.1.6
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 1.4.11 1.4.12 1.4.13 All 138 releases
← All changes | includes/functions.php +110 -9 3.0.83.1.6 View file →
@@ -784,9 +784,14 @@
784 784 $defined = explode( ',', trim( (string) $args ) );
785 785 } elseif( is_integer( $args ) || is_float( $args ) ) {
786 786 $defined[0] = $args;
787 787 } elseif( is_serialized( $args ) ) {
788 - $defined = unserialize( $args );
788 + // SECURITY: refuse to instantiate any class — only scalars and arrays
789 + // of scalars are recovered. Blocks PHP Object Injection via widget
790 + // AJAX (forumids etc.) and any other caller that may receive
791 + // attacker-controlled strings.
792 + $defined = unserialize( $args, [ 'allowed_classes' => false ] );
793 + if( ! is_array( $defined ) ) $defined = (array) $defined;
789 794 } elseif( strpos( (string) $args, '=' ) !== false ) {
790 795 parse_str( $args, $defined );
791 796 } else {
792 797 $defined = (array) $args;
@@ -810,9 +815,13 @@
810 815 if( ! function_exists( 'is_serialized' ) ) {
811 816 function is_serialized( $value ) {
812 817 if( $value == '' ) return false;
813 818 $value = trim( (string) $value );
814 - $chsd = @unserialize( $value );
819 + // SECURITY: the very act of detecting a serialized payload must not
820 + // instantiate classes — @unserialize() with the default options runs
821 + // __wakeup() / __destruct() for any embedded object even when the
822 + // caller only wanted to test the format. Limit to scalars/arrays.
823 + $chsd = @unserialize( $value, [ 'allowed_classes' => false ] );
815 824 if( $chsd !== false || $value === 'b:0;' ) {
816 825 return true;
817 826 } else {
818 827 return false;
@@ -1136,9 +1145,14 @@
1136 1145 if( $filter_views ) {
1137 1146 //to-do: don't increase views before all read point.
1138 1147 if( wpforo_setting( 'legal', 'cookies' ) ) {
1139 1148 $viwed_ids = wpforo_getcookie( wpforo_prefix( 'read_topics' ), false );
1140 - if( empty( $viwed_ids ) || ! wpfval( $viwed_ids, $data['topicid'] ) ) {
1149 + $has_viewed = ! empty( $viwed_ids ) && wpfval( $viwed_ids, $data['topicid'] );
1150 + if( ! $has_viewed && is_user_logged_in() ) {
1151 + $viwed_db_ids = wpforo_current_usermeta( wpforo_prefix( 'read_topics' ) );
1152 + $has_viewed = ! empty( $viwed_db_ids ) && wpfval( $viwed_db_ids, $data['topicid'] );
1153 + }
1154 + if( ! $has_viewed ) {
1141 1155 WPF()->db->query( "UPDATE `" . WPF()->tables->topics . "` SET `views` = `views` + 1 WHERE `topicid` = " . intval( $data['topicid'] ) );
1142 1156 }
1143 1157 } elseif( is_user_logged_in() ) {
1144 1158 if( wpfval( WPF()->current_usermeta, wpforo_prefix( 'read_topics' ) ) ) {
@@ -2812,8 +2826,89 @@
2812 2826 return false;
2813 2827 }
2814 2828 }
2815 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 +
2816 2911 function wpforo_extra_html_parser( $extra_html = '', $allowed_html = [] ) {
2817 2912 if( $extra_html ) {
2818 2913 $extra_html = explode( ',', $extra_html );
2819 2914 $extra_html = array_filter( $extra_html );
@@ -3875,20 +3970,26 @@
3875 3970
3876 3971 return _wpforo_apply_email_shortcodes( $txt, compact( 'forumid', 'topicid', 'postid', 'owner', 'user', 'unsubscribe_link' ) );
3877 3972 }
3878 3973
3879 -function wpforo_send_email( $email, $sbj, $msg, $headers = '' ) {
3974 +function wpforo_send_email( $email, $sbj, $msg, $headers = '', $context = 'general', $related_id = 0 ) {
3880 3975 if( defined( 'IS_GO2WPFORO' ) && IS_GO2WPFORO ) return false;
3881 3976 if( apply_filters( 'break_wpforo_send_email', false, $email, $sbj, $msg, $headers ) ) return false;
3882 - $key = func_get_args();
3977 + $key = [ $email, $sbj, substr( $msg, 0, 100 ) ];
3883 3978 if( WPF()->ram_cache->exists( $key ) ) return false;
3884 - add_filter( 'wp_mail_content_type', 'wpforo_set_html_content_type', 999 );
3885 - if( wp_mail( $email, $sbj, $msg, ( $headers ?: wpforo_mail_headers() ) ) ) {
3979 +
3980 + if( isset( WPF()->email_queue ) ) {
3981 + $result = WPF()->email_queue->queue_or_send( $email, $sbj, $msg, $headers, $context, $related_id );
3982 + } else {
3983 + add_filter( 'wp_mail_content_type', 'wpforo_set_html_content_type', 999 );
3984 + $result = wp_mail( $email, $sbj, $msg, ( $headers ?: wpforo_mail_headers() ) );
3985 + remove_filter( 'wp_mail_content_type', 'wpforo_set_html_content_type' );
3986 + }
3987 +
3988 + if( $result ) {
3886 3989 WPF()->ram_cache->set( $key, true );
3887 -
3888 3990 return true;
3889 3991 }
3890 - remove_filter( 'wp_mail_content_type', 'wpforo_set_html_content_type' );
3891 3992
3892 3993 return false;
3893 3994 }
3894 3995