Making WordPress.org


Ignore:
Timestamp:
02/18/2022 03:02:37 AM (3 years ago)
Author:
dd32
Message:

Support Forums: Add unsubscription links to plugin/theme/tag subscription emails.

Note: This does not add unsubscription links to bbPress emails, only the ones this plugin adds.
Emails from bbPress for forum / topic subscription ('notify me of replies') will not have these links.

See #5505, #3456.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-bbp-term-subscription/inc/class-plugin.php

    r11533 r11580  
    77    /**
    88     * @todo AJAXify subscription action.
    9      * @todo Add unsubscribe link to outgoing emails.
    10      */
    11 
    12     private $subscribers = array();
     9     */
    1310
    1411    public $taxonomy  = false;
     
    1613    public $directory = false;
    1714
     15    protected $term        = false;
     16    protected $subscribers = array();
     17
    1818    const META_KEY = '_bbp_term_subscription';
     19
     20    /**
     21     * Valid actions for this plugin.
     22     */
     23    const VALID_ACTIONS = array(
     24        'wporg_bbp_subscribe_term',
     25        'wporg_bbp_unsubscribe_term',
     26    );
     27
     28    /**
     29     * Length of time the unsubscription links are valid.
     30     *
     31     * @var int
     32     */
     33    const UNSUBSCRIBE_LIFETIME = 604800; // WEEK_IN_SECONDS
    1934
    2035    public function __construct( $args = array() ) {
     
    2641                'subscribed_user_notice' => __( 'You are not currently subscribed to any topic tags.', 'wporg-forums' ),
    2742                'subscribed_anon_notice' => __( 'This user is not currently subscribed to any topic tags.', 'wporg-forums' ),
    28                 'receipt'                => __( 'You are receiving this email because you are subscribed to a topic tag.', 'wporg-forums'),
     43                'receipt'                => __( "You are receiving this email because you are subscribed to the %s tag.", 'wporg-forums'),
    2944            ),
    3045        ) );
     
    3449        $this->directory = $r['directory'];
    3550
    36         add_action( 'bbp_init', array( $this, 'bbp_init' ) );
    37     }
    38 
    39     /**
    40      * Initialize the plugin.
    41      */
    42     public function bbp_init() {
    43         // If the user isn't logged in, there will be no topics or replies added.
    44         if ( ! is_user_logged_in() ) {
    45             return;
    46         }
    47 
     51        // If no taxonomy was provided, there's nothing we can do.
    4852        if ( ! $this->taxonomy ) {
    4953            return;
    5054        }
    5155
     56        add_action( 'bbp_init', array( $this, 'bbp_init' ) );
     57    }
     58
     59    /**
     60     * Initialize the plugin.
     61     */
     62    public function bbp_init() {
    5263        // Add views and actions for users.
    5364        add_action( 'bbp_get_request', array( $this, 'term_subscribe_handler' ) );
     65        add_action( 'bbp_post_request', array( $this, 'term_subscribe_handler' ) );
     66        add_action( 'bbp_template_redirect', array( $this, 'fix_bbpress_post_actions' ), 9 ); // before bbp_get_request/bbp_post_request
    5467
    5568        // Notify subscribers when a topic or reply with a given term is added.
     
    6275        add_filter( 'bbp_subscription_mail_title',       array( $this, 'replace_topic_subscription_mail_title' ), 10, 3 );
    6376
     77        // Add a section to the user subscriptions list to allow management of subscriptions.
    6478        add_action( 'bbp_template_after_user_subscriptions', array( $this, 'user_subscriptions' ) );
    6579
     
    7084
    7185    /**
     86     * bbPress has two action handlers, GET and POST, a POST action cannot work with ?action= from the URL.
     87     *
     88     * This is used by the email-unsubscription handler.
     89     */
     90    public function fix_bbpress_post_actions() {
     91        if (
     92            bbp_is_post_request() &&
     93            empty( $_POST['action'] ) &&
     94            ! empty( $_GET['action'] ) &&
     95            in_array( $_GET['action'], self::VALID_ACTIONS, true )
     96        ) {
     97            $_POST['action'] = $_GET['action'];
     98        }
     99    }
     100
     101    /**
    72102     * Add a notice that you're subscribed to the tag/plugin/theme, or have just unsubscribed.
    73103     */
    74104    public function before_view() {
    75         $term = false;
    76 
    77         if ( $this->directory && $this->directory->slug && $this->directory->term ) {
    78             $term      = $this->directory->term;
    79             $term_name = $this->directory->title();
    80         } elseif ( bbp_is_topic_tag() ) {
    81             $term = get_queried_object();
    82             if ( empty( $term->taxonomy ) || $term->taxonomy !== $this->taxonomy ) {
    83                 return;
    84             }
    85 
    86             $term_name = $term->name;
    87         }
    88 
    89         if ( empty( $term->term_id ) ) {
     105        $term = $this->get_current_term();
     106
     107        if ( ! $term || $term->taxonomy !== $this->taxonomy ) {
    90108            return;
    91109        }
     110
     111        do_action( 'bbp_template_notices' );
    92112
    93113        $is_subscribed = self::is_user_subscribed_to_term( get_current_user_id(), $term->term_id );
     
    95115        if ( $is_subscribed ) {
    96116            $message = sprintf(
    97                 __( 'You are subscribed to this forum, and will receive emails for future topic activity. <a href="%1$s">Unsubscribe from %2$s</a>', 'wporg-forums' ),
     117                __( 'You are subscribed to this forum, and will receive emails for future topic activity. <a href="%1$s">Unsubscribe from %2$s.</a>', 'wporg-forums' ),
    98118                self::get_subscription_url( get_current_user_id(), $term->term_id, $this->taxonomy ),
    99                 esc_html( $term_name )
     119                esc_html( $term->name )
    100120            );
    101121        } elseif ( ! empty( $_GET['success'] ) && 'subscription-removed' === $_GET['success'] ) {
    102122            $message = sprintf(
    103123                __( 'You have been unsubscribed from future emails for %1$s.', 'wporg-forums' ),
    104                 esc_html( $term_name )
     124                esc_html( $term->name )
    105125            );
    106126        } else {
     
    122142    public function term_subscribe_handler( $action = '' ) {
    123143        // Bail if the actions aren't meant for this function.
    124         if ( ! in_array( $action, self::get_valid_actions() ) ) {
     144        if ( ! in_array( $action, self::VALID_ACTIONS ) ) {
    125145            return;
    126146        }
    127147
    128         if ( ! $this->taxonomy ) {
     148        if ( ! bbp_is_subscriptions_active() ) {
     149            return false;
     150        }
     151
     152        // Determine the term the request is for, overwrite with ?term_id if specified.
     153        $term = $this->get_current_term();
     154        if ( ! empty( $_GET['term_id'] ) ) {
     155            $term = get_term( intval( $_GET['term_id'] ), $this->taxonomy );
     156        }
     157        if ( ! $term ) {
    129158            return;
    130159        }
    131160
    132         // Taxonomy mismatch; a different instance should handle this.
    133         if ( ! isset( $_GET['taxonomy'] ) || $this->taxonomy != $_GET['taxonomy'] ) {
    134             return;
    135         }
    136 
    137         if ( ! bbp_is_subscriptions_active() ) {
    138             return false;
    139         }
    140 
    141         // Bail if the actions aren't meant for this function.
    142         if ( ! in_array( $action, self::get_valid_actions() ) ) {
    143             return;
    144         }
    145 
    146         // Bail if no term id is passed.
    147         if ( ! isset( $_GET['term_id'] ) || empty( $_GET['term_id'] ) ) {
    148             return;
    149         }
    150 
    151         // Get required data.
     161        $term_id = $term->term_id;
     162        $auth    = 'nonce';
    152163        $user_id = get_current_user_id();
    153         $term_id = intval( $_GET['term_id'] );
    154         $term = get_term( $term_id, $this->taxonomy );
     164
     165        // If a user_id + token is provided, verify the request and maybe use the provided user_id.
     166        if ( isset( $_GET['token'] ) ) {
     167            $auth    = 'token';
     168            $user_id = $this->has_valid_unsubscription_token();
     169
     170            if ( ! $user_id ) {
     171                bbp_add_error( 'wporg_bbp_subscribe_invalid_token', __( '<strong>ERROR</strong>: Link expired!', 'wporg-forums' ) );
     172                return false;
     173            }
     174
     175            // Require a POST request for verification. Gmail will bypass this for one-click unsubscriptions by POST'ing to the URL.
     176            if ( empty( $_POST ) ) {
     177                wp_die(
     178                    sprintf(
     179                        '<h1>%1$s</h1>' .
     180                        '<p>%2$s</p>' .
     181                        '<form method="POST" action="%3$s">' .
     182                            '<input type="submit" name="confirm" value="%4$s">' .
     183                            '&nbsp<a href="%5$s">%6$s</a>' .
     184                        '</form>',
     185                        get_bloginfo('name'),
     186                        sprintf(
     187                            /* translators: 1: Plugin, Theme, or Tag name. */
     188                            esc_html__( 'Do you wish to unsubscribe from future emails for %s?', 'wporg-forums' ),
     189                            $term->name
     190                        ),
     191                        esc_attr( $_SERVER['REQUEST_URI'] ),
     192                        esc_attr__( 'Yes, unsubscribe me', 'wporg-forums' ),
     193                        get_term_link( $term ),
     194                        sprintf(
     195                            /* translators: 1: Plugin, Theme, or Tag name. */
     196                            esc_attr__( 'No, take me to the %s forum.', 'wporg-forums' ),
     197                            $term->name
     198                        )
     199                    )
     200                );
     201                exit;
     202            }
     203
     204        }
    155205
    156206        // Check for empty term id.
    157         if ( ! $term ) {
    158             /* translators: Term: topic tag */
    159             bbp_add_error( 'wporg_bbp_subscribe_term_id', __( '<strong>ERROR</strong>: No term was found! Which term are you subscribing/unsubscribing to?', 'wporg-forums' ) );
    160 
    161         // Check for current user.
    162         } elseif ( empty( $user_id ) ) {
     207        if ( empty( $user_id ) ) {
    163208            bbp_add_error( 'wporg_bbp_subscribe_logged_id', __( '<strong>ERROR</strong>: You must be logged in to do this!', 'wporg-forums' ) );
    164209
    165210        // Check nonce.
    166         } elseif ( ! bbp_verify_nonce_request( 'toggle-term-subscription_' . $user_id . '_' . $term_id . '_' . $this->taxonomy ) ) {
     211        } elseif ( 'nonce' === $auth && ! bbp_verify_nonce_request( 'toggle-term-subscription_' . $user_id . '_' . $term_id . '_' . $this->taxonomy ) ) {
    167212            bbp_add_error( 'wporg_bbp_subscribe_nonce', __( '<strong>ERROR</strong>: Are you sure you wanted to do that?', 'wporg-forums' ) );
    168213
    169         // Check current user's ability to spectate.
    170         } elseif ( ! current_user_can( 'spectate' ) ) {
     214        // Check user's ability to spectate.
     215        } elseif ( ! user_can( $user_id, 'spectate' ) ) {
    171216            bbp_add_error( 'wporg_bbp_subscribe_permissions', __( '<strong>ERROR</strong>: You don\'t have permission to do this!', 'wporg-forums' ) );
    172217        }
     
    176221        }
    177222
     223        $success       = false;
    178224        $is_subscribed = self::is_user_subscribed_to_term( $user_id, $term_id );
    179         $success       = false;
    180 
    181         if ( true === $is_subscribed && 'wporg_bbp_unsubscribe_term' === $action ) {
    182             $success = self::remove_user_subscription( $user_id, $term_id );
    183         } elseif ( false === $is_subscribed && 'wporg_bbp_subscribe_term' === $action ) {
    184             $success = self::add_user_subscription( $user_id, $term_id );
     225
     226        if ( 'wporg_bbp_unsubscribe_term' === $action ) {
     227            $success = ! $is_subscribed || self::remove_user_subscription( $user_id, $term_id );
     228        } elseif ( 'wporg_bbp_subscribe_term' === $action ) {
     229            $success = $is_subscribed || self::add_user_subscription( $user_id, $term_id );
    185230        }
    186231
     
    224269        }
    225270
    226         foreach ( $terms as $term ) {
    227             $subscribers = $this->get_term_subscribers( $term->term_id );
    228             if ( $subscribers ) {
    229                 $this->subscribers = array_unique( array_merge( $subscribers, $this->subscribers ) );
    230             }
    231         }
     271        // Users that will be notified another way, or have already been notified.
     272        $notified_users = array();
     273
     274        // Remove the author from being notified of their own topic.
     275        $notified_users[] = bbp_get_topic_author_id( $topic_id );
    232276
    233277        // Get users who were already notified and exclude them.
    234278        $forum_subscribers = bbp_get_forum_subscribers( $forum_id, true );
    235279        if ( ! empty( $forum_subscribers ) ) {
    236             $this->subscribers = array_diff( $this->subscribers, $forum_subscribers );
    237         }
    238 
    239         // Remove the author from being notified of their own topic.
    240         $this->subscribers = array_diff( $this->subscribers, array( bbp_get_topic_author_id( $topic_id ) ) );
    241 
    242         if ( empty( $this->subscribers ) ) {
    243             return;
     280            $notified_users = array_merge( $notified_users, $forum_subscribers );
    244281        }
    245282
     
    250287        add_filter( 'bbp_forum_subscription_user_ids', array( $this, 'add_term_subscribers' ) );
    251288
    252         // Actually notify our term subscribers.
    253         bbp_notify_forum_subscribers( $topic_id, $forum_id );
     289        // Personalize the emails.
     290        add_filter( 'wporg_bbp_subscription_email', array( $this, 'personalize_subscription_email' ) );
     291
     292        foreach ( $terms as $term ) {
     293            $subscribers = $this->get_term_subscribers( $term->term_id );
     294            if ( ! $subscribers ) {
     295                continue;
     296            }
     297
     298            $subscribers = array_diff( $subscribers, $notified_users );
     299            if ( ! $subscribers ) {
     300                continue;
     301            }
     302
     303            $this->term        = $term;
     304            $this->subscribers = $subscribers;
     305
     306            // Actually notify the term subscribers.
     307            bbp_notify_forum_subscribers( $topic_id, $forum_id );
     308
     309            // Don't email them twice.
     310            $notified_users = array_merge( $notified_users, $subscribers );
     311        }
     312
     313        // Reset
     314        $this->term        = false;
     315        $this->subscribers = array();
    254316
    255317        // Remove filters.
     318        remove_filter( 'bbp_forum_subscription_mail_message', array( $this, 'replace_forum_subscription_mail_message' ) );
    256319        remove_filter( 'bbp_forum_subscription_user_ids',     array( $this, 'add_term_subscribers' ) );
    257         remove_filter( 'bbp_forum_subscription_mail_message', array( $this, 'replace_forum_subscription_mail_message' ), 10 );
    258 
     320        remove_filter( 'wporg_bbp_subscription_email',        array( $this, 'personalize_subscription_email' ) );
    259321    }
    260322
     
    284346        $topic_url     = get_permalink( $topic_id );
    285347
    286         $message = sprintf( __( '%1$s wrote:
     348        $message = sprintf(
     349            /* translators: 1: Author, 2: Forum message, 3: Link to topic, 4: Descriptive text of why they're getting the email */
     350            __( '%1$s wrote:
    287351
    288352%2$s
    289353
    290 Topic Link: %3$s
     354Link: %3$s
    291355
    292356-----------
    293357
     358To reply, visit the above link and log in.
     359Note that replying to this email has no effect.
     360
    294361%4$s
    295362
    296 Log in and visit the topic to reply to the topic or unsubscribe from these emails. Note that replying to this email has no effect.', 'wporg-forums' ),
     363To unsubscribe from future emails click here:
     364####UNSUB_LINK####', 'wporg-forums' ),
    297365            $topic_author_name,
    298366            $topic_content,
    299367            $topic_url,
    300             // String may not have placeholders, ie. in the case of tags.
    301             sprintf( $this->labels['receipt'], $this->directory ? $this->directory->title() : '' )
     368            sprintf(
     369                $this->labels['receipt'],
     370                $this->get_current_term()->name
     371            )
    302372        );
    303373
     
    345415        }
    346416
    347         foreach ( $terms as $term ) {
    348             $subscribers = $this->get_term_subscribers( $term->term_id );
    349             if ( $subscribers ) {
    350                 $this->subscribers = array_unique( array_merge( $subscribers, $this->subscribers ) );
    351             }
    352         }
     417        // Users that will be notified another way, or have already been notified.
     418        $notified_users = array();
     419
     420        // Remove the author from being notified of their own topic.
     421        $notified_users[] = bbp_get_reply_author_id( $reply_id );
    353422
    354423        // Get users who were already notified and exclude them.
    355424        $topic_subscribers = bbp_get_topic_subscribers( $topic_id, true );
    356425        if ( ! empty( $topic_subscribers ) ) {
    357             $this->subscribers = array_diff( $this->subscribers, $topic_subscribers );
    358         }
    359 
    360         // Remove the author from being notified of their own reply.
    361         $this->subscribers = array_diff( $this->subscribers, array( bbp_get_reply_author_id( $reply_id ) ) );
    362 
    363         if ( empty( $this->subscribers ) ) {
    364             return;
     426            $notified_users = array_merge( $notified_users, $topic_subscribers );
    365427        }
    366428
     
    371433        add_filter( 'bbp_topic_subscription_user_ids', array( $this, 'add_term_subscribers' ) );
    372434
    373         // Actually notify our term subscribers.
    374         bbp_notify_topic_subscribers( $reply_id, $topic_id, $forum_id );
     435        // Personalize the emails.
     436        add_filter( 'wporg_bbp_subscription_email', array( $this, 'personalize_subscription_email' ) );
     437
     438        foreach ( $terms as $term ) {
     439            $subscribers = $this->get_term_subscribers( $term->term_id );
     440            if ( ! $subscribers ) {
     441                continue;
     442            }
     443
     444            $subscribers = array_diff( $subscribers, $notified_users );
     445            if ( ! $subscribers ) {
     446                continue;
     447            }
     448
     449            $this->term        = $term;
     450            $this->subscribers = $subscribers;
     451
     452            // Actually notify our term subscribers.
     453            bbp_notify_topic_subscribers( $reply_id, $topic_id, $forum_id );
     454
     455            // Don't email them twice.
     456            $notified_users = array_merge( $notified_users, $subscribers );
     457        }
     458
     459        // Reset
     460        $this->term        = false;
     461        $this->subscribers = array();
    375462
    376463        // Remove filters.
     464        remove_filter( 'bbp_subscription_mail_message',   array( $this, 'replace_topic_subscription_mail_message' ) );
    377465        remove_filter( 'bbp_topic_subscription_user_ids', array( $this, 'add_term_subscribers' ) );
    378         remove_filter( 'bbp_subscription_mail_message',   array( $this, 'replace_topic_subscription_mail_message' ) );
     466        remove_filter( 'wporg_bbp_subscription_email',    array( $this, 'personalize_subscription_email' ) );
    379467    }
    380468
     
    396484        $reply_url = bbp_get_reply_url( $reply_id );
    397485
    398         $message = sprintf( __( '%1$s wrote:
     486        $message = sprintf(
     487            /* translators: 1: Author, 2: Forum message, 3: Link to topic, 4: Descriptive text of why they're getting the email */
     488            __( '%1$s wrote:
    399489
    400490%2$s
    401491
    402 Reply Link: %3$s
     492Link: %3$s
    403493
    404494-----------
    405495
     496To reply, visit the above link and log in.
     497Note that replying to this email has no effect.
     498
    406499%4$s
    407500
    408 Log in and visit the topic to reply to the topic or unsubscribe from these emails. Note that replying to this email has no effect.', 'wporg-forums' ),
     501To unsubscribe from future emails click here:
     502####UNSUB_LINK####', 'wporg-forums' ),
    409503            $reply_author_name,
    410504            $reply_content,
    411505            $reply_url,
    412             // String may not have placeholders, ie. in the case of tags.
    413             sprintf( $this->labels['receipt'], $this->directory ? $this->directory->title() : '' )
     506            sprintf(
     507                $this->labels['receipt'],
     508                $this->get_current_term()->name
     509            )
    414510        );
    415511
     
    439535
    440536        return $title;
     537    }
     538
     539    /**
     540     * Personalize subscription emails by adding an unsubscription link.
     541     *
     542     * @param array $email_parts The email parts.
     543     * @return The email parts.
     544     */
     545    public function personalize_subscription_email( $email_parts ) {
     546        // get_tokenised_unsubscribe_url() will validate the user object.
     547        $user       = get_user_by( 'email', $email_parts['to'] );
     548        $unsub_link = $this->get_tokenised_unsubscribe_url( $user, $this->term );
     549
     550        if ( ! $unsub_link ) {
     551            return $email_parts;
     552        }
     553
     554        $email_parts['message'] = str_replace(
     555            '####UNSUB_LINK####',
     556            '<' . esc_url_raw( $unsub_link ) . '>',
     557            $email_parts['message']
     558        );
     559
     560        $email_parts['headers'][] = 'List-Unsubscribe: <' . esc_url_raw( $unsub_link ) . '>';
     561        $email_parts['headers'][] = 'List-Unsubscribe-Post: List-Unsubscribe=One-Click';
     562
     563        return $email_parts;
     564    }
     565
     566    /**
     567     * Get the WP_Term instance for the currently displayed item.
     568     */
     569    protected function get_current_term() {
     570        if ( $this->term ) {
     571            return $this->term;
     572        }
     573
     574        // The currently queried tag.
     575        if (
     576            bbp_is_topic_tag() &&
     577            ( $term = get_queried_object() ) &&
     578            ( $term instanceof \WP_Term )
     579        ) {
     580            return $term;
     581        }
     582
     583        // The current directory loaded.
     584        if (
     585            $this->directory &&
     586            bbp_is_single_view() &&
     587            $this->directory->compat() == bbp_get_view_id()
     588        ) {
     589            return $this->directory->term;
     590        }
     591
     592        return false;
    441593    }
    442594
     
    465617                echo '<p id="bbp-term-' . esc_attr( $this->taxonomy ) . '">' . "\n";
    466618                foreach ( $terms as $term ) {
    467                     echo '<a href="' . esc_url( get_term_link( $term->term_id ) ) . '">' . esc_html( $term->slug ) . '</a>';
     619                    echo '<a href="' . esc_url( get_term_link( $term->term_id ) ) . '">' . esc_html( $term->name ) . '</a>';
    468620                    if ( get_current_user_id() == $user_id ) {
    469621                        $url = self::get_subscription_url( $user_id, $term->term_id, $this->taxonomy );
     
    495647     */
    496648    public static function get_user_taxonomy_subscriptions( $user_id = 0, $taxonomy = 'topic-tag' ) {
    497         $retval = false;
    498 
    499649        if ( empty( $user_id ) || empty( $taxonomy ) ) {
    500650            return false;
    501651        }
    502652
    503         $terms = get_terms( array(
     653        $subscriptions = get_terms( array(
    504654            'taxonomy'   => $taxonomy,
    505655            'meta_key'   => self::META_KEY,
     
    507657        ) );
    508658
    509         if ( ! empty( $terms ) ) {
    510             $retval = $terms;
    511         }
    512         return apply_filters( 'wporg_bbp_get_user_taxonomy_subscriptions', $retval, $user_id, $taxonomy );
     659        // Default to false if empty.
     660        $subscriptions = $subscriptions ?: false;
     661
     662        return apply_filters( 'wporg_bbp_get_user_taxonomy_subscriptions', $subscriptions, $user_id, $taxonomy );
    513663    }
    514664
     
    529679            wp_cache_set( 'wporg_bbp_get_term_subscribers_' . $term_id, $subscribers, 'bbpress_users' );
    530680        }
     681
    531682        return apply_filters( 'wporg_bbp_get_term_subscribers', $subscribers, $term_id );
    532683    }
     
    619770
    620771        if ( self::is_user_subscribed_to_term( $user_id, $term_id ) ) {
    621             $query_args = array( 'action' => 'wporg_bbp_unsubscribe_term', 'term_id' => $term_id, 'taxonomy' => $taxonomy );
     772            $action = 'wporg_bbp_unsubscribe_term';
    622773        } else {
    623             $query_args = array( 'action' => 'wporg_bbp_subscribe_term', 'term_id' => $term_id, 'taxonomy' => $taxonomy );
     774            $action = 'wporg_bbp_subscribe_term';
    624775        }
    625776
     
    630781        }
    631782
    632         $url = esc_url( wp_nonce_url( add_query_arg( $query_args, $permalink ), 'toggle-term-subscription_' . $user_id . '_' . $term_id . '_' . $taxonomy ) );
    633         return $url;
    634     }
    635 
     783        $url = wp_nonce_url(
     784            add_query_arg( compact( 'action', 'term_id' ), $permalink ),
     785            'toggle-term-subscription_' . $user_id . '_' . $term_id . '_' . $taxonomy
     786        );
     787
     788        return esc_url( $url );
     789    }
     790
     791    /**
     792     * Generates a unsubscription token for a user.
     793     *
     794     * The token form is 'user_id|expiry|hash', and dependant upon the user email, password, and term.
     795     *
     796     * @param WP_Term $term   The user the token should be for.
     797     * @param WP_User $user   The user the token should be for.
     798     * @param int     $expiry The expiry of the token. Optional, only required for verifying tokens.
     799     * @return string|bool The hashed token, false on failure.
     800     */
     801    protected static function generate_unsubscribe_token( $term, $user, $expiry = 0 ) {
     802        if ( ! $term || ! $user ) {
     803            return false;
     804        }
     805        if ( ! $expiry ) {
     806            $expiry = time() + self::UNSUBSCRIBE_LIFETIME;
     807        }
     808
     809        $expiry    = intval( $expiry );
     810        $pass_frag = substr( $user->user_pass, 8, 4 ); // Password fragment used by cookie auth.
     811        $key       = wp_hash( $term->term_id . '|' . $term->taxonomy . '|' . $user->user_email . '|' . $pass_frag . '|' . $expiry, 'forum_subcriptions' );
     812        $hash      = hash_hmac( 'sha256',  $term->term_id . '|' . $term->taxonomy . '|' . $user->user_email . '|' . $expiry, $key );
     813
     814        return $user->ID . '|' . $expiry . '|' . $hash;
     815    }
     816
     817    /**
     818     * Validate if the current request has a valid tokenised unsubscription link.
     819     *
     820     * @return bool|int User ID on success, false on failure.
     821     */
     822    protected function has_valid_unsubscription_token() {
     823        if (
     824            ! isset( $_GET['token'] ) ||
     825            2 !== substr_count( $_GET['token'], '|' )
     826        ) {
     827            return false;
     828        }
     829
     830        $provided_token            = rtrim( $_GET['token'], '>' );
     831        list( $user_id, $expiry, ) = explode( '|', $provided_token );
     832        $term                      = $this->get_current_term();
     833        $user                      = get_user_by( 'id', intval( $user_id ) );
     834        $expected_token            = self::generate_unsubscribe_token( $term, $user, $expiry );
     835
     836        if (
     837            $expiry > time() &&
     838            hash_equals( $expected_token, $provided_token )
     839        ) {
     840            return $user->ID;
     841        }
     842
     843        return false;
     844    }
     845
     846    /**
     847     * Generate a tokenised unsubscription link for a given user & term.
     848     *
     849     * This link can be used without being logged in.
     850     *
     851     * @param \WP_User $user The user to generate the link for.
     852     * @param \WP_Term $term The term to generate the link for.
     853     *
     854     * @return bool|string The URL, or false upon failure.
     855     */
     856    public static function get_tokenised_unsubscribe_url( $user, $term ) {
     857        $token  = self::generate_unsubscribe_token( $term, $user );
     858        if ( ! $token ) {
     859            return false;
     860        }
     861
     862        return add_query_arg(
     863            array(
     864                'action'   => 'wporg_bbp_unsubscribe_term',
     865                'token'    => $token,
     866            ),
     867            // We don't include the term_id in the URL, and instead rely upon the term coming from the URL
     868            get_term_link( $term )
     869        );
     870    }
     871
     872    /**
     873     * Generate an unsubscription link for use in a Template.
     874     *
     875     * @param array $args
     876     * @return string
     877     */
    636878    public static function get_subscription_link( $args ) {
    637         if ( ! current_user_can( 'spectate' ) ) {
    638             return false;
    639         }
    640 
    641879        $r = bbp_parse_args( $args, array(
    642880            'user_id'     => get_current_user_id(),
     
    648886            'js_confirm'  => esc_html__( 'Are you sure you wish to subscribe by email to all future topics created in this tag?', 'wporg-forums' ),
    649887        ), 'get_term_subscription_link' );
    650         if ( empty( $r['user_id'] ) || empty( $r['term_id'] ) || empty( $r['taxonomy'] ) ) {
    651             return false;
    652         }
    653888
    654889        $user_id  = $r['user_id'];
     
    656891        $taxonomy = $r['taxonomy'];
    657892
    658         $url = self::get_subscription_url( $r['user_id'], $r['term_id'], $r['taxonomy'] );
    659         if ( self::is_user_subscribed_to_term( $r['user_id'], $r['term_id'] ) ) {
     893        if ( empty( $user_id ) || empty( $term_id ) || empty( $taxonomy ) ) {
     894            return false;
     895        }
     896
     897        if ( ! user_can( $user_id, 'spectate' ) ) {
     898            return false;
     899        }
     900
     901        $url = self::get_subscription_url( $user_id, $term_id, $taxonomy );
     902        if ( self::is_user_subscribed_to_term( $user_id, $term_id ) ) {
    660903            $text       = $r['unsubscribe'];
    661904            $js_confirm = '';
     
    673916        );
    674917    }
    675 
    676     public static function get_valid_actions() {
    677         return array(
    678             'wporg_bbp_subscribe_term',
    679             'wporg_bbp_unsubscribe_term',
    680         );
    681     }
    682918}
Note: See TracChangeset for help on using the changeset viewer.