'array', 'sanitize_callback' => array( $this, 'sanitize' ), 'default' => Aiso_Analytics::default_options(), ) ); } /** * Sanitize the options array on save. * * @param array $input Raw input. * @return array */ public function sanitize( $input ) { $current = Aiso_Analytics::options(); $input = is_array( $input ) ? $input : array(); $out = array( 'api_key' => isset( $input['api_key'] ) ? sanitize_text_field( $input['api_key'] ) : $current['api_key'], 'sync_enabled' => ! empty( $input['sync_enabled'] ) ? 1 : 0, 'track_all_bots' => ! empty( $input['track_all_bots'] ) ? 1 : 0, 'anonymize_ip' => ! empty( $input['anonymize_ip'] ) ? 1 : 0, 'retention_days' => isset( $input['retention_days'] ) ? max( 0, min( 3650, absint( $input['retention_days'] ) ) ) : $current['retention_days'], 'endpoint_base' => isset( $input['endpoint_base'] ) ? esc_url_raw( trim( $input['endpoint_base'] ) ) : $current['endpoint_base'], 'bypass_cache_for_ai_bots' => ! empty( $input['bypass_cache_for_ai_bots'] ) ? 1 : 0, 'remove_data_on_uninstall' => ! empty( $input['remove_data_on_uninstall'] ) ? 1 : 0, ); return $out; } /** * Connect: provision a new key from an email, or accept a pasted key, then * enable syncing. */ public function handle_connect() { $this->guard( 'aiso_connect' ); $options = Aiso_Analytics::options(); // Nonce is verified in guard() above via check_admin_referer( 'aiso_connect' ). $email = isset( $_POST['aiso_email'] ) ? sanitize_email( wp_unslash( $_POST['aiso_email'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( '' !== $email ) { $res = Aiso_Analytics::instance()->sync->provision_key( $email ); if ( $res['success'] ) { $options['api_key'] = $res['api_key']; $options['sync_enabled'] = 1; update_option( self::OPTION, $options ); $this->redirect_with_notice( 'success', __( 'Connected to Aiso. Your API key was created and syncing is on.', 'aiso-ai-crawler-analytics' ) ); } $this->redirect_with_notice( 'error', $res['message'] ); } $this->redirect_with_notice( 'error', __( 'Enter an email to generate a key, or paste an existing key in Settings.', 'aiso-ai-crawler-analytics' ) ); } /** * Test the saved API key. */ public function handle_test() { $this->guard( 'aiso_test' ); $key = (string) Aiso_Analytics::option( 'api_key', '' ); $res = Aiso_Analytics::instance()->sync->test_connection( $key ); if ( $res['valid'] ) { $this->redirect_with_notice( 'success', $res['domain'] /* translators: %s: verified domain. */ ? sprintf( __( 'Connection OK — key is valid for %s.', 'aiso-ai-crawler-analytics' ), $res['domain'] ) : __( 'Connection OK — your key is valid.', 'aiso-ai-crawler-analytics' ) ); } $this->redirect_with_notice( 'error', $res['message'] ); } /** * Push all pending rows to MongoDB right now. */ public function handle_sync_now() { $this->guard( 'aiso_sync_now' ); $res = Aiso_Analytics::instance()->sync->run_sync( 20 ); if ( '' !== $res['error'] ) { $this->redirect_with_notice( 'error', $res['error'] ); } $this->redirect_with_notice( 'success', sprintf( /* translators: %d: number of hits sent. */ _n( 'Synced %d hit to Aiso.', 'Synced %d hits to Aiso.', $res['sent'], 'aiso-ai-crawler-analytics' ), $res['sent'] ) ); } /** * Stream the current range as a CSV download. */ public function handle_export() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'aiso-ai-crawler-analytics' ) ); } check_admin_referer( 'aiso_export' ); $range = isset( $_GET['range'] ) ? sanitize_key( wp_unslash( $_GET['range'] ) ) : '7d'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce checked above. $rows = Aiso_Store::recent( $range, 5000 ); nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="aiso-ai-crawlers-' . gmdate( 'Y-m-d' ) . '.csv"' ); $out = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- writing the CSV download to the output stream. fputcsv( $out, array( 'time_utc', 'crawler', 'company', 'platform', 'purpose', 'path', 'country', 'verified', 'status', 'user_agent' ) ); foreach ( $rows as $r ) { fputcsv( $out, array( isset( $r['hit_time'] ) ? $r['hit_time'] : '', isset( $r['bot_name'] ) ? $r['bot_name'] : '', isset( $r['company'] ) ? $r['company'] : '', isset( $r['platform'] ) ? $r['platform'] : '', isset( $r['function_tag'] ) ? $r['function_tag'] : '', isset( $r['path'] ) ? $r['path'] : '', isset( $r['country'] ) ? $r['country'] : '', ( isset( $r['verified'] ) && null !== $r['verified'] ) ? ( $r['verified'] ? 'yes' : 'no' ) : '', isset( $r['status'] ) ? $r['status'] : '', isset( $r['user_agent'] ) ? $r['user_agent'] : '', ) ); } fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- php://output stream. exit; } /** * Capability + nonce guard for admin-post handlers. * * @param string $action Nonce action. */ private function guard( $action ) { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'aiso-ai-crawler-analytics' ) ); } check_admin_referer( $action ); } /** * Redirect back to settings with a transient notice. * * @param string $type success|error. * @param string $message Message text. */ private function redirect_with_notice( $type, $message ) { set_transient( self::NOTICE_FLAG . '_' . get_current_user_id(), array( 'type' => ( 'success' === $type ) ? 'success' : 'error', 'message' => $message, ), 60 ); wp_safe_redirect( admin_url( 'admin.php?page=aiso-settings' ) ); exit; } /** * Show (and clear) any pending notice on our admin pages. */ public function render_notices() { $screen = get_current_screen(); if ( ! $screen || false === strpos( (string) $screen->id, 'aiso' ) ) { return; } $key = self::NOTICE_FLAG . '_' . get_current_user_id(); $notice = get_transient( $key ); if ( ! $notice || empty( $notice['message'] ) ) { return; } delete_transient( $key ); printf( '

%2$s

', esc_attr( 'success' === $notice['type'] ? 'success' : 'error' ), esc_html( $notice['message'] ) ); } }