[ link action, ok ] it stands for. */ const NOTICE_STATES = array( 'confirmed' => array( 'confirm', true ), 'unsubscribed' => array( 'unsubscribe', true ), 'invalid' => array( '', false ), ); /** * Hook everything up. Nothing is hooked while the feature is unusable * (not Standalone mode, or switched off), except the emailed links: a * Recipient must always be able to unsubscribe. * * @return void */ public static function register(): void { add_action( 'template_redirect', array( __CLASS__, 'handle_link' ) ); add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_notice' ), 22 ); add_action( 'wp_footer', array( __CLASS__, 'print_notice' ) ); if ( ! Mlsimport_Saved_Search::enabled() ) { return; } add_action( 'wp_ajax_' . self::ACTION, array( __CLASS__, 'handle' ) ); add_action( 'wp_ajax_nopriv_' . self::ACTION, array( __CLASS__, 'handle' ) ); add_action( 'mlsimport_results_toolbar', array( __CLASS__, 'toolbar_button' ), 10, 1 ); add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue' ), 21 ); } /** * AJAX entry: verify the nonce, run create(), answer JSON. * * @return void */ public static function handle(): void { check_ajax_referer( self::ACTION, 'nonce' ); $result = Mlsimport_Saved_Search::create( wp_unslash( $_POST ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above. if ( $result['ok'] ) { wp_send_json_success( array( 'message' => $result['message'] ) ); } wp_send_json_error( array( 'message' => $result['message'] ), 400 ); } /** * Act on an emailed link. Pure of the HTTP layer: takes the query args, * returns what to tell the visitor — or null when the request is not ours. * * @param array $query Request query args (unslashed). * @return array{ok:bool,action:string,message:string}|null */ public static function link_result( array $query ) { $action = isset( $query['mlsimport_ss'] ) ? (string) $query['mlsimport_ss'] : ''; if ( ! in_array( $action, array( 'confirm', 'unsubscribe' ), true ) ) { return null; } $token = isset( $query['token'] ) ? (string) $query['token'] : ''; // The link does exactly one thing: confirm, or unsubscribe. Nothing else. $ok = 'confirm' === $action ? Mlsimport_Saved_Search::confirm( $token ) : Mlsimport_Saved_Search::unsubscribe( $token ); return array( 'ok' => $ok, 'action' => $action, 'message' => self::link_message( $action, $ok ), ); } /** * The words shown after an emailed link is used. * * @param string $action 'confirm' or 'unsubscribe'. * @param bool $ok Whether the link worked. * @return string */ public static function link_message( string $action, bool $ok ): string { if ( ! $ok ) { $message = __( 'This link is not valid any more.', 'mlsimport' ); } elseif ( 'confirm' === $action ) { $message = __( 'Your saved search is confirmed. You will get one email a day when new or updated listings match it.', 'mlsimport' ); } else { $message = __( 'You are unsubscribed. You will not receive any more emails for this saved search.', 'mlsimport' ); } /** Filter the message shown after an emailed link is used. @since 7.3 */ return (string) apply_filters( 'mlsimport_saved_search_link_message', $message, $action, $ok ); } /** * template_redirect shell for the emailed links: act on the token, then send * the visitor on, where print_notice() shows the answer inside the site's own * layout. A confirmed search lands on the results page it was saved from, with * its criteria applied; an unsubscribe or a dead link lands on the home page. * The token never reaches the landing URL. * * @return void */ public static function handle_link(): void { // Step 1 — act on the token; null means the request is not one of our links. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- the emailed token is the credential. $query = wp_unslash( $_GET ); $result = self::link_result( $query ); if ( null === $result ) { return; } // Step 2 — the notice state, and where it is shown. Only a successful confirm // goes back to the saved results page (stored at save time, same-host checked // there); the home page is the fallback should that URL be missing. $target = home_url( '/' ); if ( ! $result['ok'] ) { $state = 'invalid'; } elseif ( 'confirm' === $result['action'] ) { $state = 'confirmed'; $saved = Mlsimport_Saved_Search::results_url_for_token( (string) $query['token'] ); $target = '' !== $saved ? $saved : $target; } else { $state = 'unsubscribed'; } // Step 3 — redirect with the state; print_notice() strips it from the address bar. wp_safe_redirect( add_query_arg( self::NOTICE_ARG, $state, $target ) ); exit; } /** * The notice state in the current request, or '' when there is none. * * @return string */ private static function notice_state(): string { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only, whitelisted below. $state = isset( $_GET[ self::NOTICE_ARG ] ) ? sanitize_key( wp_unslash( $_GET[ self::NOTICE_ARG ] ) ) : ''; return isset( self::NOTICE_STATES[ $state ] ) ? $state : ''; } /** * Load the notice style on the landing page. Hooked apart from enqueue() so it * also works when the feature is switched off (unsubscribe must always work). * * @return void */ public static function enqueue_notice(): void { if ( '' === self::notice_state() || ! apply_filters( 'mlsimport_standalone_styles', true ) ) { return; } wp_enqueue_style( 'mlsimport-saved-search', MLSIMPORT_PLUGIN_URL . 'public/css/mlsimport-saved-search.css', array(), MLSIMPORT_VERSION ); } /** * Print the link's answer as a banner over the landing page. Fixed-position * from wp_footer, so it shows on any theme, classic or block. * * @return void */ public static function print_notice(): void { $state = self::notice_state(); if ( '' === $state ) { return; } list( $action, $ok ) = self::NOTICE_STATES[ $state ]; $message = self::link_message( $action, $ok ); printf( '
%2$s