plugin_file = $plugin_file; $this->plugin_name = basename( $this->plugin_file, '.php' ); $this->disabled_wp_cron = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON == true; $this->enable_self_cron = true == $this->disabled_wp_cron ? true : false; $this->event_hook = 'put_do_weekly_action'; $this->require_optin = isset( $args[ 'opt_in' ] ) ? $args[ 'opt_in' ] : true; $this->include_goodbye_form = isset( $args[ 'goodbye_form' ] ) ? $args[ 'goodbye_form' ] : true; $this->marketing = isset( $args[ 'email_marketing' ] ) ? $args[ 'email_marketing' ] : true; $this->options = isset( $args[ 'options' ] ) ? $args[ 'options' ] : array(); $this->item_id = isset( $args[ 'item_id' ] ) ? $args[ 'item_id' ] : false; /** * Activation Hook */ register_activation_hook( $this->plugin_file, array( $this, 'activate_this_plugin' ) ); /** * Deactivation Hook */ register_deactivation_hook( $this->plugin_file, array( $this, 'deactivate_this_plugin' ) ); } /** * When user agreed to opt-in tracking schedule is enabled. * * @since 2.5.0 */ public function schedule_tracking() { if ( $this->disabled_wp_cron ) { return; } if ( ! wp_next_scheduled( $this->event_hook ) ) { wp_schedule_event( time(), $this->recurrence, $this->event_hook ); } } /** * Add the schedule event if the plugin is tracked. * * @return void */ public function activate_this_plugin() { $allow_tracking = $this->is_tracking_allowed(); if ( ! $allow_tracking ) { return; } $this->schedule_tracking(); } /** * Remove the schedule event when plugin is deactivated and send the deactivated reason to inishghts if user submitted. * * @since 2.5.0 */ public function deactivate_this_plugin() { /** * Check tracking is allowed or not. */ $allow_tracking = $this->is_tracking_allowed(); if ( ! $allow_tracking ) { return; } $body = $this->get_data(); $body[ 'status' ] = 'Deactivated'; $body[ 'deactivated_date' ] = time(); // Check deactivation reason and add for insights data. if ( false !== get_option( 'wpins_deactivation_reason_' . $this->plugin_name ) ) { $body[ 'deactivation_reason' ] = get_option( 'wpins_deactivation_reason_' . $this->plugin_name ); } if ( false !== get_option( 'wpins_deactivation_details_' . $this->plugin_name ) ) { $body[ 'deactivation_details' ] = get_option( 'wpins_deactivation_details_' . $this->plugin_name ); } $this->send_data( $body ); delete_option( 'wpins_deactivation_reason_' . $this->plugin_name ); delete_option( 'wpins_deactivation_details_' . $this->plugin_name ); /** * Clear the event schedule. */ if ( ! $this->disabled_wp_cron ) { wp_clear_scheduled_hook( $this->event_hook ); } } /** * Initial Method to Hook Everything. * * @return void */ public function init() { add_action( 'wpdeveloper_notice_clicked_for_' . $this->plugin_name, array( $this, 'clicked' ) ); add_action( $this->event_hook, array( $this, 'do_tracking' ) ); /** * Self-heal the schedule. A file-only plugin update does NOT fire * register_activation_hook(), so already-opted-in sites whose cron was * never scheduled (or was lost) would otherwise never report again. Since * init() runs on every request, re-ensure the event whenever tracking is * allowed. schedule_tracking() guards with wp_next_scheduled(), so this is * idempotent and respects DISABLE_WP_CRON. */ if ( $this->is_tracking_allowed() ) { $this->schedule_tracking(); } // For Test add_action( 'wpdeveloper_optin_notice_for_' . $this->plugin_name, array( $this, 'notice' ) ); /** * Deactivation Reason Form and Submit Data to Insights. */ if ( ! is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' ) ) { add_filter( 'plugin_action_links_' . plugin_basename( $this->plugin_file ), array( $this, 'deactivate_action_links' ) ); add_action( 'admin_print_footer_scripts', array( $this, 'notice_script' ) ); add_action( 'admin_footer-plugins.php', array( $this, 'deactivate_reasons_form' ) ); add_action( 'wp_ajax_deactivation_form_' . esc_attr( $this->plugin_name ), array( $this, 'deactivate_reasons_form_submit' ) ); } } /** * For Redirecting Current Page without Arguments! * * @return string */ private function redirect_to() { $request_payload = isset( $_SERVER[ 'REQUEST_URI' ] ) ? esc_url_raw( wp_unslash( $_SERVER[ 'REQUEST_URI' ] ) ) : ''; $request_uri = wp_parse_url( $request_payload, PHP_URL_PATH ); $query_string = wp_parse_url( $request_payload, PHP_URL_QUERY ); parse_str( $query_string, $current_url ); $unset_array = array( 'dismiss', 'plugin', '_wpnonce', 'later', 'plugin_action', 'marketing_optin' ); foreach ( $unset_array as $value ) { if ( isset( $current_url[ $value ] ) ) { unset( $current_url[ $value ] ); } } $current_url = http_build_query( $current_url ); $redirect_url = $request_uri . '?' . $current_url; return $redirect_url; } /** * This method forcing the do_tracking method to execute instant. * * @return void */ public function force_tracking() { $this->do_tracking( true ); } /** * This method is responsible for all the magic from the front of the plugin. * * @since 2.5.0 * @param $force Force tracking if it's not the correct time to track/ */ public function do_tracking( $force = false ) { /** * Check URL is set or not. */ if ( empty( self::API_URL ) ) { return; } /** * Check is tracking allowed or not. */ if ( ! $this->is_tracking_allowed() ) { return; } /** * Check is this the correct time to track or not. * or Force to track. */ if ( ! $this->is_time_to_track() && ! $force ) { return; } /** * Get All Data. */ $body = $this->get_data(); /** * Send all data. */ return $this->send_data( $body ); } /** * Is tracking allowed? * * @since 1.0.0 */ private function is_tracking_allowed() { // First, check if the user has changed their mind and opted out of tracking if ( $this->has_user_opted_out() ) { $this->set_is_tracking_allowed( false, $this->plugin_name ); return false; } // The wpins_allow_tracking option is an array of plugins that are being tracked $allow_tracking = get_option( 'wpins_allow_tracking' ); // If this plugin is in the array, then tracking is allowed if ( isset( $allow_tracking[ $this->plugin_name ] ) ) { return true; } return false; } /** * Set a flag in DB If tracking is allowed. * * @since 2.5.0 * @param $is_allowed Boolean true if is allowed. */ public function set_is_tracking_allowed( $is_allowed, $plugin = null ) { if ( empty( $plugin ) ) { $plugin = $this->plugin_name; } /** * Get All Tracked Plugin List using this Tracker. */ $allow_tracking = get_option( 'wpins_allow_tracking' ); /** * Check user is opted out for tracking or not. */ if ( $this->has_user_opted_out() ) { if ( isset( $allow_tracking[ $plugin ] ) ) { unset( $allow_tracking[ $plugin ] ); } } elseif ( $is_allowed || ! $this->require_optin ) { /** * If user has agreed to allow tracking */ if ( empty( $allow_tracking ) || ! is_array( $allow_tracking ) ) { $allow_tracking = array( $plugin => $plugin ); } else { $allow_tracking[ $plugin ] = $plugin; } } elseif ( isset( $allow_tracking[ $plugin ] ) ) { unset( $allow_tracking[ $plugin ] ); } update_option( 'wpins_allow_tracking', $allow_tracking, 'no' ); } /** * Check the user has opted out or not. * * @since 2.5.0 * @return boolean */ protected function has_user_opted_out() { if ( ! empty( $this->options ) ) { foreach ( $this->options as $option_name ) { $options = get_option( $option_name ); if ( ! empty( $options[ 'wpins_opt_out' ] ) ) { return true; } } } return false; } /** * Check if it's time to track * * @since 2.5.0 */ public function is_time_to_track() { $track_times = get_option( 'wpins_last_track_time', array() ); return ! isset( $track_times[ $this->plugin_name ] ) ? true : ( ( isset( $track_times[ $this->plugin_name ] ) && $track_times[ $this->plugin_name ] ) < strtotime( '-1 day' ) ? true : false ); } /** * Set tracking time. * * @since 2.5.0 */ public function set_track_time() { $track_times = get_option( 'wpins_last_track_time', array() ); $track_times[ $this->plugin_name ] = time(); update_option( 'wpins_last_track_time', $track_times, 'no' ); } /** * This method is responsible for collecting all data. * * @since 2.5.0 */ public function get_data() { $body = array( 'plugin_slug' => sanitize_text_field( $this->plugin_name ), 'url' => get_bloginfo( 'url' ), 'site_name' => get_bloginfo( 'name' ), 'site_version' => get_bloginfo( 'version' ), 'site_language' => get_bloginfo( 'language' ), 'charset' => get_bloginfo( 'charset' ), 'wpins_version' => self::WPINS_VERSION, 'php_version' => phpversion(), 'multisite' => is_multisite(), 'file_location' => __FILE__ ); // Collect the email if the correct option has been set if ( $this->marketing ) { if ( ! function_exists( 'wp_get_current_user' ) ) { include ABSPATH . 'wp-includes/pluggable.php'; } $current_user = wp_get_current_user(); $email = $current_user->user_email; if ( is_email( $email ) ) { $body[ 'email' ] = $email; } } $body[ 'marketing_method' ] = $this->marketing; $body[ 'server' ] = isset( $_SERVER[ 'SERVER_SOFTWARE' ] ) ? $_SERVER[ 'SERVER_SOFTWARE' ] : ''; //phpcs:ignore /** * Collect all active and inactive plugins */ if ( ! function_exists( 'get_plugins' ) ) { include ABSPATH . '/wp-admin/includes/plugin.php'; } $plugins = array_keys( get_plugins() ); $active_plugins = is_network_admin() ? array_keys( get_site_option( 'active_sitewide_plugins', array() ) ) : get_option( 'active_plugins', array() ); foreach ( $plugins as $key => $plugin ) { if ( in_array( $plugin, $active_plugins ) ) { unset( $plugins[ $key ] ); } } $body[ 'active_plugins' ] = $active_plugins; $body[ 'inactive_plugins' ] = $plugins; /** * Text Direction. */ $body[ 'text_direction' ] = ( function_exists( 'is_rtl' ) ? ( is_rtl() ? 'RTL' : 'LTR' ) : 'NOT SET' ); /** * Get Our Plugin Data. * * @since 2.5.0 */ $plugin = $this->plugin_data(); if ( empty( $plugin ) ) { $body[ 'message' ] .= __( 'We can\'t detect any plugin information. This is most probably because you have not included the code in the plugin main file.', 'betterdocs' ); $body[ 'status' ] = 'NOT FOUND'; } else { if ( isset( $plugin[ 'Name' ] ) ) { $body[ 'plugin' ] = sanitize_text_field( $plugin[ 'Name' ] ); } if ( isset( $plugin[ 'Version' ] ) ) { $body[ 'version' ] = sanitize_text_field( $plugin[ 'Version' ] ); } $body[ 'status' ] = 'Active'; } /** * Get active theme name and version * * @since 2.5.0 */ $theme = wp_get_theme(); if ( $theme->Name ) { $body[ 'theme' ] = sanitize_text_field( $theme->Name ); } if ( $theme->Version ) { $body[ 'theme_version' ] = sanitize_text_field( $theme->Version ); } /** * Extension point for product-usage analytics. * * Collectors (Free / Pro / Chatbot) hook this to inject their `bd_*` * metric groups into `$body['optional_data']` (a nested array). See * `WPDeveloper\BetterDocs\Insights\Collector` and `docs/insights-tracking.md`. * * @param array $body The tracking payload. * @param Insights $this The Insights instance. */ $body = apply_filters( 'betterdocs_insights_data', $body, $this ); /** * wpinsight persists only the fixed base fields plus a single * `optional_data` field, which it expects as a JSON-encoded string. * Collectors assemble it as a nested array; here we JSON-encode each * first-layer group (bd_features, bd_counts, …) into its own string * value, then JSON-encode the whole `optional_data` object. */ if ( ! empty( $body[ 'optional_data' ] ) && is_array( $body[ 'optional_data' ] ) ) { foreach ( $body[ 'optional_data' ] as $group => $value ) { if ( is_array( $value ) ) { $body[ 'optional_data' ][ $group ] = wp_json_encode( $value ); } } $body[ 'optional_data' ] = wp_json_encode( $body[ 'optional_data' ] ); } return $body; } /** * Collect plugin data, * Retrieve current plugin information * * @since 2.5.0 */ public function plugin_data() { if ( ! function_exists( 'get_plugin_data' ) ) { include ABSPATH . '/wp-admin/includes/plugin.php'; } $plugin = get_plugin_data( $this->plugin_file ); return $plugin; } /** * Send the data to insights. * * @since 2.5.0 */ public function send_data( $body ) { /** * Get SITE ID */ $site_id_key = "wpins_{$this->plugin_name}_site_id"; $site_id = get_option( $site_id_key, false ); $failed_data = array(); $site_url = get_bloginfo( 'url' ); $original_site_url = get_option( "wpins_{$this->plugin_name}_original_url", false ); // Re-register when the site has no recorded origin URL yet, or when the // current URL no longer matches it (domain migration). Without the URL-change // arm (added in tracker 3.0.3), a moved site keeps reporting under its old // site ID with stale identity data. Mirrors EA Lite's 3.0.3 send_data(). if ( ( false === $original_site_url || $original_site_url != $site_url ) && version_compare( $body[ 'wpins_version' ], '3.0.1', '>=' ) ) { $site_id = false; } /** * Send Initial Data to API */ if ( false == $site_id && false !== $this->item_id && ( false === $original_site_url || $original_site_url != $site_url ) ) { // Validate before interpolating: REMOTE_ADDR went into the URL raw, so // anything the host put there (some proxy setups write a comma-joined // list, or a value taken from a client header) became URL path/query. // FILTER_VALIDATE_IP also drops private, loopback and reserved // addresses — a LAN address tells the geolocator nothing and sending // it is a needless disclosure. $remote_addr = isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? wp_unslash( $_SERVER[ 'REMOTE_ADDR' ] ) : ''; //phpcs:ignore $public_ip = filter_var( $remote_addr, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ); if ( false !== $public_ip ) { // NOTE: http, not https, is deliberate — ip-api.com serves TLS only // on its paid plans and answers 403 over https on the free tier, so // switching the scheme would silently disable country detection // rather than secure it. The response is therefore untrusted input: // it is read for a single string field, length-bounded, and // sanitised below. The residual exposure is that the site's own // outbound IP travels in cleartext to a third party. $country_request = wp_remote_get( 'http://ip-api.com/json/' . rawurlencode( $public_ip ) . '?fields=country', [ 'timeout' => 5 ] ); //phpcs:ignore if ( ! is_wp_error( $country_request ) && 200 == wp_remote_retrieve_response_code( $country_request ) ) { $ip_data = json_decode( wp_remote_retrieve_body( $country_request ) ); $country = ( is_object( $ip_data ) && isset( $ip_data->country ) && is_string( $ip_data->country ) ) ? sanitize_text_field( $ip_data->country ) : ''; $body[ 'country' ] = ( '' !== $country ) ? substr( $country, 0, 64 ) : 'NOT SET'; } } $body[ 'plugin_slug' ] = $this->plugin_name; $body[ 'url' ] = $site_url; $body[ 'item_id' ] = $this->item_id; $request = $this->remote_post( $body ); if ( ! is_wp_error( $request ) && 200 == $request[ 'response' ][ 'code' ] ) { $retrieved_body = json_decode( wp_remote_retrieve_body( $request ), true ); if ( is_array( $retrieved_body ) && isset( $retrieved_body[ 'siteId' ] ) ) { update_option( $site_id_key, $retrieved_body[ 'siteId' ], 'no' ); update_option( "wpins_{$this->plugin_name}_original_url", $site_url, 'no' ); update_option( "wpins_{$this->plugin_name}_{$retrieved_body[ 'siteId' ]}", $body, 'no' ); } } else { $failed_data = $body; } } $site_id_data_key = "wpins_{$this->plugin_name}_{$site_id}"; $site_id_data_failed_key = "wpins_{$this->plugin_name}_{$site_id}_send_failed"; if ( false != $site_id ) { $old_sent_data = get_option( $site_id_data_key, array() ); $diff_data = $this->diff( $body, $old_sent_data ); $failed_data = get_option( $site_id_data_failed_key, array() ); if ( ! empty( $failed_data ) && $diff_data != $failed_data ) { $failed_data = array_merge( $failed_data, $diff_data ); } } if ( ! empty( $failed_data ) && false != $site_id ) { $failed_data[ 'plugin_slug' ] = $this->plugin_name; $failed_data[ 'url' ] = $site_url; $failed_data[ 'site_id' ] = $site_id; if ( false != $original_site_url ) { $failed_data[ 'original_url' ] = $original_site_url; } $request = $this->remote_post( $failed_data ); if ( ! is_wp_error( $request ) ) { delete_option( $site_id_data_failed_key ); $replaced_data = array_merge( $old_sent_data, $failed_data ); update_option( $site_id_data_key, $replaced_data, 'no' ); } } if ( ! empty( $diff_data ) && false != $site_id && empty( $failed_data ) ) { $diff_data[ 'plugin_slug' ] = $this->plugin_name; $diff_data[ 'url' ] = $site_url; $diff_data[ 'site_id' ] = $site_id; if ( false != $original_site_url ) { $diff_data[ 'original_url' ] = $original_site_url; } $request = $this->remote_post( $diff_data ); if ( is_wp_error( $request ) ) { update_option( $site_id_data_failed_key, $diff_data, 'no' ); } else { $replaced_data = array_merge( $old_sent_data, $diff_data ); update_option( $site_id_data_key, $replaced_data, 'no' ); } } $this->set_track_time(); if ( isset( $request ) && is_wp_error( $request ) ) { return $request; } if ( isset( $request ) ) { return true; } return false; } /** * WP_REMOTE_POST method responsible for send data to the API_URL * * @param array $data * @param array $args * @return \WP_Error|array|null */ protected function remote_post( $data = array(), $args = array() ) { if ( empty( $data ) ) { return; } $args = wp_parse_args( $args, array( 'method' => 'POST', 'timeout' => 30, 'redirection' => 5, 'httpversion' => '1.1', 'blocking' => true, 'body' => $data, 'user-agent' => 'PUT/1.0.0; ' . get_bloginfo( 'url' ) ) ); $request = wp_remote_post( esc_url( self::API_URL ), $args ); if ( is_wp_error( $request ) || ( isset( $request[ 'response' ], $request[ 'response' ][ 'code' ] ) && 200 != $request[ 'response' ][ 'code' ] ) ) { return new \WP_Error( 500, 'Something went wrong.' ); } return $request; } /** * Difference between old and new data * * @param array $new_data * @param array $old_data * @return array */ protected function diff( $new_data, $old_data ) { $data = array(); if ( ! empty( $new_data ) ) { foreach ( $new_data as $key => $value ) { if ( isset( $old_data[ $key ] ) ) { if ( $old_data[ $key ] == $value ) { continue; } } $data[ $key ] = $value; } } return $data; } /** * Display the admin notice to users to allow them to opt in * * @since 2.5.0 */ public function notice() { /** * Return if notice is not set. */ if ( ! isset( $this->notice_options[ 'notice' ] ) ) { return; } /** * Check is allowed or blocked for notice. */ $block_notice = get_option( 'wpins_block_notice' ); if ( isset( $block_notice[ $this->plugin_name ] ) ) { return; } if ( ! current_user_can( 'manage_options' ) ) { return; } $this->has_notice = true; $url_yes = add_query_arg( array( 'plugin' => $this->plugin_name, 'plugin_action' => 'yes' ) ); $url_no = add_query_arg( array( 'plugin' => $this->plugin_name, 'plugin_action' => 'no' ) ); $url_yes = wp_nonce_url( $url_yes, '_wpnonce_optin_' . $this->plugin_name ); $url_no = wp_nonce_url( $url_no, '_wpnonce_optin_' . $this->plugin_name ); // Decide on notice text $notice_text = $this->notice_options[ 'notice' ] . ' ' . $this->notice_options[ 'consent_button_text' ] . ''; $extra_notice_text = $this->notice_options[ 'extra_notice' ]; ?>

notice_options[ 'yes' ] ); ?>   notice_options[ 'no' ] ); ?>

has_notice ) { echo ""; } } /** * Set all notice options to customized notice. * * @since 2.5.0 * @param array $options * @return void */ public function set_notice_options( $options = array() ) { $default_options = array( 'consent_button_text' => __( 'What we collect.', 'betterdocs' ), 'yes' => __( 'Sure, I\'d like to help', 'betterdocs' ), 'no' => __( 'No Thanks.', 'betterdocs' ) ); $options = wp_parse_args( $options, $default_options ); $this->notice_options = $options; } /** * Responsible for track the click from Notice. * * @return void */ public function clicked( $notice = null ) { if ( isset( $_GET[ '_wpnonce' ] ) && isset( $_GET[ 'plugin' ] ) && isset( $_GET[ 'plugin_action' ] ) ) { $tab = isset( $_GET[ 'tab' ] ) ? sanitize_text_field( wp_unslash( $_GET[ 'tab' ] ) ) : ''; if ( 'plugin-information' === $tab ) { return; } $nonce = sanitize_text_field( wp_unslash( $_GET[ '_wpnonce' ] ) ); if ( ! wp_verify_nonce( $nonce, '_wpnonce_optin_' . $this->plugin_name ) ) { return; } $plugin = sanitize_text_field( wp_unslash( $_GET[ 'plugin' ] ) ); $action = sanitize_text_field( wp_unslash( $_GET[ 'plugin_action' ] ) ); if ( 'yes' == $action ) { $this->schedule_tracking(); $this->set_is_tracking_allowed( true, $plugin ); if ( $this->do_tracking( true ) ) { $this->update_block_notice( $plugin ); } } else { $this->set_is_tracking_allowed( false, $plugin ); $this->update_block_notice( $plugin ); } if ( ! is_null( $notice ) ) { $notice->dismiss->dismiss_notice(); } /** * Redirect User To the Current URL, but without set query arguments. */ wp_safe_redirect( $this->redirect_to() ); } } /** * Set if we should block the opt-in notice for this plugin * * @since 2.5.0 */ public function update_block_notice( $plugin = null ) { if ( empty( $plugin ) ) { $plugin = $this->plugin_name; } $block_notice = get_option( 'wpins_block_notice' ); if ( empty( $block_notice ) || ! is_array( $block_notice ) ) { $block_notice = array( $plugin => $plugin ); } else { $block_notice[ $plugin ] = $plugin; } update_option( 'wpins_block_notice', $block_notice, 'no' ); } /** * AJAX callback when the deactivated form is submitted. * * @since 2.5.0 */ public function deactivate_reasons_form_submit() { check_ajax_referer( 'wpins_deactivation_nonce', 'security' ); if ( isset( $_POST[ 'values' ] ) ) { $values = sanitize_text_field( wp_unslash( $_POST[ 'values' ] ) ); update_option( 'wpins_deactivation_reason_' . $this->plugin_name, $values ); } if ( isset( $_POST[ 'details' ] ) ) { $details = sanitize_text_field( wp_unslash( $_POST[ 'details' ] ) ); update_option( 'wpins_deactivation_details_' . $this->plugin_name, $details ); } wp_die(); } /** * Filter the deactivation link to allow us to present a form when the user deactivates the plugin * * @since 2.5.0 */ public function deactivate_action_links( $links ) { /** * Check is tracking allowed or not. */ if ( ! $this->is_tracking_allowed() ) { return $links; } if ( isset( $links[ 'deactivate' ] ) && $this->include_goodbye_form ) { $deactivation_link = $links[ 'deactivate' ]; /** * Change the default deactivate button link. */ $deactivation_link = str_replace( '
__( 'I found a better plugin', 'betterdocs' ), 'extra_field' => __( 'Please share which plugin', 'betterdocs' ) ), __( "I couldn't get the plugin to work", 'betterdocs' ), __( 'It\'s a temporary deactivation', 'betterdocs' ), array( 'label' => __( 'Other', 'betterdocs' ), 'extra_field' => __( 'Please share the reason', 'betterdocs' ), 'type' => 'textarea' ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WP Insights (appsero) library hook; name is defined by the upstream tracker library. return apply_filters( 'wpins_form_text_' . $this->plugin_name, $form ); } /** * Deactivate Reasons Form. * This form will appears when user wants to deactivate the plugin to send you deactivated reasons. * * @since 3.0.0 */ public function deactivate_reasons_form() { $form = $this->deactivation_reasons(); $class_plugin_name = esc_attr( $this->plugin_name ); $html = '

' . $form[ 'heading' ] . '

'; $html .= '

' . esc_html( $form[ 'body' ] ) . '

'; if ( is_array( $form[ 'options' ] ) ) { $html .= '
    '; foreach ( $form[ 'options' ] as $option ) { if ( is_array( $option ) ) { $id = strtolower( str_replace( ' ', '_', esc_attr( $option[ 'label' ] ) ) ); $id = $id . '_' . $class_plugin_name; $html .= '
  • '; $html .= ''; $html .= '
    '; if ( isset( $option[ 'extra_field' ] ) && ! isset( $option[ 'type' ] ) ) { $html .= ''; } if ( isset( $option[ 'extra_field' ] ) && isset( $option[ 'type' ] ) ) { $html .= '<' . $option[ 'type' ] . ' style="display: none" type="text" name="' . esc_attr( $id ) . '" id="' . str_replace( ' ', '', esc_attr( $option[ 'extra_field' ] ) ) . '" placeholder="' . esc_attr( $option[ 'extra_field' ] ) . '">'; } $html .= '
  • '; } else { $id = strtolower( str_replace( ' ', '_', esc_attr( $option ) ) ); $id = $id . '_' . $class_plugin_name; $html .= '
  • '; } } $html .= '
'; } $html .= '
'; $html .= '

' . __( 'Submitting form', 'betterdocs' ) . '

'; $wrapper_class = '.wpinsights-goodbye-form-wrapper-' . $class_plugin_name; $styles = ''; $styles .= ''; $styles .= ''; echo $styles; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is styles, which is why escaping is not needed ?>