*/ final class Ajax { /** * The ID of this plugin. * * @since 2.0.0 * @access private * @var string $plugin_name The ID of this plugin. */ private $plugin_name; /** * All ajax events * * @since 2.0.0 * @access private * @var array $events All ajax events. */ private $events; /** * Api class * * @since 2.0.0 * @access private * @var Api $api Api class. */ private $api; /** * Cron class * * @since 2.0.0 * @access private * @var Cron $cron Cron class. */ private $cron; /** * Initialize the class and set its properties. * * @since 2.0.0 * @param string $plugin_name The name of this plugin. */ public function __construct( $plugin_name ) { $this->plugin_name = $plugin_name; $this->api = new Api(); $this->cron = new Cron(); $this->events = array( 'set_custom_var' => 'set_custom_var', 'toggle_feature' => 'toggle_feature', 'get_adstxt' => 'get_adstxt', 'get_adstxt_custom' => 'get_adstxt_custom', 'set_adstxt' => 'set_adstxt', 'check_adstxt' => 'check_adstxt', 'set_presized_div' => 'set_presized_div', 'get_articles_by_keywords' => 'get_articles_by_keywords', 'add_no_pub' => 'add_no_pub', 'remove_no_pub' => 'remove_no_pub', 'check_url_no_pub' => 'check_url_no_pub', 'url_exists' => 'url_exists', 'enabled_debug' => 'enabled_debug', 'options_fast_cmp' => 'options_fast_cmp', 'save_cmp_vendors' => 'save_cmp_vendors', 'login' => 'login', 'logout' => 'logout', 'create_account' => 'create_account', 'resend_registration_email' => 'resend_registration_email', 'create_website' => 'create_website', 'update_currency' => 'update_currency', 'get_logs' => 'get_logs', 'set_auto_update' => 'set_auto_update', 'save_billing_status' => 'save_billing_status', 'update_onboarding_status' => 'update_onboarding_status', 'get_contract_preview' => 'get_contract_preview', 'sign_contract' => 'sign_contract', 'sync_data' => 'sync_data', 'get_slots' => 'get_slots', 'update_slots' => 'update_slots', 'get_has_cmp' => 'get_has_cmp', 'get_xpath_check_results' => 'get_xpath_check_results', 'save_hosted_adstxt' => 'save_hosted_adstxt', 'update_plugin' => 'update_plugin', 'check_adstxt_file_for_hosted' => 'check_adstxt_file_for_hosted', 'check_hosted_adstxt_redirect' => 'check_hosted_adstxt_redirect', 'get_health_issues' => 'get_health_issues', 'force_xpath_check' => 'force_xpath_check', 'save_gitbook_consent' => 'save_gitbook_consent', 'toggle_optimization_checklist' => 'toggle_optimization_checklist', 'get_event_annotations' => 'get_event_annotations', ); add_filter( 'posts_where', array( $this, 'search_post_title' ), 10, 2 ); } /** * Dispatch ajax request * * @since 2.0.0 * @return void */ public function dispatch() { if ( $this->security_check() !== true ) { return; } $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { $error = new WP_Error( '403', 'Security check is not valid' ); wp_send_json_error( $error ); } $action = isset( $_POST['action_actirise'] ) ? sanitize_text_field( wp_unslash( $_POST['action_actirise'] ) ) : null; if ( is_null( $action ) ) { $error = new WP_Error( '404', 'Action empty' ); wp_send_json_error( $error ); } if ( ! array_key_exists( $action, $this->events ) ) { $error = new WP_Error( '404', 'Action not found' ); wp_send_json_error( $error ); } $method = $this->events[ $action ]; if ( ! method_exists( $this, $method ) ) { $error = new WP_Error( '404', 'Method not found' ); wp_send_json_error( $error ); } $this->$method(); } /** * Filter for search post title by LIKE * * @since 2.0.0 * @param string $where * @param \WP_Query $wp_query * @return string */ public function search_post_title( $where, $wp_query ) { /** @var \wpdb $wpdb */ global $wpdb; $title = $wp_query->get( 'search_title' ); if ( $title ) { /** @var string $title_sql */ $title_sql = esc_sql( $wpdb->esc_like( $title ) ); // @phpstan-ignore argument.type $where .= ' AND ' . $wpdb->posts . ".post_title LIKE '%" . $title_sql . "%'"; } return $where; } /** * Save custom var * * @since 2.0.0 * @return void */ private function set_custom_var() { $post_data = $this->get_sanitized_post_data( array( 'customVar' ) ); if ( in_array( null, $post_data, true ) ) { Helpers::send_error( '500', 'Post empty' ); } /** @var string $custom_var */ $custom_var = $post_data['customVar']; $json_custom_var = json_decode( stripslashes( $custom_var ) ); if ( ! is_array( $json_custom_var ) ) { $error = new WP_Error( '500', 'Need array' ); wp_send_json_error( $error ); } /** @var array<\stdClass> $json_custom_var */ foreach ( $json_custom_var as $custom ) { Options::update( $custom->name, $custom->value ); // @phpstan-ignore argument.type } wp_send_json( array( 'success' => true, ) ); } /** * Enabled or disabled feature * * @since 2.0.0 * @return void */ private function toggle_feature() { $post_data = $this->get_sanitized_post_data( array( 'feature', 'feature_value' ) ); if ( in_array( null, $post_data, true ) ) { Helpers::send_error( '500', 'Post empty' ); } if ( ! in_array( $post_data['feature'], array( 'adstxt', 'presizeddiv' ), true ) ) { $error = new WP_Error( '500', 'Feature not found' ); wp_send_json_error( $error ); } if ( $post_data['feature'] === 'adstxt' ) { $path_ads_txt = ABSPATH . 'ads.txt'; if ( $post_data['feature_value'] === 'true' ) { AdsTxt::update_file(); $this->cron->update_adstxt(); // Don't block if hosted mode is enabled $is_hosted = Options::get( 'hosted-adstxt-enabled', false ) === true; if ( ! $is_hosted && Options::get( 'adstxt-existing', false ) ) { wp_send_json( array( 'success' => false, ) ); } } else { $this->cron->check_adstxt(); } EventAnnotation::add_event( 'Ads.txt Manager', 'Ads.txt feature set to ' . ( $post_data['feature_value'] === 'true' ? 'enabled' : 'disabled' ), 'website_update', Options::get( 'adstxt-active', false ), $post_data['feature_value'] === 'true' ? 'true' : 'false' ); } if ( $post_data['feature'] === 'presizeddiv' ) { EventAnnotation::add_event( 'Core web vitals', 'Presized Div feature set to ' . ( $post_data['feature_value'] === 'true' ? 'enabled' : 'disabled' ), 'website_update', Options::get( 'presizeddiv-active', false ) === true ? 'true' : 'false', $post_data['feature_value'] === 'true' ? 'true' : 'false' ); } Logger::add_log_action( 'Feature ' . $post_data['feature'] . ' set to ' . $post_data['feature_value'] ); Options::update( $post_data['feature'] . '-active', filter_var( $post_data['feature_value'], FILTER_VALIDATE_BOOLEAN ) ); // Get updated health issues $health_check = new HealthCheck(); $health_issues = $health_check->get_issues(); wp_send_json( array( 'success' => true, 'health_issues' => $health_issues, ) ); } /** * Get ads.txt * * @since 2.0.0 * @return void */ private function get_adstxt() { if ( ! Options::get( 'settings-uuid' ) || ! Options::get( 'settings-uuid-type', 'boot' ) ) { $error = new WP_Error( '500', 'Error' ); wp_send_json_error( $error ); } $response = AdsTxt::get_from_api(); if ( false !== $response ) { wp_send_json( array( 'success' => true, 'data' => $response, ) ); } else { Logger::add_log( 'get_adstxt false', 'admin/include/ajax', 'error' ); $error = new WP_Error( '500', 'Error' ); wp_send_json_error( $error ); } } /** * Get ads.txt custom * * @since 2.0.0 * @return void */ private function get_adstxt_custom() { $adstxt_custom = Options::get( 'adstxt-custom', array() ); wp_send_json( array( 'success' => true, 'data' => ! $adstxt_custom ? array() : $adstxt_custom, ) ); } /** * Set ads.txt * * @since 2.0.0 * @return void */ private function set_adstxt() { $post_data = $this->get_sanitized_post_data( array( 'adsTxtCustom' ) ); if ( in_array( null, $post_data, true ) || empty( $post_data['adsTxtCustom'] ) ) { Helpers::send_error( '500', 'Post empty' ); } $old_adstxt = Options::get( 'adstxt-custom', '' ); Options::update( 'adstxt-custom', $post_data['adsTxtCustom'] ); Logger::add_log_action( 'Ads.txt custom update' ); EventAnnotation::add_event( 'Custom Ads.txt modified', 'The custom ads.txt file content has been updated', 'website_update', $old_adstxt, $post_data['adsTxtCustom'] ); AdsTxt::update_file(); $health_check = new HealthCheck(); $health_issues = $health_check->get_issues(); wp_send_json( array( 'success' => true, 'health_issues' => $health_issues, ) ); } /** * Check ads.txt lines * * @since 3.0.0 * @return void */ private function check_adstxt() { $post_data = $this->get_sanitized_post_data( array( 'force' ) ); /** @var string $force */ $force = $post_data['force']; $response = AdsTxt::check_from_api( $force ); wp_send_json( array( 'success' => true, 'data' => $response, ) ); } /** * Get settings * * @since 2.0.0 * @return void */ private function set_presized_div() { $post_data = $this->get_sanitized_post_data( array( 'presizedDiv' ) ); /** @var string $presized_div_data */ $presized_div_data = $post_data['presizedDiv']; Logger::add_log_action( 'Presized div ' . wp_json_encode( stripslashes( $presized_div_data ) ) ); if ( in_array( null, $post_data, true ) || empty( $presized_div_data ) ) { Helpers::send_error( '500', 'Post empty' ); } /** @var array $presized_div_post */ $presized_div_post = json_decode( stripslashes( $presized_div_data ) ); $presized_div = array(); $old_presized_div = Options::get( 'presizeddiv-selected', array() ); $old_presized_div_map = array(); // Create a map of old settings for comparison if ( is_array( $old_presized_div ) ) { foreach ( $old_presized_div as $old_div ) { if ( isset( $old_div['slotName'] ) ) { /** @var string $slot_name */ $slot_name = $old_div['slotName']; $old_presized_div_map[ $slot_name ] = isset( $old_div['active'] ) ? $old_div['active'] : false; } } } foreach ( $presized_div_post as $key => $value ) { $presized_div[] = array( 'slotName' => $value->slotName, 'active' => $value->active, ); // Track changes for each presized div slot $old_status = isset( $old_presized_div_map[ $value->slotName ] ) ? $old_presized_div_map[ $value->slotName ] : false; $new_status = $value->active; if ( $old_status !== $new_status ) { EventAnnotation::add_event( 'Presized div `' . $value->slotName . '`', 'Presized div `' . $value->slotName . '` feature set to ' . ( $new_status ? 'enabled' : 'disabled' ), 'website_update', $old_status, $new_status ); } } Options::update( 'presizeddiv-selected', $presized_div ); // Get updated health issues $health_check = new HealthCheck(); $health_issues = $health_check->get_issues(); wp_send_json( array( 'success' => true, 'health_issues' => $health_issues, ) ); } /** * Get page, post, category, tag by keywords * * @since 2.0.0 * @return void */ private function get_articles_by_keywords() { $post_data = $this->get_sanitized_post_data( array( 'keywords' ) ); if ( in_array( null, $post_data, true ) ) { Helpers::send_error( '500', 'Post empty' ); } /** @var string $keywords */ $keywords = $post_data['keywords']; $query_page = new \WP_Query( array( 'post_type' => 'any', 'search_title' => $keywords, 'cache_result' => false, 'orderby' => 'post_date', ) ); /** @var array<\WP_Post> $post */ $post = $query_page->posts; $result = array(); foreach ( $post as $page ) { if ( $page->post_type !== 'attachment' ) { $image = get_the_post_thumbnail_url( $page->ID ); if ( has_excerpt( $page ) ) { $page->post_content = wp_strip_all_tags( get_the_excerpt( $page ), true ); } if ( $page->post_type === 'page' ) { $page->post_content = 'article'; } $result[] = array( 'id' => $page->ID, 'guid' => get_permalink( $page->ID ), 'title' => $page->post_title, 'publishedDate' => $page->post_date, 'summary' => substr( $page->post_content, 0, 100 ), 'image' => $image, 'type' => $page->post_type, ); } } $count = count( $result ); $query_terms = get_terms( array( 'taxonomy' => array( 'post_tag', 'category' ), 'name__like' => $keywords, ) ); $result_terms = array(); /** @var array<\WP_Term> $query_terms */ foreach ( $query_terms as &$terms ) { $term_link = get_term_link( $terms ); if ( is_wp_error( $term_link ) ) { continue; } $result_terms[] = array( 'id' => $terms->term_id, 'title' => $terms->name, 'summary' => substr( $terms->description, 0, 100 ), 'type' => $terms->taxonomy, 'url' => $term_link, ); } $count += count( $result_terms ); wp_send_json( array( 'page' => $result, 'taxonomy' => $result_terms, 'count' => $count, 'keywords' => $keywords, 'success' => true, ) ); } /** * Add no pub item * * @since 2.0.0 * @return void */ private function add_no_pub() { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { $error = new WP_Error( '403', 'Security check is not valid' ); wp_send_json_error( $error ); } $post_data = array( 'article' => isset( $_POST['article'] ) ? sanitize_text_field( wp_unslash( $_POST['article'] ) ) : null, 'summary' => isset( $_POST['summary'] ) ? sanitize_text_field( wp_unslash( $_POST['summary'] ) ) : null, ); if ( is_null( $post_data['article'] ) || is_null( $post_data['summary'] ) ) { if ( is_null( $post_data['article'] ) ) { Logger::add_log( 'add_no_pub post_data is null for article', 'admin/include/ajax', 'error' ); } if ( is_null( $post_data['summary'] ) ) { Logger::add_log( 'add_no_pub post_data is null for summary', 'admin/include/ajax', 'error' ); } $error = new WP_Error( '500', 'Post empty' ); wp_send_json_error( $error ); } /** @var string $article */ $article = urldecode( $post_data['article'] ); /** @var \stdClass $article */ $article = json_decode( $article ); if ( ! is_object( $article ) ) { Logger::add_log( 'add_no_pub article is not object', 'admin/include/ajax', 'error' ); $error = new WP_Error( '500', 'Need object' ); wp_send_json_error( $error ); } /** @var string $summary */ $summary = urldecode( $post_data['summary'] ); $article->summary = $summary; $start = Helpers::wp_local_to_utc( isset( $article->startDate ) ? $article->startDate : '' ); $end = Helpers::wp_local_to_utc( isset( $article->endDate ) ? $article->endDate : '' ); if ( false === $start || false === $end ) { wp_send_json( array( 'success' => false, 'error' => 'invalid_date', ) ); } if ( null === $start ) { $start = gmdate( 'Y-m-d\TH:i:s\Z' ); } if ( null !== $end && strtotime( $end ) <= strtotime( $start ) ) { wp_send_json( array( 'success' => false, 'error' => 'invalid_range', ) ); } $article->startDate = $start; $article->endDate = $end; /** @var array<\stdClass> $no_pub */ $no_pub = Options::get( 'nopub', array() ); // Check for duplicates by ID and type, or by URL foreach ( $no_pub as $existing_item ) { if ( ! is_object( $existing_item ) ) { continue; } // Check by ID and type if ( isset( $existing_item->id ) && isset( $article->id ) && isset( $existing_item->type ) && isset( $article->type ) && $existing_item->id === $article->id && $existing_item->type === $article->type ) { wp_send_json( array( 'success' => false, 'error' => 'duplicate', 'message' => 'URL already exists in the list', ) ); } // Check by URL if ( isset( $existing_item->url ) && isset( $article->url ) && $existing_item->url === $article->url ) { wp_send_json( array( 'success' => false, 'error' => 'duplicate', 'message' => 'URL already exists in the list', ) ); } } $no_pub[] = $article; foreach ( $no_pub as $key => $value ) { if ( ! isset( $value->url ) ) { unset( $no_pub[ $key ] ); } } $add_item_url = null; if ( isset( $article->url ) && is_string( $article->url ) ) { $add_item_url = strlen( $article->url ) > 145 ? substr( $article->url, 0, 142 ) . '...' : $article->url; } Options::update( 'nopub', $no_pub ); EventAnnotation::add_event( 'No ad - page added', 'URL: ' . ( isset( $add_item_url ) ? $add_item_url : 'Unknown' ), 'website_update', count( $no_pub ) - 1, count( $no_pub ) ); // View-only field for the client list (not persisted). $article->windowLabel = Helpers::format_window( $article, __( 'unlimited', 'actirise' ) ); wp_send_json( array( 'success' => true, 'item' => $article, ) ); } /** * Remove no pub item * * @since 2.0.0 * @return void */ private function remove_no_pub() { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { $error = new WP_Error( '403', 'Security check is not valid' ); wp_send_json_error( $error ); } $post_data = array( 'id' => isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : null, 'type' => isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : null, ); if ( is_null( $post_data['id'] ) || is_null( $post_data['type'] ) ) { $error = new WP_Error( '500', 'Post empty' ); wp_send_json_error( $error ); } /** @var int $id */ $id = intval( $post_data['id'] ); /** @var string $type */ $type = $post_data['type']; /** @var array<\stdClass> $no_pub */ $no_pub = Options::get( 'nopub', array() ); /** @var array $no_pub */ foreach ( $no_pub as $key => $value ) { if ( ! isset( $value->url ) ) { unset( $no_pub[ $key ] ); } } $removed_item_url = null; $removed_item = null; /** @var array $no_pub */ foreach ( $no_pub as $key => $value ) { if ( $value->id === $id && $value->type === $type ) { $removed_item = $value; unset( $no_pub[ $key ] ); } } if ( $removed_item !== null && isset( $removed_item->url ) && is_string( $removed_item->url ) ) { $removed_item_url = strlen( $removed_item->url ) > 145 ? substr( $removed_item->url, 0, 142 ) . '...' : $removed_item->url; } Options::update( 'nopub', $no_pub ); if ( $removed_item !== null ) { EventAnnotation::add_event( 'No ad - page removed', 'URL: ' . ( isset( $removed_item_url ) ? $removed_item_url : 'Unknown' ), 'website_update', $removed_item, null ); } wp_send_json( array( 'success' => true, ) ); } /** * Check if url is valid and get informations (post, page, tag, category) * * @since 2.0.0 * @return void */ private function check_url_no_pub() { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { $error = new WP_Error( '403', 'Security check is not valid' ); wp_send_json_error( $error ); } $post_data = array( 'url' => isset( $_POST['url'] ) ? sanitize_text_field( wp_unslash( $_POST['url'] ) ) : null, ); if ( is_null( $post_data['url'] ) ) { Logger::add_log( 'check_url_no_pub post_data is null for url', 'admin/include/ajax', 'error' ); $error = new WP_Error( '500', 'Post empty' ); wp_send_json_error( $error ); } $url = $post_data['url']; $result = Helpers::resolve_url_to_content( $url ); wp_send_json( array( 'success' => true, 'result' => $result, ) ); } /** * Check if url exist * * @since 2.0.0 * @return void */ private function url_exists() { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { $error = new WP_Error( '403', 'Security check is not valid' ); wp_send_json_error( $error ); } $post_data = array( 'id' => isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : null, 'type' => isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : null, ); if ( is_null( $post_data['id'] ) || is_null( $post_data['type'] ) ) { if ( is_null( $post_data['id'] ) ) { Logger::add_log( 'url_exists post_data is null for id', 'admin/include/ajax', 'error' ); } if ( is_null( $post_data['type'] ) ) { Logger::add_log( 'url_exists post_data is null for type', 'admin/include/ajax', 'error' ); } $error = new WP_Error( '500', 'Post empty' ); wp_send_json_error( $error ); } /** @var int $id */ $id = $post_data['id']; $type = $post_data['type']; $exist = false; if ( $type === 'category' || $type === 'post_tag' ) { $terms = get_term_by( 'ID', $id, $type ); if ( $terms !== false ) { $exist = true; } } else { $post = get_post( $id ); if ( $post !== 0 && $post !== null ) { if ( $post->post_status !== 'trash' ) { $exist = true; } } } wp_send_json( array( 'success' => true, 'exist' => $exist, ) ); } /** * Set debug mode * * @since 2.0.0 * @return void */ private function enabled_debug() { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { $error = new WP_Error( '403', 'Security check is not valid' ); wp_send_json_error( $error ); } $post_data = array( 'debug' => isset( $_POST['debug'] ) ? sanitize_text_field( wp_unslash( $_POST['debug'] ) ) : null, ); if ( is_null( $post_data['debug'] ) ) { $error = new WP_Error( '500', 'Post empty' ); wp_send_json_error( $error ); } $debug = $post_data['debug']; $old_debug = Options::get( 'debug-enabled', false ); Options::update( 'debug-enabled', $debug ); wp_send_json( array( 'success' => true, ) ); } /** * Update options for fast cmp * * @since 2.2.0 * @return void */ private function options_fast_cmp() { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { $error = new WP_Error( '403', 'Security check is not valid' ); wp_send_json_error( $error ); } $post_data = array( 'options' => isset( $_POST['options'] ) ? sanitize_text_field( wp_unslash( $_POST['options'] ) ) : null, ); if ( is_null( $post_data['options'] ) ) { $error = new WP_Error( '500', 'Post empty' ); wp_send_json_error( $error ); } /** @var array $options */ $options = json_decode( $post_data['options'] ); $enabled_change = false; $old_options = array(); foreach ( $options as $name => $value ) { $old_options[ $name ] = Options::get( 'fastcmp-' . $name, null ); } // The keys outside the Actirise contract are persisted first and // unconditionally — `fastcmp-enabled` above all. It decides whether the // consent banner is injected at all, so an Actirise outage must never stop // a publisher from switching their CMP off: a site in breach would stay // live until the API came back. $contract_keys = ConfigPayload::contract_keys(); foreach ( $options as $name => $value ) { if ( in_array( $name, $contract_keys, true ) ) { continue; } if ( $name === 'enabled' ) { $enabled_change = $old_options[$name] !== ( $value === '1' ); } /** @var string $value */ Options::update( 'fastcmp-' . $name, $value ); } // Publish what changed before persisting it, so WordPress and Actirise can // never end up describing different banners. The publication reads the // option store while it still holds the pre-submission values, which is // what lets it send only the fields the publisher actually touched. $sync = new ConfigSync(); if ( ! $sync->publish_save( $options ) ) { Logger::add_log( 'FastCMP save not published; the local write was held back', ConfigSync::LOG_CONTEXT, 'error' ); wp_send_json_error( new WP_Error( '502', 'FastCMP configuration could not be saved to Actirise' ) ); } foreach ( $options as $name => $value ) { if ( ! in_array( $name, $contract_keys, true ) ) { continue; } if ( $name === 'consentV2' ) { Logger::add_log_action( 'Change consent mode V2: ' . ( $value ? 'true' : 'false' ) ); } /** @var string $value */ Options::update( 'fastcmp-' . $name, $value ); } if ( $enabled_change ) { $enabled = Options::get( 'fastcmp-enabled', null ); EventAnnotation::add_event( 'FastCMP', 'FastCMP feature set to ' . ( $enabled ? 'enabled' : 'disabled' ), 'website_update', $old_options['enabled'] ? 'enabled' : 'disabled', $enabled ? 'enabled' : 'disabled' ); Logger::add_log_action( 'Feature CMP set to ' . ( $enabled ? 'true' : 'false' ) ); } else { EventAnnotation::add_event( 'FastCMP settings updated', '', 'website_update', $old_options, $options ); } $health_check = new HealthCheck(); $health_issues = $health_check->get_issues(); // Keep the admin bootstrap in step with the save: window.actiriseJS.health_check // is served from 'health-check-cache', so without this a reload would show the // pre-save state until Cron::check_optimization() next runs. Options::update( 'health-check-cache', $health_issues ); wp_send_json( array( 'success' => true, 'health_issues' => $health_issues, ) ); } /** * Login to API v2 * * @since 2.3.15 * @return void */ private function login() { $post_data = $this->get_sanitized_post_data( array( 'email', 'password' ) ); if ( in_array( null, $post_data, true ) ) { Helpers::send_error( '500', 'Post empty' ); } $response = $this->api->post( 'api', 'authentication_token', $post_data ); if ( $this->is_email_not_validated_response( $response ) ) { wp_send_json( array( 'success' => true, 'data' => array( 'emailNotValidated' => true, ), ) ); } $validated_response = $this->validate_api_response( $response, 'Error while login' ); /** @var string $token */ $token = $validated_response['token']; /** @var string $user */ $user = $validated_response['user']; /** @var string $user_id */ $user_id = Helpers::extract_id( $user ); Options::update( 'settings-analytics-token', $token ); Options::update( 'settings-analytics-userid', $user_id ); $user_role = Helpers::get_user_role( $user_id, $token ); $currency = $validated_response['currency']; $api_token_response = $this->api->post( 'api', 'api_tokens', array( 'name' => 'wordpress' ), $token ); $api_token = $this->validate_api_response( $api_token_response, 'Error while login api token', false ); $website = Helpers::get_website_info(); $uuid = ''; $organizations = array(); if ( empty( $website ) ) { if ( $user_role !== 'organization_manager' ) { wp_send_json( array( 'success' => false, 'message' => 'Access denied. Please try to log in with an account associated with this website.', ) ); } $organizations = $this->get_organizations( $token ); } else { Core::check_website_status( $website ); $uuid = $this->get_organization_uuid( $token ); Options::update( 'settings-uuid', $uuid, true ); Options::update( 'settings-uuid-type', 'universal', true ); Options::update( 'settings-analytics-token', $api_token['token'], true ); Options::update( 'website-exists', true ); $this->cron->update_adstxt(); $this->cron->check_presized_div(); if ( ACTIRISE_CRON === 'true' ) { $this->cron->schedule(); } } wp_send_json( array( 'success' => true, 'data' => array( 'auth_token' => $token, 'token' => $api_token['token'], 'uuid' => $uuid, 'userId' => $user_id, 'userRole' => $user_role, 'currency' => $currency, 'organizations' => $organizations, 'onboardingStatus' => Options::get( 'onboarding-status' ), 'contractType' => Options::get( 'contract-type', 'essential' ), 'contractStatus' => Options::get( 'contract-status' ), 'trialEndDate' => Options::get( 'trial-end-date' ), 'adsServing' => Options::get( 'ads-serving' ), 'type' => Options::get( 'settings-uuid-type' ), 'presizedDiv' => Options::get( 'presizeddiv-selected', array() ), 'adstxt' => Options::get( 'adstxt-actirise', '' ), 'customv' => array( 'custom1' => Options::get( 'custom1', 'author_ID' ), 'custom2' => Options::get( 'custom2', 'category_0_slug' ), 'custom3' => Options::get( 'custom3', 'post_ID' ), 'custom4' => Options::get( 'custom4', '' ), 'custom5' => Options::get( 'custom5', '' ), ), ), ) ); } /** * Logout from API v2 * * @since 2.0.0 * @return void */ private function logout() { Options::delete( 'settings-analytics-token' ); Options::delete( 'settings-analytics-userid' ); Options::delete( 'gitbook-consent' ); wp_send_json( array( 'success' => true, ) ); } /** * Create a lightweight account through the centralized registration workflow. * * Only creates the user (firstname, lastname, email, password) via the new * `/user_registrations` endpoint. The organization, website and NDA steps are * completed on the central Actirise platform. The API answers 204 (no token): * a validation email is sent and the user must confirm it before logging in. * * @since 2.5.0 * @return void */ private function create_account() { $post_data = $this->get_sanitized_post_data( array( 'firstname', 'lastname', 'email', 'password', ) ); if ( in_array( null, $post_data, true ) ) { Helpers::send_error( '500', 'Post empty' ); } $args = array( 'workEmail' => $post_data['email'], 'firstName' => $post_data['firstname'], 'lastName' => $post_data['lastname'], 'password' => $post_data['password'], 'registrationOrigin' => 'wordpress_plugin', // Full URL of the Actirise plugin page in the WP admin dashboard, // used as the fallback link in the welcome email. 'wordpressFallback' => Helpers::get_page_url(), ); $response = $this->api->post( 'api', 'user_registrations', $args ); if ( is_wp_error( $response ) ) { $error_code = (string) $response->get_error_code(); // 409: an account with this email address already exists. if ( '409' === $error_code ) { wp_send_json( array( 'success' => true, 'data' => array( 'emailAlreadyExists' => true, ), ) ); } // 422: validation error (invalid email, weak/compromised password, ...). if ( '422' === $error_code ) { wp_send_json_error( array( 'response' => $response->get_error_data(), 'status' => 422, ) ); } Logger::add_log( 'Error while user registration', 'admin/include/ajax', 'error' ); wp_send_json_error( array( 'response' => $response, 'status' => $error_code, ) ); } wp_send_json( array( 'success' => true, 'data' => array( 'email' => $post_data['email'], ), ) ); } /** * Resend the registration validation email. * * Calls the public `/user_registrations/resend` endpoint (202 Accepted). The * endpoint is idempotent: the same answer is returned whether the email is * unknown or already validated, to avoid account enumeration. * * @since 4.2.0 * @return void */ private function resend_registration_email() { $post_data = $this->get_sanitized_post_data( array( 'email' ) ); if ( $post_data['email'] === null ) { Helpers::send_error( '500', 'Post empty' ); } $response = $this->api->post( 'api', 'user_registrations/resend', array( 'workEmail' => $post_data['email'] ) ); if ( is_wp_error( $response ) ) { $error_code = (string) $response->get_error_code(); // 429: too many resend attempts. if ( '429' === $error_code ) { wp_send_json_error( array( 'response' => 'rate_limited', 'status' => 429, ) ); } Logger::add_log( 'Error while resending registration email', 'admin/include/ajax', 'error' ); wp_send_json_error( array( 'response' => $response, 'status' => $error_code, ) ); } wp_send_json( array( 'success' => true, ) ); } /** * Create new website * * @since 2.5.0 * @return void */ private function create_website() { $post_data = $this->get_sanitized_post_data( array( 'organization' ) ); if ( $post_data['organization'] === null ) { Helpers::send_error( '500', 'Post empty' ); } $website_domain = rawurlencode( Helpers::get_server_details()['host'] ); $args = array( 'domain' => $website_domain, 'name' => get_bloginfo( 'name' ), 'websiteProduct' => '/v2.0/website_products/3', 'organization' => $post_data['organization'], ); /** @var string $token */ $token = Options::get( 'settings-analytics-token', '' ); /** @var string $organization_iri */ $organization_iri = $post_data['organization']; $organization_id = Helpers::extract_id( $organization_iri ); $response = $this->api->post( 'api', 'websites', $args, $token ); if ( is_wp_error( $response ) ) { Logger::add_log( 'Error while register wp_e', 'admin/include/ajax', 'error' ); $error = new WP_Error( '500', 'Error while register wp_e' ); wp_send_json_error( array( $error, $response, $args ) ); } if ( ! is_array( $response ) ) { Logger::add_log( 'Error while register not_a', 'admin/include/ajax', 'error' ); $error = new WP_Error( '500', 'Error while register not_a' ); wp_send_json_error( array( $error, $response ) ); } $api_token_response = $this->api->post( 'api', 'api_tokens', array( 'name' => 'wordpress' ), $token ); $api_token = $this->validate_api_response( $api_token_response, 'Error while login api token', false ); $uuid = $this->get_organization_uuid( $token, $organization_id ); Options::update( 'settings-uuid', $uuid, true ); Options::update( 'settings-uuid-type', 'universal', true ); Options::update( 'settings-analytics-token', $api_token['token'] ); Options::update( 'website-exists', true ); // The creation response is a full website resource, so hand it to the // shared writer instead of dropping it: the website uuid it carries is // the last prerequisite of the push-capability report, and storing it // here reports the capability during signup rather than on some later // page load. Runs after the API token is stored so the report goes out // with the plugin's own credential, and before the explicit // `onboarding-status` below so that value still wins. /** @var array $response */ Core::check_website_status( $response ); Options::update( 'onboarding-status', 'approval' ); if ( Options::get( 'settings-analytics-token', '' ) !== '' && Options::get( 'settings-analytics-userid', '' ) === '' ) { Helpers::get_user_id(); } $this->cron->update_adstxt(); if ( ACTIRISE_CRON === 'true' ) { $this->cron->schedule(); } wp_send_json( array( 'success' => true, 'data' => $response, ) ); } /** * Update currency settings * * @since 2.5.0 * @return void */ private function update_currency() { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { $error = new WP_Error( '403', 'Security check is not valid' ); wp_send_json_error( $error ); } $post_data = array( 'currency' => isset( $_POST['currency'] ) ? sanitize_text_field( wp_unslash( $_POST['currency'] ) ) : null, ); if ( $post_data['currency'] === null ) { $error = new WP_Error( '500', 'Post empty' ); wp_send_json_error( $error ); } /** @var string $user_id */ $user_id = Options::get( 'settings-analytics-userid', '' ); /** @var string $token */ $token = Options::get( 'settings-analytics-token', '' ); if ( $user_id !== '' && $token !== '' ) { $response = $this->api->patch( 'api', 'users/' . $user_id, array( 'currency' => $post_data['currency'], ), $token ); if ( is_wp_error( $response ) ) { Logger::add_log( 'Error while updating currency ' . $response->get_error_code(), 'admin/include/ajax', 'error' ); $error = new WP_Error( '500', 'Error while updating currency' ); wp_send_json_error( $error ); } } Options::update( 'currency', $post_data['currency'] ); Logger::add_log_action( 'Currency set to ' . $post_data['currency'] ); wp_send_json( array( 'success' => true, 'data' => $post_data['currency'], ) ); } /** * Get logs * * @since 2.5.0 * @return void */ private function get_logs() { $logs = Logger::get_logs(); $logs_actions = Logger::get_logs_action(); wp_send_json( array( 'success' => true, 'data' => array( 'logs' => $logs, 'logs_action' => $logs_actions, ), ) ); } /** * Set auto update settings * * @since 2.6.5 * @return void */ private function set_auto_update() { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { $error = new WP_Error( '403', 'Security check is not valid' ); wp_send_json_error( $error ); } $post_data = array( 'autoUpdate' => isset( $_POST['autoUpdate'] ) ? sanitize_text_field( wp_unslash( $_POST['autoUpdate'] ) ) : null, ); if ( is_null( $post_data['autoUpdate'] ) ) { $error = new WP_Error( '500', 'Post empty' ); wp_send_json_error( $error ); } $this->cron->set_auto_update( $post_data['autoUpdate'] === 'true' ); Logger::add_log_action( 'Auto update set to ' . $post_data['autoUpdate'] ); wp_send_json( array( 'success' => true, ) ); } /** * Update onboarding status * * @since 2.5.0 * @return void */ private function update_onboarding_status() { $post_data = $this->get_sanitized_post_data( array( 'status' ) ); if ( $post_data['status'] === null ) { $error = new WP_Error( '500', 'Post empty' ); wp_send_json_error( $error ); } $statuses = array( 'premium_io', 'ads', 'cmp', 'activation', 'congrats', ); $status_index = array_search( $post_data['status'], $statuses, true ); if ( $status_index === false || $status_index === count( $statuses ) - 1 ) { $error = new WP_Error( '400', 'Invalid or final status' ); wp_send_json_error( $error ); } $next_status = $statuses[$status_index + 1]; $trial_end_date = null; if ( $next_status === 'ads' ) { $this->cron->update_adstxt(); } if ( $post_data['status'] === 'activation' ) { Options::update( 'activation-time', time() ); $website = Helpers::get_website_info(); if ( $website['adsServing'] === false ) { /** @var string $token */ $token = Options::get( 'settings-analytics-token', '' ); $response_config = $this->api->post( 'api', 'default_configs', array( 'website' => $website['@id'] ), $token ); if ( is_wp_error( $response_config ) ) { Logger::add_log( 'update_onboarding_status default_configs error', 'admin/include/ajax', 'error' ); Helpers::send_error( '500', 'Error while activating Actirise' ); return; } } if ( isset( $website['trialEndDate'] ) ) { $trial_end_date = $website['trialEndDate']; } } Options::update( 'onboarding-status', $next_status ); wp_send_json( array( 'success' => true, 'status' => $next_status, 'trialEndDate' => $trial_end_date, ) ); } /** * Get contract preview * * @since 3.0.0 * @return void */ private function get_contract_preview() { $website = Helpers::get_website_info(); /** @var string $website_id */ $website_id = $website['@id']; $endpoint = str_replace( '/v2.0/', '', $website_id ) . '/contract'; /** @var string $token */ $token = Options::get( 'settings-analytics-token', '' ); $response = $this->api->get( 'api', $endpoint, array(), $token ); if ( is_wp_error( $response ) ) { Helpers::send_error( '500', 'Error while get_contract_preview' ); } wp_send_json( array( 'success' => true, 'data' => $response, ) ); } /** * Sign contract * * @since 3.0.0 * @return void */ private function sign_contract() { $website = Helpers::get_website_info(); /** @var string $token */ $token = Options::get( 'settings-analytics-token', '' ); $response = $this->api->post( 'api', 'websites_contracts_signed', array( 'websites' => array( $website['@id'] ) ), $token ); if ( is_wp_error( $response ) ) { Helpers::send_error( '500', 'Error while signing contract' ); } wp_send_json( array( 'success' => true, 'data' => $response, ) ); } /** * Save CMP vendors snapshot for comparison * * @since 3.3.0 * @return void */ private function save_cmp_vendors() { $post_data = $this->get_sanitized_post_data( array( 'vendors' ) ); if ( in_array( null, $post_data, true ) ) { Helpers::send_error( '500', 'Post empty' ); } /** @var string $vendors_json */ $vendors_json = $post_data['vendors']; $vendors = json_decode( stripslashes( $vendors_json ), true ); if ( ! is_array( $vendors ) ) { Helpers::send_error( '400', 'Invalid vendors data' ); } /** @var array $vendors */ // Extract only vendor IDs for comparison $vendor_ids = array_map( function ( $vendor ) { return isset( $vendor['id'] ) ? $vendor['id'] : null; }, $vendors ); // Remove null values $vendor_ids = array_filter( $vendor_ids ); // Sort for consistent comparison sort( $vendor_ids ); Options::update( 'cmp_vendors_snapshot', $vendor_ids ); wp_send_json( array( 'success' => true, 'data' => $vendor_ids, ) ); } /** * Save platform billing status * * @since 3.0.0 * @return void */ private function save_billing_status() { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { $error = new WP_Error( '403', 'Security check is not valid' ); wp_send_json_error( $error ); } $post_data = array( 'billingStatus' => isset( $_POST['billingStatus'] ) ? sanitize_text_field( wp_unslash( $_POST['billingStatus'] ) ) : null, ); if ( $post_data['billingStatus'] === null ) { $error = new WP_Error( '500', 'Billing status is null' ); wp_send_json_error( $error ); } $billing_status = $post_data['billingStatus']; Options::update( 'billing-status', $billing_status ); wp_send_json( array( 'success' => true, ) ); } /** * Sync data * * @since 2.6.8 * @return void */ private function sync_data() { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { $error = new WP_Error( '403', 'Security check is not valid' ); wp_send_json_error( $error ); } $post_data = array( 'feature' => isset( $_POST['feature'] ) ? sanitize_text_field( wp_unslash( $_POST['feature'] ) ) : null, ); if ( is_null( $post_data['feature'] ) ) { $error = new WP_Error( '500', 'Post empty' ); wp_send_json_error( $error ); } $response = null; if ( $post_data['feature'] === 'adstxt' ) { $this->cron->update_adstxt(); $response = AdsTxt::get_adstxt(); } if ( $post_data['feature'] === 'presized_div' ) { $this->cron->check_presized_div(); $response = Options::get( 'presizeddiv-selected', array() ); } if ( $post_data['feature'] === 'fastcmp' ) { $this->cron->get_fast_cmp(); $response = Options::get( 'fastcmp-vendors', array() ); } wp_send_json( array( $response, ) ); } /** * Security check for ajax request * * @since 2.4.0 * @return bool */ private function security_check() { if ( false === check_ajax_referer( $this->plugin_name . '-settings', 'nonce', true ) ) { $error = new WP_Error( '403', 'Security check is not valid' ); wp_send_json_error( $error ); } if ( ! current_user_can( 'manage_options' ) ) { $error = new WP_Error( '403', 'Security check is not valid' ); wp_send_json_error( $error ); } return true; } /** * Get sanitized post data for every requests * * @since 3.0.0 * @param array $keys * @return array */ private function get_sanitized_post_data( $keys ) { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { $error = new WP_Error( '403', 'Security check is not valid' ); wp_send_json_error( $error ); } $data = array(); foreach ( $keys as $key ) { /** @var string $key_string */ $key_string = $key; $data[$key_string] = isset( $_POST[$key_string] ) ? sanitize_text_field( wp_unslash( $_POST[$key_string] ) ) : null; } return $data; } /** * Validate api response on auth routes * * @since 3.0.0 * @param mixed $response * @param string $error_message * @param bool $return_error * @return array */ private function validate_api_response( $response, $error_message, $return_error = true ) { $is_error = is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['token'] ); if ( $is_error && $return_error === true ) { Helpers::send_error( '500', $error_message ); } elseif ( $is_error && $return_error === false ) { $response = array( 'token' => '' ); } if ( is_array( $response ) ) { return $response; } return array(); } /** * Detect whether an authentication response failed because the account email * has not been validated yet. * * Both invalid credentials and a not-yet-activated account answer 401, so the * only discriminator the API exposes is the message returned by the security * UserChecker (`user_checker.not_activated`). The known translations are matched * explicitly. Keep this list in sync with the krull-api `validators` catalog. * * @since 4.2.0 * @param mixed $response The raw API response. * @return bool */ private function is_email_not_validated_response( $response ) { if ( ! is_wp_error( $response ) || (string) $response->get_error_code() !== '401' ) { return false; } $error_data = $response->get_error_data(); if ( ! is_array( $error_data ) || ! isset( $error_data['message'] ) || ! is_string( $error_data['message'] ) ) { return false; } $not_activated_messages = array( 'Your account is not activated.', "Votre compte n'est pas activé.", ); return in_array( $error_data['message'], $not_activated_messages, true ); } /** * Get organization uuid if website exists * * @since 3.0.0 * @param string $token * @param string $organization_id * @return string */ private function get_organization_uuid( $token, $organization_id = '' ) { if ( empty( $organization_id ) ) { $organization_id = Options::get( 'organization-id' ); } if ( ! empty( $organization_id ) && is_string( $organization_id ) ) { $response = $this->api->get( 'api', 'organizations/' . $organization_id, array(), $token ); if ( is_wp_error( $response ) || ! is_array( $response ) || empty( $response ) ) { return ''; } if ( isset( $response['uuid'] ) && is_string( $response['uuid'] ) ) { return $response['uuid']; } } return ''; } /** * Fetches organizations * * @since 3.0.0 * @param string $token * @return array> */ private function get_organizations( $token ) { $response = $this->api->get( 'api', 'organizations', array(), $token, true ); if ( ! is_array( $response ) || empty( $response['hydra:member'] ) ) { return array(); } if ( is_array( $response['hydra:member'] ) ) { return array_map( function ( $org ) { // `ndaRequired` is flipped to false by Krull once the NDA is signed // (or when no NDA is required), so it doubles as a "signed" flag. return array( 'name' => $org['name'], 'value' => $org['@id'], 'ndaSigned' => isset( $org['ndaRequired'] ) ? ! $org['ndaRequired'] : false, ); }, $response['hydra:member'] ); } return array(); } /** * Get website enabled slots * * @since 3.1.0 * @return void */ private function get_slots() { $ad_format = new AdFormat(); $response = $ad_format->get_slots(); if ( is_wp_error( $response ) ) { Helpers::send_error( '500', $response->get_error_message() ); } if ( ! is_wp_error( $response ) ) { AdFormat::cache_enabled_slugs( $response ); } wp_send_json( array( 'success' => true, 'data' => $response, ) ); } /** * Update website slots * * @since 3.1.0 * @return void */ private function update_slots() { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { Helpers::send_error( '403', 'Security check is not valid' ); } /** @var string $token */ $token = Options::get( 'settings-analytics-token', '' ); $ad_format = new AdFormat(); $slots_raw = isset( $_POST['slots'] ) ? wp_unslash( $_POST['slots'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $slots = is_string( $slots_raw ) ? sanitize_textarea_field( $slots_raw ) : ''; if ( empty( $slots ) ) { Helpers::send_error( '400', 'Slots data is required' ); } $in_image_raw = isset( $_POST['inImage'] ) ? wp_unslash( $_POST['inImage'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $in_image = is_string( $in_image_raw ) ? sanitize_textarea_field( $in_image_raw ) : ''; $note_raw = isset( $_POST['note'] ) ? wp_unslash( $_POST['note'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $note = is_string( $note_raw ) ? sanitize_textarea_field( $note_raw ) : ''; $response = $ad_format->update_slots( $slots, $in_image, $note ); if ( is_wp_error( $response ) ) { Helpers::send_error( '500', $response->get_error_message() ); } if ( ! is_wp_error( $response ) ) { AdFormat::cache_enabled_slugs( $response ); } EventAnnotation::add_event( 'New Ad formats configuration', 'A new Ad formats configuration was published', 'config_changes' ); // Get updated health issues $health_check = new HealthCheck(); $health_issues = $health_check->get_issues(); wp_send_json( array( 'success' => true, 'data' => $response, 'message' => 'Slots updated successfully', 'health_issues' => $health_issues, 'ad_format_enabled_slugs' => Options::get( 'ad-format-enabled-slugs', array() ), ) ); } /** * Check whether the website has an active CMP detected by Krull. * * Krull centralizes the analytics-based detection and works for unsigned websites too, * whereas the analytics dashboard endpoint filters them out. * * @since 4.1.2 * @return void */ private function get_has_cmp() { /** @var string $token */ $token = Options::get( 'settings-analytics-token', '' ); $website_id = Options::get( 'website-id', '' ); if ( $website_id === '' ) { $website = Helpers::get_website_info(); if ( isset( $website['@id'] ) && is_string( $website['@id'] ) ) { $website_id = basename( $website['@id'] ); Options::update( 'website-id', $website_id ); } } if ( $website_id === '' ) { Helpers::send_error( '500', 'Unable to retrieve website ID' ); } $endpoint = 'websites/' . $website_id . '/has_cmp'; $response = $this->api->get( 'api', $endpoint, array(), $token ); if ( is_wp_error( $response ) ) { Helpers::send_error( '500', $response->get_error_message() ); } wp_send_json( array( 'success' => true, 'data' => $response, ) ); } /** * Get XPath check results from the last check * * @since 3.1.0 * @return void */ private function get_xpath_check_results() { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { Helpers::send_error( '403', 'Security check is not valid' ); } $ad_format = new AdFormat(); $results = $ad_format->get_xpath_check_results(); wp_send_json( array( 'success' => true, 'data' => $results, ) ); } /** * Save hosted ads.txt setting * * @since 3.3.1 * @return void */ private function save_hosted_adstxt() { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { Helpers::send_error( '403', 'Security check is not valid' ); } $post_data = array( 'enabled' => isset( $_POST['enabled'] ) ? sanitize_text_field( wp_unslash( $_POST['enabled'] ) ) : 'false', ); $enabled = $post_data['enabled'] === 'true' || $post_data['enabled'] === '1' || $post_data['enabled'] === 1; Options::update( 'hosted-adstxt-enabled', $enabled ); Logger::add_log( 'Hosted ads.txt ' . ( $enabled ? 'enabled' : 'disabled' ), 'admin/include/ajax', 'info' ); // Clear redirect error if disabling if ( ! $enabled ) { Options::delete( 'hosted-adstxt-redirect-error' ); } // If enabling, verify the redirect works correctly if ( $enabled ) { $uuid = Options::get( 'website-uuid', '' ); if ( empty( $uuid ) ) { wp_send_json( array( 'success' => false, 'error' => 'redirect_verification_failed', 'message' => 'Website UUID is not configured.', ) ); } $expected_url = 'https://gateway.actirise.com/services/adstxt/' . $uuid; $site_url = get_site_url(); $ads_txt_url = trailingslashit( $site_url ) . 'ads.txt'; $response = wp_remote_get( $ads_txt_url, array( 'timeout' => 10, 'redirection' => 0, 'sslverify' => false, 'user-agent' => 'Actirise-Redirect-Check/1.0', ) ); $status_code = is_wp_error( $response ) ? 0 : wp_remote_retrieve_response_code( $response ); $headers = is_wp_error( $response ) ? array() : wp_remote_retrieve_headers( $response ); $location = isset( $headers['location'] ) ? $headers['location'] : ''; $body = is_wp_error( $response ) ? '' : wp_remote_retrieve_body( $response ); // Case 1: No redirect at all (status 200) - third-party config or file served directly if ( $status_code === 200 ) { // Check if it's the Actirise file being served (should contain specific marker) $is_actirise_file = strpos( $body, 'actirise.com' ) !== false || strpos( $body, 'sparteo.com' ) !== false; if ( ! $is_actirise_file ) { wp_send_json( array( 'success' => false, 'error' => 'redirect_verification_failed', 'error_type' => 'third_party', 'flashbid_url' => $expected_url, 'message' => 'Third-party configuration detected blocking redirection.', ) ); } } // Case 2: Redirect to wrong URL if ( ( $status_code === 302 || $status_code === 301 ) && ! empty( $location ) ) { // Ignore WordPress trailing slash redirects (ads.txt -> ads.txt/) $is_trailing_slash_redirect = ( $location === trailingslashit( $ads_txt_url ) ); if ( ! $is_trailing_slash_redirect && $location !== $expected_url ) { $location_str = is_string( $location ) ? $location : ''; wp_send_json( array( 'success' => false, 'error' => 'redirect_verification_failed', 'error_type' => 'wrong_url', 'flashbid_url' => $expected_url, 'message' => sprintf( 'Redirect points to wrong URL: %s instead of %s', $location_str, $expected_url ), ) ); } } // Case 3: Other errors (timeout, no redirect, etc.) if ( $status_code !== 302 && $status_code !== 200 ) { wp_send_json( array( 'success' => false, 'error' => 'redirect_verification_failed', 'error_type' => 'manual_required', 'flashbid_url' => $expected_url, 'message' => 'Server-side rule or security layer preventing automatic redirection.', ) ); } Logger::add_log( 'Hosted ads.txt redirect verified successfully', 'admin/include/ajax', 'info' ); // Clear any previous redirect error Options::delete( 'hosted-adstxt-redirect-error' ); // Ensure adstxt-active is false so the right option defaults to manual Options::update( 'adstxt-active', false ); } wp_send_json( array( 'success' => true, ) ); } /** * Check if ads.txt file exists and can be renamed for hosted mode * * @since 3.3.5 * @return void */ private function check_adstxt_file_for_hosted() { $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'actirise-settings' ) ) { Helpers::send_error( '403', 'Security check is not valid' ); return; } $path_ads_txt = ABSPATH . 'ads.txt'; $wp_fs = Helpers::get_wp_fs(); if ( is_wp_error( $wp_fs ) ) { wp_send_json( array( 'success' => false, 'file_exists' => false, 'error' => 'filesystem_error', ) ); } $file_exists = $wp_fs->exists( $path_ads_txt ); // Check if a redirect exists (not ours) $has_third_party_redirect = false; $uuid = Options::get( 'website-uuid', '' ); $expected_url = 'https://gateway.actirise.com/services/adstxt/' . $uuid; $site_url = get_site_url(); $ads_txt_url = trailingslashit( $site_url ) . 'ads.txt'; $response = wp_remote_get( $ads_txt_url, array( 'timeout' => 10, 'redirection' => 0, 'sslverify' => false, ) ); if ( ! is_wp_error( $response ) ) { $status_code = wp_remote_retrieve_response_code( $response ); if ( $status_code === 302 || $status_code === 301 ) { $redirect_location = wp_remote_retrieve_header( $response, 'location' ); // Ignore WordPress trailing slash redirects (ads.txt -> ads.txt/) $is_trailing_slash_redirect = ( $redirect_location === trailingslashit( $ads_txt_url ) ); // If there's a redirect but not to our URL and not a trailing slash redirect, it's a third-party redirect if ( ! empty( $redirect_location ) && $redirect_location !== $expected_url && ! $is_trailing_slash_redirect ) { $has_third_party_redirect = true; } } } // Persist or clear redirect error in DB so health check reflects current state if ( ! $file_exists && ! $has_third_party_redirect ) { Options::update( 'hosted-adstxt-redirect-error', false ); } elseif ( $file_exists ) { Options::update( 'hosted-adstxt-redirect-error', array( 'error_type' => 'file_exists' ) ); } elseif ( $has_third_party_redirect ) { Options::update( 'hosted-adstxt-redirect-error', array( 'error_type' => 'third_party' ) ); } wp_send_json( array( 'success' => true, 'file_exists' => $file_exists, 'has_third_party_redirect' => $has_third_party_redirect, ) ); } /** * Get health check issues * * @since 4.0.0 * @return void */ private function get_health_issues() { $health_check = new HealthCheck(); $health_issues = $health_check->get_issues(); wp_send_json( array( 'success' => true, 'health_issues' => $health_issues, ) ); } /** * Force XPath check and return updated health issues * * @since 4.0.0 * @return void */ private function force_xpath_check() { $ad_format = new AdFormat(); $ad_format->validate_xpath_and_notify(); // Get updated health issues $health_check = new HealthCheck(); $health_issues = $health_check->get_issues(); wp_send_json( array( 'success' => true, 'health_issues' => $health_issues, ) ); } /** * Save GitBook AI assistant consent for this website. * * @since 4.0.0 * @return void */ private function save_gitbook_consent() { Options::update( 'gitbook-consent', true ); wp_send_json( array( 'success' => true, ) ); } /** * Toggle an optimization checklist manual step. * * @since 4.1.0 * @return void */ private function toggle_optimization_checklist() { $allowed_steps = array( 'cache_exclusion', 'editorial_best_practices' ); $step = isset( $_POST['step'] ) ? sanitize_text_field( wp_unslash( $_POST['step'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified in dispatch() $value = isset( $_POST['value'] ) ? filter_var( wp_unslash( $_POST['value'] ), FILTER_VALIDATE_BOOLEAN ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified in dispatch() if ( ! in_array( $step, $allowed_steps, true ) ) { Helpers::send_error( '400', 'Invalid step' ); return; } $manual_steps = Options::get( 'optimization-checklist-manual', array() ); if ( ! is_array( $manual_steps ) ) { $manual_steps = array(); } $manual_steps[ $step ] = $value; Options::update( 'optimization-checklist-manual', $manual_steps ); $health_check = new HealthCheck(); $health_issues = $health_check->get_issues(); wp_send_json( array( 'success' => true, 'health_issues' => $health_issues, ) ); } /** * Proxy request for event annotations list to avoid CORS issues. * * @since 4.1.0 * @return void */ private function get_event_annotations() { /** @var string $token */ $token = Options::get( 'settings-analytics-token', '' ); if ( empty( $token ) ) { Helpers::send_error( '401', 'Missing API token' ); return; } $website_iri = '/v2.0/websites/' . Options::get( 'website-id' ); $body = array( 'timezone' => 'Europe/Paris', 'startDate' => '2026-01-20', 'scope' => array( 'public' ), 'page' => 1, 'itemsPerPage' => 5, 'sort' => 'desc', 'websites' => array( $website_iri ), ); $response = $this->api->post( 'api', 'event_annotations/list', $body, $token, true ); if ( is_wp_error( $response ) ) { Helpers::send_error( '500', 'Error fetching event annotations' ); return; } wp_send_json( array( 'success' => true, 'data' => $response, ) ); } }