settings = $settings; $this->tdmrep = $tdmrep; $this->signals = $signals; $this->meta = $meta; $this->terms = $terms; $this->history = $history; $this->badge = $badge; } /** * Registers hooks. * * @return void */ public function register(): void { add_action( 'admin_post_aiblackout_save_declarations', array( $this, 'handle_save' ) ); add_action( 'admin_post_aiblackout_save_retention', array( $this, 'handle_retention' ) ); } /** * Returns the URL of the merged screen these sections live on. * * @param array $args Optional query arguments to add. * @return string Admin URL. */ private function get_screen_url( array $args = array() ): string { $url = MainScreen::url(); $url = empty( $args ) ? $url : add_query_arg( $args, $url ); return $url . '#declarations'; } /** * Saves the declaration settings and records what is now published. * * @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 ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- check_admin_referer() ran above; every value is sanitized or validated against a fixed list below. $posted = isset( $_POST['aiblackout'] ) && is_array( $_POST['aiblackout'] ) ? wp_unslash( $_POST['aiblackout'] ) : array(); $declarations = $this->settings->get( 'declarations', array() ); $declarations = is_array( $declarations ) ? $declarations : array(); foreach ( array( 'tdm_reservation', 'content_signals', 'noai', 'noimageai' ) as $switch ) { $declarations[ $switch ] = ! empty( $posted[ $switch ] ); } $declarations['tdm_policy_url'] = isset( $posted['tdm_policy_url'] ) && is_string( $posted['tdm_policy_url'] ) ? esc_url_raw( trim( $posted['tdm_policy_url'] ) ) : ''; $refused = false; foreach ( array( 'signal_search', 'signal_ai_input', 'signal_ai_train' ) as $key ) { $value = isset( $posted[ $key ] ) ? sanitize_key( $posted[ $key ] ) : Settings::SIGNAL_UNSET; if ( ! in_array( $value, array( Settings::SIGNAL_YES, Settings::SIGNAL_NO, Settings::SIGNAL_UNSET ), true ) ) { $value = Settings::SIGNAL_UNSET; } if ( 'signal_search' === $key && Settings::SIGNAL_NO === $value && empty( $posted['ack_search_signal'] ) ) { $refused = true; $value = isset( $declarations[ $key ] ) ? (string) $declarations[ $key ] : Settings::SIGNAL_YES; } $declarations[ $key ] = $value; } $this->settings->set( 'declarations', $declarations ); $this->settings->set( 'badge', array( 'enabled' => ! empty( $posted['badge']['enabled'] ), ) ); $this->tdmrep->record(); $this->signals->record(); $this->meta->record(); wp_safe_redirect( $this->get_screen_url( $refused ? array( 'aiblackout_decl_refused' => '1' ) : array( 'aiblackout_decl_saved' => '1' ) ) ); exit; } /** * Saves the choice of what survives deleting the plugin. * * @return void */ public function handle_retention(): 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_RETENTION ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- check_admin_referer() ran above; the value is only ever read as present or absent. $this->settings->set( 'preserve_settings', isset( $_POST['aiblackout_preserve_settings'] ) ); wp_safe_redirect( $this->get_screen_url( array( 'aiblackout_retention_saved' => '1' ) ) ); exit; } /** * Renders every declaration section on the screen. * * @return void */ public function render_sections(): void { if ( ! current_user_can( self::CAPABILITY ) ) { return; } $this->render_notices(); printf( '
%s
', esc_html__( 'Blocking a crawler stops it reading your site. A declaration states, in a form courts and companies both recognise, that you have not given permission for your work to be used to train AI. It costs nothing, cannot break your site, and is the part that still counts if someone takes your content anyway.', 'ai-blackout' ) ); echo ''; $this->render_card( __( 'A clause for your terms page', 'ai-blackout' ), '', array( $this, 'render_terms' ) ); $this->render_card( __( 'What you have declared, and since when', 'ai-blackout' ), $this->history_status(), array( $this, 'render_history' ) ); $kept = (bool) $this->settings->get( 'preserve_settings', false ); $this->render_card( __( 'If you delete AI Blackout', 'ai-blackout' ), $kept ? __( 'Kept', 'ai-blackout' ) : __( 'Not kept', 'ai-blackout' ), array( $this, 'render_retention' ) ); } /** * Summarises the three Content Signals for the card header. * * @return string Compact status text. */ private function signals_status(): string { $counts = array( Settings::SIGNAL_YES => 0, Settings::SIGNAL_NO => 0, Settings::SIGNAL_UNSET => 0, ); foreach ( array( 'signal_search', 'signal_ai_input', 'signal_ai_train' ) as $key ) { $value = (string) $this->settings->get_declaration( $key, Settings::SIGNAL_UNSET ); if ( isset( $counts[ $value ] ) ) { ++$counts[ $value ]; } } $templates = array( /* translators: %s: Number of uses. */ Settings::SIGNAL_YES => _n( '%s allowed', '%s allowed', $counts[ Settings::SIGNAL_YES ], 'ai-blackout' ), /* translators: %s: Number of uses. */ Settings::SIGNAL_NO => _n( '%s not allowed', '%s not allowed', $counts[ Settings::SIGNAL_NO ], 'ai-blackout' ), /* translators: %s: Number of uses. */ Settings::SIGNAL_UNSET => _n( '%s unset', '%s unset', $counts[ Settings::SIGNAL_UNSET ], 'ai-blackout' ), ); $parts = array(); foreach ( $counts as $signal => $count ) { if ( $count > 0 ) { $parts[] = sprintf( $templates[ $signal ], number_format_i18n( $count ) ); } } return implode( ' · ', $parts ); } /** * Summarises the two page-level notices for the card header. * * @return string Compact status text. */ private function meta_status(): string { $on = 0; foreach ( array( 'noai', 'noimageai' ) as $key ) { if ( $this->settings->get_declaration( $key, false ) ) { ++$on; } } if ( 0 === $on ) { return __( 'Off', 'ai-blackout' ); } return sprintf( /* translators: %s: Number of notices switched on, out of 2. */ __( '%s of 2 on', 'ai-blackout' ), number_format_i18n( $on ) ); } /** * Summarises the footer badge for the card header. * * @return string Compact status text. */ private function badge_status(): string { $badge = $this->settings->get( 'badge', array() ); $badge = is_array( $badge ) ? $badge : array(); if ( empty( $badge['enabled'] ) ) { return __( 'Off', 'ai-blackout' ); } return $this->badge->is_earned() ? __( 'Visible', 'ai-blackout' ) : __( 'Not shown yet', 'ai-blackout' ); } /** * Summarises the declaration history for the card header. * * @return string Compact status text. */ private function history_status(): string { $since = $this->history->first_published( Tdmrep::TYPE ); if ( null === $since ) { return __( 'Nothing recorded yet', 'ai-blackout' ); } return sprintf( /* translators: %s: Date the reservation was first published. */ __( 'Since %s', 'ai-blackout' ), wp_date( get_option( 'date_format' ), $since ) ); } /** * Prints the choice of what survives deleting the plugin. * * @return void */ private function render_retention(): void { printf( '%s
', esc_html__( 'Deleting the plugin always puts your files back and always removes the activity log. That part is not a choice: rules left in a file that nothing is managing any more are worse than no rules at all.', 'ai-blackout' ) ); printf( '%s
', esc_html__( 'What you can keep is every setting on this page and every other page of AI Blackout -- which crawlers you block, how enforcement is set up, your declarations -- plus the record above, so that reinstalling picks up where you left off instead of starting from nothing.', 'ai-blackout' ) ); echo ''; } /** * Prints the outcome notices. * * @return void */ private function render_notices(): void { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Display-only flags; the save itself is nonce-checked in handle_save(). if ( isset( $_GET['aiblackout_decl_saved'] ) ) { printf( '%s
%s
%1$s %2$s
%s
', esc_html__( 'European copyright law lets anyone mine published work for text and data unless the rights holder has reserved that right in a machine-readable way. This publishes exactly that reservation, at a standard address AI companies check, so "we did not know" stops being available to them.', 'ai-blackout' ) ); echo '| %s | ', esc_html__( 'Reserve my rights', 'ai-blackout' ) ); echo '';
printf(
'',
checked( (bool) $this->settings->get_declaration( 'tdm_reservation', false ), true, false ),
esc_html__( 'On', 'ai-blackout' )
);
printf(
' %s ', esc_html__( 'Publishes a reservation under Article 4(3) of the EU copyright directive, as a file at /.well-known/tdmrep.json, as a tag on every page, and as an HTTP header.', 'ai-blackout' ) ); echo ' |
|---|---|
| ', esc_html__( 'Link to your terms', 'ai-blackout' ) ); echo ' | ';
printf(
'',
esc_attr( $this->settings->get_declaration( 'tdm_policy_url', '' ) )
);
printf(
' %s ', esc_html__( 'Optional. The address of the page holding your terms. Publishing one makes the reservation harder to argue with, because it points at the conditions in full. The clause further down this page is written to go there.', 'ai-blackout' ) ); echo ' |
%s
%s
%s
%s
', esc_html__( 'Blocking a crawler is all or nothing. This is finer: it separates being findable in search from being fed to an AI, and states your answer for each one in your robots.txt file, in the wording the Content Signals policy defines. Leaving one unanswered says nothing about it either way, which is not the same as saying no.', 'ai-blackout' ) ); echo '| %s | ', esc_html__( 'Publish these signals', 'ai-blackout' ) ); echo ''; printf( '', checked( (bool) $this->settings->get_declaration( 'content_signals', false ), true, false ), esc_html__( 'On', 'ai-blackout' ) ); echo ' |
|---|
%s %s
', esc_html__( 'Right now:', 'ai-blackout' ), esc_html( $this->signals->describe() ) ); $block = $this->signals->build_block(); if ( '' === $block ) { return; } printf( '%s
%s
', esc_html( $description ) ); if ( $search_impact && Settings::SIGNAL_NO !== $current ) { printf( '', esc_html__( 'Setting this to "Not allowed" asks search engines to stop indexing your site, which will cost you traffic. Tick this to confirm you mean it. Without it the change is refused.', 'ai-blackout' ) ); } echo '%s
', esc_html__( 'Two older directives that some tools and stock libraries check, added to every page. They are not a standard and nobody is obliged to honour them. They cost nothing, and they add one more place where your refusal is on the record.', 'ai-blackout' ) ); echo '| %s | ', esc_html__( 'Refuse AI use of my text', 'ai-blackout' ) ); echo ''; printf( '', checked( (bool) $this->settings->get_declaration( 'noai', false ), true, false ), esc_html__( 'On', 'ai-blackout' ) ); echo ' |
|---|---|
| %s | ', esc_html__( 'Refuse AI use of my images', 'ai-blackout' ) ); echo ''; printf( '', checked( (bool) $this->settings->get_declaration( 'noimageai', false ), true, false ), esc_html__( 'On', 'ai-blackout' ) ); echo ' |
%s
%s
', esc_html__( 'An optional mark for your footer. It is off unless you switch it on, and it never appears unless you are actually declaring or blocking something. A badge on a site doing neither would be the kind of claim this plugin exists to argue against.', 'ai-blackout' ) ); echo '| %s | ', esc_html__( 'Show the badge', 'ai-blackout' ) ); echo '';
printf(
'',
checked( ! empty( $badge['enabled'] ), true, false ),
esc_html__( 'On', 'ai-blackout' )
);
printf( ' %s ', esc_html__( 'Added to the footer of your site, styled to inherit your theme\'s colours. Nothing is loaded from anywhere else. It links to the plugin, marked so it passes no ranking value either way.', 'ai-blackout' ) ); echo ' |
|---|
%s
%s
%s
', esc_html__( 'How it looks:', 'ai-blackout' ) ); echo wp_kses_post( $this->badge->build() ); echo '%s
', esc_html__( 'Everything above is machine-readable, which is what an AI company\'s crawler reads. This is the same position in words, for the page a person reads. It is written from your settings, so the two agree.', 'ai-blackout' ) ); printf( '', esc_textarea( $this->terms->build() ) ); printf( '%s
', esc_html( $this->terms->get_disclaimer() ) ); } /** * Prints the record of what has been declared and from when. * * @return void */ private function render_history(): void { $entries = $this->history->all(); if ( empty( $entries ) ) { printf( '%s
%s
%s
', esc_html__( 'Newest first. Times are shown in your site\'s timezone; each entry also stores the UTC time it was recorded.', 'ai-blackout' ) ); echo '| %s | ', esc_html__( 'When', 'ai-blackout' ) ); printf( '%s | ', esc_html__( 'Declaration', 'ai-blackout' ) ); printf( '%s | ', esc_html__( 'What it said', 'ai-blackout' ) ); echo '
|---|---|---|
| %s | ', esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), isset( $entry['recorded_at'] ) ? (int) $entry['recorded_at'] : 0 ) ) ); printf( '%s | ', esc_html( $this->type_label( isset( $entry['type'] ) ? (string) $entry['type'] : '' ) ) ); printf( '%s | ', esc_html( isset( $entry['summary'] ) ? (string) $entry['summary'] : '' ) ); echo '
%s
', esc_html( sprintf( /* translators: %s: Date the reservation was first published. */ __( 'You have been reserving your rights since %s.', 'ai-blackout' ), wp_date( get_option( 'date_format' ), $since ) ) ) ); if ( null !== $changed && $changed !== $since ) { printf( '%s
', esc_html( sprintf( /* translators: %s: Date the declaration last changed. */ __( 'Last changed %s.', 'ai-blackout' ), wp_date( get_option( 'date_format' ), $changed ) ) ) ); } } /** * Says plainly what this record is worth, and points at what is worth * more. * * @return void */ private function render_history_proof(): void { printf( '%s
', esc_html__( 'This is your own record, kept on your own site. It is here so you can answer "since when?" — but because it lives in your database, it is not something you could hold up as proof against anyone else.', 'ai-blackout' ) ); printf( '%s
', esc_html__( 'For a dated copy nobody can say you wrote yourself, ask the Internet Archive to save your robots.txt. That timestamps the published file independently of you, which is the copy worth having if it ever comes up — and worth repeating whenever you change something here.', 'ai-blackout' ) ); printf( '', esc_url( 'https://web.archive.org/save/' . home_url( '/robots.txt' ) ), esc_html__( 'Archive my robots.txt now', 'ai-blackout' ), esc_html__( '(opens in a new tab)', 'ai-blackout' ) ); } /** * Returns the plain-language name of a declaration type. * * @param string $type Declaration type. * @return string Human-readable label. */ private function type_label( string $type ): string { switch ( $type ) { case Tdmrep::TYPE: return __( 'Reservation of rights', 'ai-blackout' ); case ContentSignals::TYPE: return __( 'Content signals', 'ai-blackout' ); case MetaTags::TYPE: return __( 'Page-level notices', 'ai-blackout' ); default: return $type; } } }