can_generate_link( $user_id ); if ( is_wp_error( $can ) ) { return $can; } $db = ADSD_RCC_DB::instance(); $s = ADSD_RCC_Settings::instance(); $token = ADSD_RCC_Crypto::short_code( 8 ); // Ensure token uniqueness while ( $db->get_link_by_token( $token ) ) { $token = ADSD_RCC_Crypto::short_code( 8 ); } $expires_at = null; $expiry_days = $s->code_expiry_days(); if ( $expiry_days > 0 ) { $expires_at = gmdate( 'Y-m-d H:i:s', strtotime( "+{$expiry_days} days" ) ); } $link_id = $db->insert_referral_link( [ 'user_id' => $user_id, 'token' => $token, 'device_hash' => ADSD_RCC_Crypto::device_hash(), 'expires_at' => $expires_at, 'status' => 'active', ] ); if ( ! $link_id ) { return new WP_Error( 'adsd_rcc_db_error', __( 'Could not generate referral link. Please try again.', 'adsd-referral-couponcore' ) ); } $register_page = $s->page_url( 'register' ); $url = add_query_arg( 'ref', $token, $register_page ); ADSD_RCC_DB::instance()->log( 'link_generated', $user_id, $link_id, 'Referral link generated.' ); return [ 'token' => $token, 'url' => $url, 'link_id' => $link_id, ]; } /** * Capture referral token from query string and store it via a cookie + transient. * Avoids PHP native sessions which break under page caching and many managed hosts. */ public function capture_referral_token(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- referral token from URL param, read-only tracking, no state mutation requiring nonce if ( ! isset( $_GET['ref'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $token = sanitize_text_field( wp_unslash( $_GET['ref'] ) ); // Store in a first-party cookie (7-day window, httponly, samesite=Lax) $this->set_ref_cookie( $token ); // Track the click $db = ADSD_RCC_DB::instance(); $link = $db->get_link_by_token( $token ); if ( $link ) { $db->increment_link_click( (int) $link->id ); } } /** * Get the referral token stored in the cookie. */ public function get_session_token(): string { $token = sanitize_text_field( wp_unslash( $_COOKIE['adsd_rcc_ref_token'] ?? '' ) ); return $token; } /** * Clear the referral cookie after use. */ public function clear_session_token(): void { $this->set_ref_cookie( '', time() - HOUR_IN_SECONDS ); } /** * Write (or delete) the referral cookie. * * @param string $value Token value, or empty string to delete. * @param int $expire Unix timestamp; defaults to 7 days from now. */ private function set_ref_cookie( string $value, int $expire = 0 ): void { if ( ! $expire ) { $expire = time() + 7 * DAY_IN_SECONDS; } // Use secure flag when site is HTTPS $secure = is_ssl(); // setcookie() must be called before any output. On init this is guaranteed. setcookie( 'adsd_rcc_ref_token', $value, [ 'expires' => $expire, 'path' => COOKIEPATH ?: '/', 'domain' => COOKIE_DOMAIN ?: '', 'secure' => $secure, 'httponly' => true, 'samesite' => 'Lax', ] ); } // ─── Referral Recording ─────────────────────────────────────────────────── /** * Record a pending referral at registration time. * * @return int|WP_Error Referral ID or error. */ public function record_pending_referral( string $referee_email, string $token ) { $db = ADSD_RCC_DB::instance(); $link = $db->get_link_by_token( $token ); if ( ! $link ) { return new WP_Error( 'adsd_rcc_invalid_token', __( 'Invalid referral code.', 'adsd-referral-couponcore' ) ); } // Check link expiry if ( $link->expires_at && strtotime( $link->expires_at ) < time() ) { return new WP_Error( 'adsd_rcc_link_expired', __( 'This referral link has expired.', 'adsd-referral-couponcore' ) ); } $guard = ADSD_RCC_Fraud_Guard::instance(); $can = $guard->can_register_as_referee( $referee_email, (int) $link->user_id ); if ( is_wp_error( $can ) ) { return $can; } $referral_id = $db->insert_referral( [ 'link_id' => $link->id, 'referrer_id' => $link->user_id, 'referee_email'=> $referee_email, 'status' => 'pending', 'ip_address' => ADSD_RCC_Crypto::get_client_ip(), 'device_hash' => ADSD_RCC_Crypto::device_hash(), ] ); if ( ! $referral_id ) { return new WP_Error( 'adsd_rcc_db_error', __( 'Could not record referral. Please try again.', 'adsd-referral-couponcore' ) ); } return $referral_id; } /** * Mark referral as verified and trigger reward generation. */ public function confirm_referral( int $referral_id, int $referee_user_id ): bool { $db = ADSD_RCC_DB::instance(); $referral = $db->get_referral( $referral_id ); if ( ! $referral || $referral->status !== 'pending' ) { return false; } $db->update_referral_status( $referral_id, 'verified', [ 'referee_id' => $referee_user_id, 'verified_at' => current_time( 'mysql', true ), ] ); // Trigger reward generation $reward_engine = ADSD_RCC_Reward_Engine::instance(); $reward_engine->generate_rewards_for_referral( $referral_id ); $db->log( 'referral_confirmed', $referee_user_id, $referral_id, 'Referral confirmed and reward generated.' ); return true; } /** * Expire links past their expiry date. */ public function expire_old_links(): void { global $wpdb; // Table name is built from $wpdb->prefix + plugin constant — never user input. $table = esc_sql( $wpdb->prefix . ADSD_RCC_TABLE_PREFIX . 'referral_links' ); // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.Security.NonceVerification.Recommended $wpdb->query( $wpdb->prepare( "UPDATE `{$table}` SET status = 'expired' WHERE status = 'active' AND expires_at IS NOT NULL AND expires_at < NOW()" ) ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.Security.NonceVerification.Recommended ADSD_RCC_DB::instance()->expire_overdue_rewards(); } // ─── Dashboard Data ─────────────────────────────────────────────────────── public function get_dashboard_data( int $user_id ): array { $db = ADSD_RCC_DB::instance(); $settings = ADSD_RCC_Settings::instance(); return [ 'links' => $db->get_links_by_user( $user_id ), 'referrals' => $db->get_referrals_by_user( $user_id ), 'rewards' => $db->get_rewards_for_user( $user_id ), 'daily_limit' => $settings->daily_link_limit(), 'links_today' => $db->count_links_today( $user_id ), ]; } }