blackout = $blackout; $this->settings = $settings; $this->providers = $providers; $this->resolver = $resolver; } /** * Registers hooks. * * @return void */ public function register(): void { add_action( 'admin_menu', array( $this, 'add_menu' ), 13 ); add_action( 'admin_post_aiblackout_save_blackout', array( $this, 'handle_save' ) ); add_action( 'admin_post_aiblackout_clear_blackout_log', array( $this, 'handle_clear' ) ); } /** * Adds the submenu item. * * @return void */ public function add_menu(): void { add_submenu_page( MainScreen::SLUG, __( 'AI inside WordPress', 'ai-blackout' ), __( 'AI inside WordPress', 'ai-blackout' ), self::CAPABILITY, self::SLUG, array( $this, 'render' ) ); } /** * Returns the URL of this screen. * * @param array $args Optional query arguments to add. * @return string Admin URL. */ private function get_screen_url( array $args = array() ): string { $url = admin_url( 'admin.php?page=' . self::SLUG ); return empty( $args ) ? $url : add_query_arg( $args, $url ); } /** * Saves the settings. * * @return void */ public function handle_save(): void { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to change these settings.', 'ai-blackout' ), '', array( 'response' => 403 ) ); } check_admin_referer( self::NONCE_SAVE ); $blackout = $this->settings->get( 'blackout', array() ); $blackout = is_array( $blackout ) ? $blackout : array(); foreach ( array( Settings::BLACKOUT_CORE_AI, Settings::BLACKOUT_ABILITIES, Settings::BLACKOUT_CONNECTORS_UI, Settings::BLACKOUT_OUTBOUND ) as $switch ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- check_admin_referer() ran above. $blackout[ $switch ] = isset( $_POST['aiblackout'][ $switch ] ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- check_admin_referer() ran above; the value is validated against a fixed list below. $mode = isset( $_POST['aiblackout']['outbound_mode'] ) ? sanitize_key( wp_unslash( $_POST['aiblackout']['outbound_mode'] ) ) : Settings::OUTBOUND_MONITOR; if ( ! in_array( $mode, array( Settings::OUTBOUND_OFF, Settings::OUTBOUND_MONITOR, Settings::OUTBOUND_BLOCK ), true ) ) { $mode = Settings::OUTBOUND_MONITOR; } $blackout['outbound_mode'] = $mode; $integrations = array(); foreach ( array_keys( $this->blackout->get_integrations() ) as $id ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- check_admin_referer() ran above. $integrations[ $id ] = isset( $_POST['aiblackout']['integrations'][ $id ] ); } $blackout['integrations'] = $integrations; // Only ids present in the bundled list are ever stored. $submitted = array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- check_admin_referer() ran above. if ( isset( $_POST['aiblackout']['providers'] ) && is_array( $_POST['aiblackout']['providers'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- check_admin_referer() ran above; each value is sanitized in the loop. foreach ( wp_unslash( $_POST['aiblackout']['providers'] ) as $id ) { $submitted[] = sanitize_key( $id ); } } $providers = array(); foreach ( $this->providers->get_providers() as $provider ) { if ( in_array( $provider['id'], $submitted, true ) ) { $providers[] = $provider['id']; } } $blackout['providers'] = $providers; $this->settings->set( 'blackout', $blackout ); wp_safe_redirect( $this->get_screen_url( array( 'aiblackout_saved' => '1' ) ) ); exit; } /** * Empties the call log. * * @return void */ public function handle_clear(): void { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to clear this log.', 'ai-blackout' ), '', array( 'response' => 403 ) ); } check_admin_referer( self::NONCE_CLEAR ); $this->blackout->log()->clear(); wp_safe_redirect( $this->get_screen_url( array( 'aiblackout_cleared' => '1' ) ) ); exit; } /** * Renders the screen. * * @return void */ public function render(): void { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'ai-blackout' ), '', array( 'response' => 403 ) ); } echo '
%s
', esc_html__( 'WordPress 7.0 ships with AI built in, and many plugins add their own. This page switches that off and shows you what your site has been sending to AI companies.', 'ai-blackout' ) ); $this->render_card( __( 'What your site has been sending to AI companies', 'ai-blackout' ), $this->activity_status(), array( $this, 'render_activity' ) ); echo ''; echo '%s
%s
%s %s
%s
%s
', esc_html__( 'Nothing recorded yet.', 'ai-blackout' ) ); if ( Settings::OUTBOUND_OFF === $this->blackout->firewall()->get_mode() ) { printf( '%s
', esc_html__( 'Watching outgoing requests is switched off, so nothing is being recorded. Turn it on below to find out which of your plugins talk to AI companies.', 'ai-blackout' ) ); } else { printf( '%s
', esc_html__( 'Nothing on your site has tried to reach an AI company since this was switched on. Attempts appear here as they happen.', 'ai-blackout' ) ); } echo '| %s | ', esc_html__( 'What made the request', 'ai-blackout' ) ); printf( '%s | ', esc_html__( 'Where it was going', 'ai-blackout' ) ); printf( '%s | ', esc_html__( 'Times', 'ai-blackout' ) ); printf( '%s | ', esc_html__( 'Result', 'ai-blackout' ) ); printf( '%s | ', esc_html__( 'Last seen', 'ai-blackout' ) ); echo '
|---|---|---|---|---|
| %s | ', esc_html( $this->resolver->get_display_name( (string) $row['source'] ) ) ); printf( '%s | ', esc_html( (string) $row['host'] ) );
printf( '%s | ', esc_html( number_format_i18n( (int) $row['hits'] ) ) ); printf( '%s | ', esc_attr( $blocked ? 'blocked' : 'watched' ), esc_html( $blocked ? __( 'Blocked', 'ai-blackout' ) : __( 'Allowed through', 'ai-blackout' ) ) ); printf( '%s | ', esc_html( mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (string) $row['last_seen'] ) ) ); echo '
%s
', esc_html__( 'Counts are grouped by day. Records older than 30 days are removed automatically.', 'ai-blackout' ) ); echo ''; } /** * Prints the WordPress core switches. * * @return void */ private function render_core_switches(): void { $core_ai = $this->blackout->core_ai(); if ( ! $core_ai->has_core_ai() ) { printf( '%s
', esc_html__( 'This version of WordPress does not include AI features, so there is nothing here to switch off. These settings will start applying if you update to a version that does.', 'ai-blackout' ) ); } echo '%s
', esc_html( sprintf( /* translators: %s: A comma-separated list of ability names. */ _n( 'One AI action was removed on this page load: %s.', 'These AI actions were removed on this page load: %s.', count( $removed ), 'ai-blackout' ), implode( ', ', $removed ) ) ) ); } $keys = $core_ai->find_environment_keys(); if ( ! empty( $keys ) ) { printf( '%s %s
%s
%s
', esc_html__( 'This watches every request your site makes through WordPress and can refuse the ones going to AI companies. It covers WordPress itself and nearly every plugin. A plugin that makes its own network connection instead of using WordPress would not be stopped, and this page will never claim otherwise.', 'ai-blackout' ) ); echo '| %s | ', esc_html__( 'What to do with them', 'ai-blackout' ) ); echo '
|---|
%s
%s
', esc_html__( 'These are switched off here by default. Blocking one of these stops that plugin\'s AI at the network level. That is a blunter tool than the per-plugin switches above, so use those first.', 'ai-blackout' ) ); echo ''; } /** * Prints one provider checkbox. * * @param array $provider Provider array. * @param string[] $enabled Currently enabled provider ids. * @return void */ private function render_provider_checkbox( array $provider, array $enabled ): void { $hosts = array_merge( $provider['hosts'], $provider['host_suffixes'] ); printf( '', esc_attr( $provider['id'] ), checked( in_array( $provider['id'], $enabled, true ), true, false ), esc_html( $provider['label'] ), esc_html( implode( ', ', $hosts ) ), '' !== $provider['warning'] ? '' . esc_html( $provider['warning'] ) . '' : '' ); } /** * Prints one blackout switch row. * * @param string $key One of the BLACKOUT_* constants. * @param string $label Row label. * @param string $description What the switch does. * @return void */ private function render_switch( string $key, string $label, string $description ): void { $this->render_checkbox( 'aiblackout[' . $key . ']', $this->settings->is_blackout_enabled( $key ), $label, $description ); } /** * Prints one checkbox row. * * @param string $name Field name. * @param bool $checked Whether it is on. * @param string $label Row label. * @param string $description What it does. * @param string $note Optional extra warning. * @return void */ private function render_checkbox( string $name, bool $checked, string $label, string $description, string $note = '' ): void { echo '%s
', esc_html( $description ) ); if ( '' !== $note ) { printf( '%s
', esc_html( $note ) ); } echo '