Settings > Contact attributes, type Text) * and be added to the form, for example as a hidden field. */ const SOURCE_FIELD = 'PLUGIN'; const PRIVACY_URL = 'https://sanjayshankar.me/privacy/'; /** * Google Apps Script web app that writes deactivation feedback to the * author's Google Sheet, one tab per plugin (see bin/feedback-receiver.gs). * While empty, the deactivation survey is not shown. */ const FEEDBACK_URL = 'https://script.google.com/macros/s/AKfycbzzkhPcTof2yL8b4pSPnwmps3U7zJUvCRdzAj6ZErjpv_lwo1_cfRQsxM7mQQ6Igjikng/exec'; /** * Matches TOKEN in the receiver script. Filters stray requests; not a secret. */ const FEEDBACK_TOKEN = 'ssc-feedback-1'; /** * Days the card stays hidden after it is closed ("×" or "Not now"). */ const SNOOZE_DAYS = 30; /** * Folder of the copy that is running. * * @var string */ private static $dir = ''; /** * Whether the card was printed on this screen. * * @var bool */ private static $printed = false; /** * Hooks. * * @param string $dir Folder of this copy. */ public static function init( $dir ) { self::$dir = $dir; add_action( 'sanjayshankar_connect_card', array( __CLASS__, 'card' ) ); add_action( 'sanjayshankar_connect_box', array( __CLASS__, 'box' ) ); add_action( 'wp_ajax_sanjayshankar_connect', array( __CLASS__, 'ajax' ) ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'plugins_screen' ) ); } /** * Site-wide state. * * @return array */ private static function state() { $state = get_option( self::OPTION, array() ); return wp_parse_args( is_array( $state ) ? $state : array(), array( 'subscribed' => 0, ) ); } /** * Whether the card should show to the current user. * * @return bool */ public static function should_show() { if ( ! current_user_can( 'manage_options' ) ) { return false; } $state = self::state(); if ( $state['subscribed'] ) { return false; } $hidden_until = (int) get_user_meta( get_current_user_id(), self::USER_META, true ); /** * Filters whether the "Plugins by Sanjay Shankar" card shows. * * @param bool $show Show the card. */ return (bool) apply_filters( 'sanjayshankar_connect_show', $hidden_until < time() ); } /** * Names of the plugins by Sanjay Shankar active on this site. * * @return string[] */ private static function plugin_names() { return array_values( array_filter( wp_list_pluck( self::plugins(), 'name' ) ) ); } /** * Plugins by Sanjay Shankar active on this site. * * @return array Slug => { name, file, version }. */ private static function plugins() { /** * Plugins by Sanjay Shankar register themselves here: * slug => name, or slug => array( 'name' => …, 'file' => plugin_basename( __FILE__ ), 'version' => … ). * The file is needed for the deactivation survey. * * @param array $plugins Plugins. */ $out = array(); foreach ( (array) apply_filters( 'sanjayshankar_connect_plugins', array() ) as $slug => $plugin ) { $plugin = is_array( $plugin ) ? $plugin : array( 'name' => $plugin ); $out[ (string) $slug ] = array( 'name' => isset( $plugin['name'] ) ? (string) $plugin['name'] : (string) $slug, 'file' => isset( $plugin['file'] ) ? (string) $plugin['file'] : '', 'version' => isset( $plugin['version'] ) ? (string) $plugin['version'] : '', ); } return $out; } /** * Prints the dismissible card (plugin dashboards), once per screen. * * @param string $source Slug of the plugin whose screen this is. */ public static function card( $source = '' ) { if ( self::$printed || ! self::should_show() ) { return; } self::$printed = true; self::render( $source, 'card' ); } /** * Prints the permanent sign-up box (plugin settings screens), so admins who * closed the card can still subscribe. Always shown to administrators. * * @param string $source Slug of the plugin whose screen this is. */ public static function box( $source = '' ) { // On a screen that already shows the card (settings-only plugins), one form is enough. if ( ! self::$printed && current_user_can( 'manage_options' ) ) { self::render( $source, 'box' ); } } /** * Loads the card's stylesheet and script. */ private static function enqueue() { $main = self::$dir . '/connect.php'; wp_enqueue_style( 'sanjayshankar-connect', plugins_url( 'connect.css', $main ), array(), self::VERSION ); wp_enqueue_script( 'sanjayshankar-connect', plugins_url( 'connect.js', $main ), array(), self::VERSION, true ); wp_localize_script( 'sanjayshankar-connect', 'sanjayshankarConnect', array( 'ajax' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'sanjayshankar_connect' ), 'i18n' => array( 'sending' => __( 'Subscribing…', 'advanced-preloader' ), 'invalid' => __( 'Please enter a valid email address.', 'advanced-preloader' ), 'error' => __( 'Something went wrong. Please try again later.', 'advanced-preloader' ), ), ) ); } /** * The card or box markup. * * @param string $source Slug of the plugin whose screen this is. * @param string $mode 'card' (dismissible) or 'box' (permanent). */ private static function render( $source, $mode ) { static $count = 0; ++$count; $id = 'sanjayshankar-connect-' . $count; $card = 'card' === $mode; $state = self::state(); self::enqueue(); // A different pastel panel on each page load, from the sanjayshankar.me palette. $themes = array( 'green', 'blue', 'purple', 'yellow', 'orange' ); $theme = $themes[ wp_rand( 0, count( $themes ) - 1 ) ]; // Name the plugin whose screen this is; count the others. $all = self::plugins(); $current = isset( $all[ $source ] ) ? $all[ $source ]['name'] : ''; $others = array_values( array_diff( self::plugin_names(), array( $current ) ) ); if ( '' === $current ) { $current = array_shift( $others ); } if ( ! $current ) { $names = ''; } elseif ( ! $others ) { $names = $current; } elseif ( 1 === count( $others ) ) { /* translators: 1: this plugin's name, 2: another plugin's name */ $names = sprintf( __( '%1$s and %2$s', 'advanced-preloader' ), $current, $others[0] ); } else { /* translators: 1: this plugin's name, 2: number of the author's other plugins */ $names = sprintf( _n( '%1$s and %2$d of my other plugins', '%1$s and %2$d of my other plugins', count( $others ), 'advanced-preloader' ), $current, count( $others ) ); } // Pre-filled from the admin's own profile so they don't retype it. Nothing is // sent until they click Subscribe, and they can change both fields first. $user = wp_get_current_user(); $first = $user->first_name ? $user->first_name : ''; $email = is_email( $user->user_email ) ? $user->user_email : (string) get_option( 'admin_email' ); ?>

' . esc_html__( 'Privacy policy', 'advanced-preloader' ) . '' ); ?>

__( 'You are not allowed to do this.', 'advanced-preloader' ) ), 403 ); } $task = isset( $_POST['task'] ) ? sanitize_key( wp_unslash( $_POST['task'] ) ) : ''; if ( 'snooze' === $task ) { update_user_meta( get_current_user_id(), self::USER_META, time() + self::SNOOZE_DAYS * DAY_IN_SECONDS ); wp_send_json_success(); } if ( 'feedback' === $task ) { self::feedback(); } if ( 'subscribe' !== $task ) { wp_send_json_error( array( 'message' => __( 'Unknown request.', 'advanced-preloader' ) ), 400 ); } $email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : ''; $name = isset( $_POST['first_name'] ) ? mb_substr( sanitize_text_field( wp_unslash( $_POST['first_name'] ) ), 0, 60 ) : ''; $slug = isset( $_POST['source'] ) ? sanitize_key( wp_unslash( $_POST['source'] ) ) : ''; $all = self::plugins(); // The plugin's display name when it registered one, so Brevo shows "Shopnook" rather than "shopnook". $from = isset( $all[ $slug ] ) ? $all[ $slug ]['name'] : $slug; if ( ! is_email( $email ) ) { wp_send_json_error( array( 'message' => __( 'Please enter a valid email address.', 'advanced-preloader' ) ), 400 ); } $response = wp_remote_post( add_query_arg( 'isAjax', '1', self::FORM_URL ), array( 'timeout' => 15, 'body' => array( 'EMAIL' => $email, 'FIRSTNAME' => $name, self::SOURCE_FIELD => $from, 'email_address_check' => '', 'locale' => 'en', 'html_type' => 'simple', ), ) ); $body = is_wp_error( $response ) ? null : json_decode( wp_remote_retrieve_body( $response ), true ); if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) || empty( $body['success'] ) ) { wp_send_json_error( array( 'message' => __( 'Could not reach the newsletter service. Please try again later.', 'advanced-preloader' ) ), 502 ); } $state = self::state(); $state['subscribed'] = time(); update_option( self::OPTION, $state, false ); wp_send_json_success( array( 'message' => __( 'Thanks! Check your inbox to confirm your subscription.', 'advanced-preloader' ) ) ); } /** * Deactivation reasons. Keys are sent; the English text goes to the sheet. * * @return array Key => [ label, prompt for the details box ]. */ private static function reasons() { return array( 'not_working' => array( __( 'I couldn’t get it to work', 'advanced-preloader' ), __( 'What went wrong?', 'advanced-preloader' ) ), 'better_plugin' => array( __( 'I found a better plugin', 'advanced-preloader' ), __( 'Which plugin? What does it do better?', 'advanced-preloader' ) ), 'missing_feature' => array( __( 'It’s missing a feature I need', 'advanced-preloader' ), __( 'Which feature would you like?', 'advanced-preloader' ) ), 'broke_site' => array( __( 'It broke my site or conflicts with something', 'advanced-preloader' ), __( 'What happened, and with which theme or plugin?', 'advanced-preloader' ) ), 'temporary' => array( __( 'I only needed it for a short time', 'advanced-preloader' ), __( 'Anything you’d like to add? (optional)', 'advanced-preloader' ) ), 'other' => array( __( 'Other', 'advanced-preloader' ), __( 'Please tell me more', 'advanced-preloader' ) ), ); } /** * English reason text for the sheet, whatever the admin's language. * * @param string $key Reason key. * @return string */ private static function reason_text( $key ) { $english = array( 'not_working' => 'I couldn’t get it to work', 'better_plugin' => 'I found a better plugin', 'missing_feature' => 'It’s missing a feature I need', 'broke_site' => 'It broke my site or conflicts with something', 'temporary' => 'I only needed it for a short time', 'other' => 'Other', ); return isset( $english[ $key ] ) ? $english[ $key ] : ''; } /** * Where deactivation feedback is sent. * * @return string */ private static function feedback_url() { /** * Filters the deactivation feedback endpoint. Empty turns the survey off. * * @param string $url Endpoint. */ return (string) apply_filters( 'sanjayshankar_connect_feedback_url', self::FEEDBACK_URL ); } /** * Loads the deactivation survey on the Plugins screen. * * @param string $hook Screen hook. */ public static function plugins_screen( $hook ) { if ( 'plugins.php' !== $hook || '' === self::feedback_url() || is_network_admin() || ! current_user_can( 'activate_plugins' ) ) { return; } $plugins = array(); foreach ( self::plugins() as $slug => $plugin ) { if ( $plugin['file'] ) { $plugins[ $slug ] = array( 'name' => $plugin['name'], 'file' => $plugin['file'], ); } } if ( ! $plugins ) { return; } $main = self::$dir . '/connect.php'; wp_enqueue_style( 'sanjayshankar-connect', plugins_url( 'connect.css', $main ), array(), self::VERSION ); wp_enqueue_script( 'sanjayshankar-feedback', plugins_url( 'feedback.js', $main ), array(), self::VERSION, true ); $prompts = array(); foreach ( self::reasons() as $key => $reason ) { $prompts[ $key ] = $reason[1]; } wp_localize_script( 'sanjayshankar-feedback', 'sanjayshankarFeedback', array( 'ajax' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'sanjayshankar_connect' ), 'plugins' => $plugins, 'prompts' => $prompts, 'i18n' => array( /* translators: %s: plugin name */ 'title' => __( 'Quick question before you deactivate %s', 'advanced-preloader' ), 'sending' => __( 'Sending…', 'advanced-preloader' ), ), ) ); add_action( 'admin_footer', array( __CLASS__, 'survey' ) ); } /** * The deactivation survey dialog, shared by all registered plugins. */ public static function survey() { $user = wp_get_current_user(); $name = trim( $user->first_name . ' ' . $user->last_name ); $name = '' !== $name ? $name : $user->display_name; $email = is_email( $user->user_email ) ? $user->user_email : (string) get_option( 'admin_email' ); ?>

$reason ) : ?>

' . esc_html__( 'Privacy policy', 'advanced-preloader' ) . '' ); ?>

8, 'redirection' => 0, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'token' => self::FEEDBACK_TOKEN, 'slug' => $slug, 'plugin' => $plugins[ $slug ]['name'], 'version' => $plugins[ $slug ]['version'], 'reason' => self::reason_text( $reason ), 'details' => $details, 'wp' => get_bloginfo( 'version' ), 'wc' => defined( 'WC_VERSION' ) ? WC_VERSION : '', 'php' => PHP_VERSION, 'locale' => get_user_locale(), 'name' => $name, 'email' => is_email( $email ) ? $email : '', ) ), ) ); $location = is_wp_error( $response ) ? '' : (string) wp_remote_retrieve_header( $response, 'location' ); if ( $location && 0 === strpos( $location, 'https://' ) ) { $response = wp_remote_get( $location, array( 'timeout' => 8 ) ); } $result = is_wp_error( $response ) ? null : json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $result['ok'] ) ) { wp_send_json_error( null, 502 ); } wp_send_json_success(); } }