array( 'Complianz', true ), 'complianz-gdpr-premium' => array( 'Complianz', true ), 'cookie-law-info' => array( 'CookieYes', true ), 'cookiebot' => array( 'Cookiebot', true ), 'real-cookie-banner' => array( 'Real Cookie Banner', true ), 'real-cookie-banner-pro' => array( 'Real Cookie Banner', true ), 'gdpr-cookie-compliance' => array( 'GDPR Cookie Compliance', true ), 'cookie-notice' => array( 'Cookie Notice', true ), 'iubenda-cookie-law-solution' => array( 'iubenda', true ), 'borlabs-cookie' => array( 'Borlabs Cookie', false ), 'uk-cookie-consent' => array( 'Termly', false ), 'cookie-script-com' => array( 'Cookie-Script', false ), 'usercentrics-consent-management-platform' => array( 'Usercentrics', false ), ); /** * Hooks. */ public function register() { add_action( 'admin_post_dealroot_setup', array( $this, 'handle_save' ) ); add_action( 'admin_post_dealroot_setup_dismiss', array( $this, 'handle_dismiss' ) ); add_action( 'wp_ajax_dealroot_setup_check', array( $this, 'ajax_check' ) ); add_action( 'wp_ajax_dealroot_setup_chat', array( $this, 'ajax_chat' ) ); add_action( 'wp_ajax_dealroot_setup_telegram', array( $this, 'ajax_telegram' ) ); add_action( 'admin_notices', array( $this, 'notice' ) ); } /** * Steps in their order. * * @return array */ public static function steps() { return array( 'forms' => __( 'Forms', 'dealroot' ), 'tracking' => __( 'What to count', 'dealroot' ), 'test' => __( 'Test enquiry', 'dealroot' ), 'alerts' => __( 'Notifications', 'dealroot' ), 'consent' => __( 'Cookie consent', 'dealroot' ), ); } /** * Address of a step. * * @param string $step Step key, or "done". * @param array $args Further query arguments. * @return string */ public static function url( $step = '', array $args = array() ) { return Shell::url( self::PAGE, array_filter( array_merge( array( 'step' => $step ), $args ) ) ); } /** * Stored progress. * * @return array{seen: string[], finished: int, notice: int} */ public static function state() { $state = get_option( self::OPTION, array() ); $state = is_array( $state ) ? $state : array(); return array( 'seen' => array_values( array_intersect( (array) ( $state['seen'] ?? array() ), array_keys( self::steps() ) ) ), 'finished' => (int) ( $state['finished'] ?? 0 ), 'notice' => (int) ( $state['notice'] ?? 0 ), ); } /** * Whether the assistant was gone through to the end. * * @return bool */ public static function finished() { return self::state()['finished'] > 0; } /** * Called on activation: a first activation asks once, on the plugins screen, to set up. */ public static function activated() { if ( false !== get_option( self::OPTION ) ) { return; } add_option( self::OPTION, array( 'seen' => array(), 'finished' => 0, 'notice' => 1, ), '', false ); } /** * Stores progress. * * @param array $changes Keys of the state to change. */ private static function update_state( array $changes ) { update_option( self::OPTION, array_merge( self::state(), $changes ), false ); } /** * Notice after the first activation, on the plugins screen and the dashboard only. */ public function notice() { $screen = get_current_screen(); if ( ! $screen || ! in_array( $screen->id, array( 'plugins', 'dashboard' ), true ) || ! current_user_can( 'manage_options' ) ) { return; } $state = self::state(); if ( ! $state['notice'] || $state['finished'] || $state['seen'] ) { return; } $dismiss = wp_nonce_url( admin_url( 'admin-post.php?action=dealroot_setup_dismiss' ), 'dealroot_setup_dismiss' ); ?>

0 ) ); wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'plugins.php' ) ); exit; } /** * Current step from the address. * * @return string */ private static function current() { $step = isset( $_GET['step'] ) ? sanitize_key( wp_unslash( $_GET['step'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( 'done' === $step || isset( self::steps()[ $step ] ) ) { return $step; } return 'forms'; } /** * Step after the given one. * * @param string $step Step key. * @return string Key, or "done". */ private static function next( $step ) { $keys = array_keys( self::steps() ); $index = array_search( $step, $keys, true ); return false !== $index && isset( $keys[ $index + 1 ] ) ? $keys[ $index + 1 ] : 'done'; } /** * Step before the given one. * * @param string $step Step key. * @return string Key, or an empty string for the first step. */ private static function previous( $step ) { $keys = array_keys( self::steps() ); $index = 'done' === $step ? count( $keys ) : array_search( $step, $keys, true ); return $index ? $keys[ $index - 1 ] : ''; } /** * Whether a step has what it asks for, for the tick in the list of steps. * * @param string $step Step key. * @return bool */ private static function complete( $step ) { switch ( $step ) { case 'forms': return null !== Form_Scan::last(); case 'test': return (bool) self::test_lead( 0 ); case 'alerts': return Settings::get( 'telegram_enabled' ) && '' !== Settings::get( 'telegram_token' ) && '' !== Settings::get( 'telegram_chat_id' ); default: return in_array( $step, self::state()['seen'], true ); } } /** * Data for the script of the screen. * * @return array */ public static function script_data() { return array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'dealroot_setup' ), 'since' => time(), 'visits' => Visits::recent( 1 ), 'live' => Live::enabled(), 'visit' => __( 'Your visit was counted.', 'dealroot' ), /* translators: %s: title or address of a page */ 'onPage' => __( 'You are on the site now: %s', 'dealroot' ), /* translators: 1: name or email of the person, 2: name of the form */ 'lead' => __( 'Lead received: %1$s, sent with “%2$s”.', 'dealroot' ), 'leadOnly' => __( 'Lead received.', 'dealroot' ), 'open' => __( 'Open the lead', 'dealroot' ), 'looking' => __( 'Looking for your chat…', 'dealroot' ), 'sending' => __( 'Sending…', 'dealroot' ), 'sent' => __( 'Sent. Check Telegram.', 'dealroot' ), 'failed' => __( 'Something went wrong. Please reload the page and try again.', 'dealroot' ), ); } /** * Saves a step and moves on. */ public function handle_save() { check_admin_referer( 'dealroot_setup' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to change these settings.', 'dealroot' ), 403 ); } $step = isset( $_POST['step'] ) ? sanitize_key( wp_unslash( $_POST['step'] ) ) : ''; $go = isset( $_POST['go'] ) ? sanitize_key( wp_unslash( $_POST['go'] ) ) : 'next'; if ( ! isset( self::steps()[ $step ] ) ) { wp_safe_redirect( self::url() ); exit; } if ( 'skip' !== $go && 'back' !== $go ) { $changes = $this->posted( $step ); if ( $changes ) { update_option( Settings::OPTION, array_merge( Settings::all(), $changes ) ); } } $state = self::state(); if ( 'back' !== $go ) { $state['seen'][] = $step; } $target = 'back' === $go ? self::previous( $step ) : self::next( $step ); self::update_state( array( 'seen' => array_values( array_unique( $state['seen'] ) ), 'finished' => 'done' === $target ? time() : $state['finished'], ) ); wp_safe_redirect( self::url( '' !== $target ? $target : $step ) ); exit; } /** * Settings sent with a step. * * @param string $step Step key. * @return array */ private function posted( $step ) { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Checked in handle_save(). $input = isset( $_POST['dealroot'] ) && is_array( $_POST['dealroot'] ) ? wp_unslash( $_POST['dealroot'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Every value goes through Settings::sanitize(). // phpcs:enable $fields = array( 'tracking' => array( 'count_visits', 'track_clicks', 'form_stats', 'geo_cities', 'live_view', 'ai_enabled' ), 'alerts' => array( 'telegram_enabled', 'telegram_token', 'telegram_chat_id', 'digest_enabled', 'digest_recipients' ), 'consent' => array( 'consent_mode' ), ); if ( ! isset( $fields[ $step ] ) ) { return array(); } $changes = array(); foreach ( $fields[ $step ] as $key ) { $changes[ $key ] = isset( $input[ $key ] ) && is_scalar( $input[ $key ] ) ? (string) $input[ $key ] : ''; } // The digest goes to the site's email address when no one else is entered. if ( isset( $changes['digest_recipients'] ) && get_option( 'admin_email' ) === trim( $changes['digest_recipients'] ) ) { $changes['digest_recipients'] = ''; } return $changes; } /** * Latest lead sent through the test link. * * @param int $since Unix time, 0 for any time. * @return object|null */ private static function test_lead( $since ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->get_row( $wpdb->prepare( 'SELECT id, name, email, phone, form_name FROM %i WHERE created_at >= %s AND ( campaign = %s OR last_campaign = %s ) ORDER BY id DESC LIMIT 1', Installer::table(), gmdate( 'Y-m-d H:i:s', max( 0, (int) $since ) ), self::TEST_CAMPAIGN, self::TEST_CAMPAIGN ) ); } /** * What arrived since the test step was opened: the visit, the visitor on the live screen * and the lead. */ public function ajax_check() { check_ajax_referer( 'dealroot_setup', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( null, 403 ); } $since = isset( $_POST['since'] ) ? absint( $_POST['since'] ) : time(); $lead = self::test_lead( $since - 60 ); $page = ''; foreach ( Live::current() as $visitor ) { if ( strtotime( $visitor->updated . ' UTC' ) >= $since ) { $page = '' !== (string) $visitor->title ? (string) $visitor->title : (string) $visitor->page; break; } } $who = ''; if ( $lead ) { foreach ( array( 'name', 'email', 'phone' ) as $key ) { if ( '' !== (string) $lead->$key ) { $who = (string) $lead->$key; break; } } } wp_send_json_success( array( 'visits' => Visits::recent( 1 ), 'page' => $page, 'lead' => $lead ? array( 'who' => $who, 'form' => (string) $lead->form_name, 'url' => Shell::url( Admin::PAGE, array( 'lead' => (int) $lead->id ) ), ) : null, ) ); } /** * Bot token from the request; the masked value stands for the stored token. * * @return string */ private static function posted_token() { $token = isset( $_POST['token'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['token'] ) ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Checked by the callers. $saved = (string) Settings::get( 'telegram_token' ); return '' !== $saved && Settings::masked_token( $saved ) === $token ? $saved : $token; } /** * Finds the chat in which the bot was last written to. */ public function ajax_chat() { check_ajax_referer( 'dealroot_setup', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( null, 403 ); } $chat = Notifier::find_chat( self::posted_token() ); if ( is_wp_error( $chat ) ) { wp_send_json_error( array( 'message' => $chat->get_error_message() ) ); } wp_send_json_success( $chat ); } /** * Sends a test message with the values in the form, before they are saved. */ public function ajax_telegram() { check_ajax_referer( 'dealroot_setup', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( null, 403 ); } $chat = isset( $_POST['chat'] ) ? sanitize_text_field( wp_unslash( $_POST['chat'] ) ) : ''; $result = Notifier::send_telegram( self::posted_token(), $chat, /* translators: %s: site name. */ sprintf( __( 'Dealroot is connected to %s. New leads will appear here.', 'dealroot' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ) ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } wp_send_json_success(); } /** * Consent plugins that are active. * * @return array Name => whether it works with the WP Consent API. */ public static function consent_plugins() { $found = array(); foreach ( (array) get_option( 'active_plugins', array() ) as $file ) { $folder = strtok( (string) $file, '/' ); if ( isset( self::CONSENT_PLUGINS[ $folder ] ) ) { $found[ self::CONSENT_PLUGINS[ $folder ][0] ] = self::CONSENT_PLUGINS[ $folder ][1]; } } return $found; } /** * Whether the WP Consent API is there. * * @return bool */ private static function consent_api() { return class_exists( '\WP_CONSENT_API' ) || function_exists( 'wp_has_consent' ); } /** * Page output. */ public function render() { if ( ! current_user_can( 'manage_options' ) ) { return; } $step = self::current(); $steps = self::steps(); $keys = array_keys( $steps ); Shell::open( '', __( 'Set up Dealroot', 'dealroot' ), array( 'description' => __( 'Five short steps. Each one can be skipped and changed later in the settings.', 'dealroot' ), 'class' => 'dealroot-setup-page', 'back' => array( 'url' => Shell::url( Admin::PAGE ), 'label' => __( 'Overview', 'dealroot' ), ), ) ); ?>
    $label ) : ?>
  1. >
done(); ?>

step_forms(); } else { $this->open_form( $step ); call_user_func( array( $this, 'step_' . $step ) ); $this->close_form( $step ); } ?>

8 ) : ?>

>

toggle( 'count_visits', __( 'Visits', 'dealroot' ), __( 'Daily totals per channel, so you see how many visitors became leads.', 'dealroot' ) ); $this->toggle( 'track_clicks', __( 'Calls, WhatsApp and email clicks', 'dealroot' ), __( 'Clicks on your phone number, WhatsApp and email links, with the channel the visitor came from.', 'dealroot' ) ); $this->toggle( 'form_stats', __( 'Forms started and left', 'dealroot' ), __( 'How many visitors start a form and how many send it. What they type is never sent.', 'dealroot' ) ); $this->toggle( 'live_view', __( 'Live', 'dealroot' ), __( 'Who is on the site right now, which page they are on and whether they are filling in a form. Kept for ten minutes.', 'dealroot' ) ); $this->toggle( 'geo_cities', __( 'Cities on the map', 'dealroot' ), __( 'Downloads the free DB-IP City Lite database (about 60 MB) to your site, so visits show the city, such as Hamburg, instead of only the country. The address itself is not stored.', 'dealroot' ) ); $this->toggle( 'ai_enabled', __( 'AI assistant', 'dealroot' ), __( 'Sums up each enquiry in one sentence with its topics, marks urgent ones and spam, and writes a first draft of your answer on request. Without an account or key; email addresses and phone numbers are not sent.', 'dealroot' ) ); ?>

'dealroot', 'utm_medium' => 'test', 'utm_campaign' => self::TEST_CAMPAIGN, ), $url ); $lead = self::test_lead( 0 ); ?>

Telegram toggle( 'telegram_enabled', __( 'Send every new lead to Telegram', 'dealroot' ), __( 'With the name, the message and where the visitor came from.', 'dealroot' ) ); ?>
  1. @BotFather, send /newbot and copy the token it gives you.', 'dealroot' ), array( 'a' => array( 'href' => array(), 'target' => array(), 'rel' => array(), ), 'code' => array(), ) ); ?>
toggle( 'digest_enabled', __( 'Send a summary every Monday', 'dealroot' ), __( 'New leads, won deals and the channels that brought them, compared with the week before.', 'dealroot' ) ); ?>

array( __( 'Do not wait for consent', 'dealroot' ), __( 'For sites without visitors from the EU or the UK, or when your legal advisor says the tracking needs no consent.', 'dealroot' ), ), 'wp_consent_api' => array( __( 'Wait for my cookie banner', 'dealroot' ), __( 'Dealroot starts once the visitor accepts marketing cookies: in Cookiebot, CookieYes or Complianz, in a banner with the WP Consent API such as Real Cookie Banner, or in any banner that sets Google Consent Mode v2.', 'dealroot' ), ), 'custom' => array( __( 'My banner starts Dealroot itself', 'dealroot' ), __( 'For other banners: Dealroot waits until the banner runs dealroot.grantConsent() after the visitor agrees.', 'dealroot' ), ), ); ?>

$text ) : ?>