| @@ -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 | |