diff --git a/README.txt b/README.txt index d52c9e9..0cf120e 100644 --- a/README.txt +++ b/README.txt @@ -3,7 +3,7 @@ Contributors: xecdev, klakurka Donate link: https://donate.paybutton.org/ Tags: paywall, monetization, donation, crypto, ecash Requires at least: 5.0 -Tested up to: 6.7 +Tested up to: 6.8 Requires PHP: 7.0 Stable tag: 3.0.0 PayButton Client: 4.1.0 diff --git a/includes/class-paybutton-ajax.php b/includes/class-paybutton-ajax.php index 8a7a8cb..500da60 100644 --- a/includes/class-paybutton-ajax.php +++ b/includes/class-paybutton-ajax.php @@ -19,7 +19,7 @@ class PayButton_AJAX { * - Fires if the visitor is not recognized as logged in by WordPress. * * Since our plugin implements a separate "pay-to-login" process (storing user wallet - * addresses in sessions), from WP’s point of view, most of our pay-to-login users + * addresses in cookies), from WP’s point of view, most of our pay-to-login users * are still not "logged in" in the standard WordPress sense. * * If we want both WP-logged-in and non-WP-logged-in visitors to access the same @@ -172,15 +172,12 @@ private function verify_signature($payload, $signature, $public_key_hex) { * in a variable called pb_paywall_user_wallet_address from the handleLogin() method * of the "paybutton-paywall-cashtab-login.js" file via AJAX. * - * This function verifies the AJAX nonce for security, ensures a PHP session is active, - * sanitizes the 'address' field from the POST data, and then stores it in both the session - * (under 'pb_paywall_user_wallet_address') and as a cookie (lasting 30 days). + * This function verifies the AJAX nonce for security, + * sanitizes the 'address' field from the POST data, and then stores it in + * a cookie (lasting a week). */ public function save_address() { check_ajax_referer( 'paybutton_paywall_nonce', 'security' ); - if ( ! session_id() ) { - session_start(); - } $address = sanitize_text_field( $_POST['address'] ); // Retrieve the blacklist and check the address @@ -191,45 +188,20 @@ public function save_address() { } // blacklist End - $_SESSION['pb_paywall_user_wallet_address'] = $address; - - // Write the new cookie - setcookie( - 'pb_paywall_user_wallet_address', - $address, - time() + 2592000, - COOKIEPATH ?: '/', - COOKIE_DOMAIN ?: '', - is_ssl(), - true - ); - - wp_send_json_success( array( 'message' => 'Address stored in session & cookie' ) ); + PayButton_State::set_address( $address ); wp_send_json_success(); } /** * Logs the user out via AJAX. * - * This function verifies the AJAX nonce for security and ensures a PHP session is active. - * It then removes the stored 'pb_paywall_user_wallet_address' from the session and clears - * the corresponding cookie. Additionally, it unsets any session data tracking paid articles. + * This function verifies the AJAX nonce for security. + * It then removes the stored 'pb_paywall_user_wallet_address' from the cookie and clears + * the corresponding cookie. Additionally, it unsets any cookie data tracking paid articles. */ public function logout() { check_ajax_referer( 'paybutton_paywall_nonce', 'security' ); - if ( ! session_id() ) { - session_start(); - } - unset( $_SESSION['pb_paywall_user_wallet_address'] ); - setcookie( - 'pb_paywall_user_wallet_address', - '', - time() - 3600, - COOKIEPATH ?: '/', - COOKIE_DOMAIN ?: '', - is_ssl(), - true - ); - unset( $_SESSION['paid_articles'] ); + PayButton_State::clear_address(); + PayButton_State::clear_articles(); wp_send_json_success( array( 'message' => 'Logged out' ) ); } @@ -238,9 +210,6 @@ public function logout() { */ public function mark_payment_successful() { check_ajax_referer( 'paybutton_paywall_nonce', 'security' ); - if ( ! session_id() ) { - session_start(); - } $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0; $tx_hash = isset( $_POST['tx_hash'] ) ? sanitize_text_field( $_POST['tx_hash'] ) : ''; @@ -255,14 +224,14 @@ public function mark_payment_successful() { } if ( $post_id > 0 ) { - // Mark this post as "unlocked" in the session - $_SESSION['paid_articles'][ $post_id ] = true; + // Mark this post as "unlocked" in the cookie + PayButton_State::add_article( $post_id ); - // Determine if user was "logged in" (i.e., session has a stored user wallet address) - $is_logged_in = ! empty( $_SESSION['pb_paywall_user_wallet_address'] ) ? 1 : 0; + // Determine if user was "logged in" (i.e., cookie has a stored user wallet address) + $is_logged_in = PayButton_State::get_address() ? 1 : 0; // Decide which address to store: - $address_to_store = $is_logged_in ? sanitize_text_field( $_SESSION['pb_paywall_user_wallet_address'] ) : $user_address; + $address_to_store = $is_logged_in ? sanitize_text_field( PayButton_State::get_address() ) : $user_address; // If we have any address to store, insert a record if ( ! empty( $address_to_store ) ) { diff --git a/includes/class-paybutton-public.php b/includes/class-paybutton-public.php index 3c26a21..e0ce6ff 100644 --- a/includes/class-paybutton-public.php +++ b/includes/class-paybutton-public.php @@ -111,8 +111,8 @@ public function enqueue_public_assets() { wp_localize_script( 'paybutton-cashtab-login', 'PaywallAjax', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'paybutton_paywall_nonce' ), - 'isUserLoggedIn' => ! empty( $_SESSION['pb_paywall_user_wallet_address'] ) ? 1 : 0, - 'userAddress' => ! empty( $_SESSION['pb_paywall_user_wallet_address'] ) ? sanitize_text_field( $_SESSION['pb_paywall_user_wallet_address'] ) : '', + 'isUserLoggedIn' => PayButton_State::get_address() ? 1 : 0, + 'userAddress' => sanitize_text_field( PayButton_State::get_address() ), 'defaultAddress' => get_option( 'paybutton_admin_wallet_address', '' ), 'scrollToUnlocked' => get_option( 'paybutton_scroll_to_unlocked', '1' ), ) ); @@ -154,7 +154,7 @@ private function load_public_template( $template_name, $args = array() ) { * Output the sticky header HTML. */ public function output_sticky_header() { - $user_wallet_address = ! empty( $_SESSION['pb_paywall_user_wallet_address'] ) ? sanitize_text_field( $_SESSION['pb_paywall_user_wallet_address'] ) : ''; + $user_wallet_address = sanitize_text_field( PayButton_State::get_address() ); $this->load_public_template( 'sticky-header', array( 'user_wallet_address' => $user_wallet_address ) ); @@ -237,7 +237,7 @@ public function paybutton_paywall_shortcode( $atts, $content = null ) { * @return string */ public function profile_shortcode() { - $user_wallet_address = ! empty( $_SESSION['pb_paywall_user_wallet_address'] ) ? sanitize_text_field( $_SESSION['pb_paywall_user_wallet_address'] ) : ''; + $user_wallet_address = sanitize_text_field( PayButton_State::get_address() ); if ( empty( $user_wallet_address ) ) { return '
You must be logged in to view your unlocked content.
'; } @@ -259,14 +259,11 @@ public function profile_shortcode() { * Checks if the given post is unlocked for the current user. */ private function post_is_unlocked( $post_id ) { - if ( ! session_id() ) { - session_start(); - } - if ( ! empty( $_SESSION['paid_articles'][ $post_id ] ) && $_SESSION['paid_articles'][ $post_id ] === true ) { + if ( isset( PayButton_State::get_articles()[ $post_id ] ) ) { return true; } - if ( ! empty( $_SESSION['pb_paywall_user_wallet_address'] ) ) { - $address = sanitize_text_field( $_SESSION['pb_paywall_user_wallet_address'] ); + $addr = PayButton_State::get_address(); if ( $addr ) { + $address = sanitize_text_field( $addr ); if ( $this->is_unlocked_in_db( $address, $post_id ) ) { return true; } diff --git a/includes/class-paybutton-state.php b/includes/class-paybutton-state.php new file mode 100644 index 0000000..6fca410 --- /dev/null +++ b/includes/class-paybutton-state.php @@ -0,0 +1,237 @@ += 70300 ) { + setcookie( + self::COOKIE_USER_ADDR, + $cookieValue, + [ + 'expires' => time() + self::TTL, + 'path' => '/', + 'domain' => COOKIE_DOMAIN ?: '', + 'secure' => is_ssl(), + 'httponly' => true, + 'samesite' => 'Lax', + ] + ); + } else { + //Fall back to a raw header with SameSite=Lax for older PHP versions + $expiry = gmdate( 'D, d-M-Y H:i:s T', time() + self::TTL ); + $header = sprintf( + '%s=%s; Expires=%s; Path=%s; Domain=%s; %s; HttpOnly; SameSite=Lax', + self::COOKIE_USER_ADDR, + $cookieValue, + $expiry, + '/', + COOKIE_DOMAIN ?: '', + is_ssl() ? 'Secure' : '' + ); + header( 'Set-Cookie: ' . $header, false ); + } + $_COOKIE[ self::COOKIE_USER_ADDR ] = $cookieValue; + } + + /** + * Retrieve and validate the wallet address from cookie + */ + public static function get_address() { + if ( empty( $_COOKIE[ self::COOKIE_USER_ADDR ] ) ) { + return ''; + } + if ( ! self::verify_and_extract( $_COOKIE[ self::COOKIE_USER_ADDR ], $addr ) ) { + return ''; + } + return $addr; + } + + /** + * Clear the wallet address cookie + */ + public static function clear_address() { + if ( PHP_VERSION_ID >= 70300 ) { + setcookie( + self::COOKIE_USER_ADDR, + '', + [ + 'expires' => time() - 3600, + 'path' => '/', + 'domain' => COOKIE_DOMAIN ?: '', + 'secure' => is_ssl(), + 'httponly' => true, + 'samesite' => 'Lax', + ] + ); + } else { + //Fall back to a raw header with SameSite=Lax for older PHP versions + $header = sprintf( + '%s=; Expires=%s; Path=%s; Domain=%s; %s; HttpOnly; SameSite=Lax', + self::COOKIE_USER_ADDR, + gmdate( 'D, d-M-Y H:i:s T', time() - 3600 ), + '/', + COOKIE_DOMAIN ?: '', + is_ssl() ? 'Secure' : '' + ); + header( 'Set-Cookie: ' . $header, false ); + } + unset( $_COOKIE[ self::COOKIE_USER_ADDR ] ); + } + + /** + * Add a post ID to the unlocked content cookie + */ + public static function add_article( $post_id ) { + $list = array_keys( self::get_articles() ); + $list[] = (int) $post_id; + $json = wp_json_encode( array_values( array_unique( $list ) ) ); + $payload = base64_encode( $json ); + $cookieValue = self::make_cookie_value( $payload ); + + if ( isset( $_COOKIE[ self::COOKIE_CONTENT ] ) && + hash_equals( $_COOKIE[ self::COOKIE_CONTENT ], $cookieValue ) ) { + return; // nothing new → don’t send a Set-Cookie header, good for caching + } + + if ( PHP_VERSION_ID >= 70300 ) { + setcookie( + self::COOKIE_CONTENT, + $cookieValue, + [ + 'expires' => time() + self::TTL, + 'path' => '/', + 'domain' => COOKIE_DOMAIN ?: '', + 'secure' => is_ssl(), + 'httponly' => true, + 'samesite' => 'Lax', + ] + ); + } else { + //Fall back to a raw header with SameSite=Lax for older PHP versions + $expiry = gmdate( 'D, d-M-Y H:i:s T', time() + self::TTL ); + $header = sprintf( + '%s=%s; Expires=%s; Path=%s; Domain=%s; %s; HttpOnly; SameSite=Lax', + self::COOKIE_CONTENT, + $cookieValue, + $expiry, + '/', + COOKIE_DOMAIN ?: '', + is_ssl() ? 'Secure' : '' + ); + header( 'Set-Cookie: ' . $header, false ); + } + $_COOKIE[ self::COOKIE_CONTENT ] = $cookieValue; + } + + /** + * Get the list of unlocked post IDs from cookie + */ + public static function get_articles() { + if ( empty( $_COOKIE[ self::COOKIE_CONTENT ] ) ) { + return []; + } + if ( ! self::verify_and_extract( $_COOKIE[ self::COOKIE_CONTENT ], $payload ) ) { + return []; + } + $json = base64_decode( $payload ); + $article_ids = json_decode( wp_unslash( $json ), true ); + return is_array( $article_ids ) ? array_fill_keys( $article_ids, true ) : []; + } + + /** + * Clear the unlocked content cookie + */ + public static function clear_articles() { + if ( PHP_VERSION_ID >= 70300 ) { + setcookie( + self::COOKIE_CONTENT, + '', + [ + 'expires' => time() - 3600, + 'path' => '/', + 'domain' => COOKIE_DOMAIN ?: '', + 'secure' => is_ssl(), + 'httponly' => true, + 'samesite' => 'Lax', + ] + ); + } else { + //Fall back to a raw header with SameSite=Lax for older PHP versions + $header = sprintf( + '%s=; Expires=%s; Path=%s; Domain=%s; %s; HttpOnly; SameSite=Lax', + self::COOKIE_CONTENT, + gmdate( 'D, d-M-Y H:i:s T', time() - 3600 ), + '/', + COOKIE_DOMAIN ?: '', + is_ssl() ? 'Secure' : '' + ); + header( 'Set-Cookie: ' . $header, false ); + } + unset( $_COOKIE[ self::COOKIE_CONTENT ] ); + } +} \ No newline at end of file diff --git a/paybutton.php b/paybutton.php index 763f7c8..bd61036 100644 --- a/paybutton.php +++ b/paybutton.php @@ -30,6 +30,7 @@ require_once PAYBUTTON_PLUGIN_DIR . 'includes/class-paybutton-admin.php'; require_once PAYBUTTON_PLUGIN_DIR . 'includes/class-paybutton-public.php'; require_once PAYBUTTON_PLUGIN_DIR . 'includes/class-paybutton-ajax.php'; +require_once PAYBUTTON_PLUGIN_DIR . 'includes/class-paybutton-state.php'; /** * Registers the plugin's activation and deactivation hooks. @@ -45,11 +46,6 @@ // Initialize plugin functionality. add_action( 'plugins_loaded', function() { - // Start a PHP session if none exists. - if ( ! session_id() ) { - session_start(); - } - // Initialize admin functionality if in admin area. if ( is_admin() ) { new PayButton_Admin(); @@ -70,4 +66,4 @@ exit; } } -}); +}); \ No newline at end of file