' . esc_html__( 'Not flagged (no disclosure)', 'aim-transparency' ) . ''; foreach ( AICL_Plugin::SOURCE_TYPES as $token => $info ) { $options .= sprintf( '', esc_attr( $token ), selected( $current, $token, false ), esc_html( AICL_Plugin::admin_label_for( $token ) ) ); } return $content . ''; } /** * Returns the post types offered the featured-image AI source control. * * Everything that supports a featured image, by default. The block panel registers * globally, so without this it offered to mark a featured image on post types that * cannot have one. Filterable for the same reason `aicl_text_post_types` is: which * types a site actually discloses for is a judgement, not a capability. * * @since 2.1.3 * * @return string[] Post type names. */ protected static function featured_post_types() { /** * Filters the post types offered the featured-image AI source control. * * @since 2.1.3 * * @param string[] $post_types Post type names. */ $types = apply_filters( 'aicl_featured_post_types', array_values( get_post_types_by_support( 'thumbnail' ) ) ); return array_values( (array) $types ); } /** * Builds the shared localization data passed to the featured-image editor scripts. * * The strings travel with the options rather than through wp_set_script_translations, so they * resolve from the .mo that is already loaded and neither script needs a JED file of its own. * * @since 1.0.0 * * @return array{flag:string,nonce:string,options:array,i18n:array} Endpoint URL, REST nonce, source-type option list, and the strings both editor scripts display. */ protected function featured_data() { $options = array( array( 'value' => '', 'label' => __( 'Not flagged (no disclosure)', 'aim-transparency' ), ), ); foreach ( AICL_Plugin::SOURCE_TYPES as $token => $info ) { $options[] = array( 'value' => $token, 'label' => AICL_Plugin::admin_label_for( $token ), ); } $strings = array( 'panel' => __( 'AI image source', 'aim-transparency' ), 'noImage' => __( 'Set a featured image to mark its AI status.', 'aim-transparency' ), 'label' => __( 'Featured image', 'aim-transparency' ), 'help' => __( 'Saves to the file at once, not with the post. Changes it everywhere the image is used.', 'aim-transparency' ), 'saving' => __( 'Saving…', 'aim-transparency' ), 'saved' => __( 'Saved', 'aim-transparency' ), 'cleared' => __( 'Cleared', 'aim-transparency' ), 'failed' => __( 'Save failed', 'aim-transparency' ), ); return array( 'flag' => esc_url_raw( home_url( '/' ) ) . '?rest_route=/aim-transparency/v1/media/flag', 'nonce' => wp_create_nonce( 'wp_rest' ), 'options' => $options, 'i18n' => $strings, 'types' => self::featured_post_types(), ); } /** * Enqueues the classic-editor featured-image script on the post edit screens. * * @since 1.0.0 * * @param string $hook The current admin page hook suffix. * @return void */ public function featured_classic_assets( $hook ) { if ( ! in_array( $hook, array( 'post.php', 'post-new.php' ), true ) ) { return; } wp_enqueue_script( 'aicl-featured-classic', AICL_URL . 'assets/js/featured-classic.js', array(), AICL_VERSION, true ); wp_localize_script( 'aicl-featured-classic', 'AICL_FEATURED', $this->featured_data() ); } /** * Enqueues the block-editor featured-image script. * * @since 1.0.0 * * @return void */ public function featured_block_assets() { wp_enqueue_script( 'aicl-featured-block', AICL_URL . 'assets/js/featured-block.js', array( 'wp-plugins', 'wp-editor', 'wp-element', 'wp-components', 'wp-data' ), AICL_VERSION, true ); wp_localize_script( 'aicl-featured-block', 'AICL_FEATURED', $this->featured_data() ); } /** * Adds the AI-content dropdown to the media modal and attachment edit screen. * * @since 1.0.0 * * @param array $form_fields The attachment form fields keyed by field slug. * @param WP_Post $post The attachment post object being edited. * @return array The form fields with the AI-content field appended. */ public function field( $form_fields, $post ) { $current = AICL_Plugin::get_type( $post->ID ); $options = ''; foreach ( AICL_Plugin::SOURCE_TYPES as $token => $info ) { $options .= sprintf( '', esc_attr( $token ), selected( $current, $token, false ), esc_html( AICL_Plugin::admin_label_for( $token ) ) ); } $html = ''; $embed = get_post_meta( $post->ID, AICL_Plugin::META_EMBED, true ); if ( $embed ) { $mark = AICL_Report::mark( (string) $embed ); $html .= '

' . esc_html__( 'Written into the file:', 'aim-transparency' ) . ' ' . '' . esc_html( $mark['text'] ) . ''; if ( 'missing' === $mark['state'] ) { $html .= '
' . esc_html__( 'The image is not on the server, so nothing could be written into it. The badge and the JSON-LD still work; re-upload the file to embed the mark.', 'aim-transparency' ); } $html .= '

'; } $form_fields['aicl_source_type'] = array( 'label' => __( 'AI content', 'aim-transparency' ), 'input' => 'html', 'html' => $html, 'helps' => __( 'Record how this image was made. A visible badge shows on your site where Article 50 asks for one, and the source type is written into the file either way.', 'aim-transparency' ), ); return $form_fields; } /** * Persists the AI-content flag and (re)embeds the file metadata. * * @since 1.0.0 * * @param array $post The attachment post data array being saved. * @param array $attachment The submitted attachment field values. * @return array The unmodified attachment post data array. */ public function save( $post, $attachment ) { if ( ! isset( $attachment['aicl_source_type'] ) ) { return $post; } $id = (int) $post['ID']; if ( ! current_user_can( 'edit_post', $id ) ) { return $post; } $token = sanitize_text_field( wp_unslash( $attachment['aicl_source_type'] ) ); if ( AICL_Plugin::is_valid_type( $token ) ) { update_post_meta( $id, AICL_Plugin::META_KEY, $token ); AICL_Embed::embed_attachment( $id ); } else { delete_post_meta( $id, AICL_Plugin::META_KEY ); delete_post_meta( $id, AICL_Plugin::META_EMBED ); } AICL_Plugin::flush_flagged_count(); return $post; } /** * Registers the "AI" column in the Media Library list view. * * @since 1.0.0 * * @param array $columns The existing Media Library columns keyed by slug. * @return array The columns with the "AI" column appended. */ public function column( $columns ) { $columns['aicl'] = __( 'AI', 'aim-transparency' ); return $columns; } /** * Outputs the "AI" column cell content for an attachment row. * * @since 1.0.0 * * @param string $column The slug of the column currently being rendered. * @param int $post_id The attachment post ID for the current row. * @return void */ public function column_value( $column, $post_id ) { if ( 'aicl' !== $column ) { return; } $token = AICL_Plugin::get_type( $post_id ); if ( '' === $token ) { echo ''; return; } $short = array( 'trainedAlgorithmicMedia' => __( 'Gen', 'aim-transparency' ), 'compositeWithTrainedAlgorithmicMedia' => __( 'Mod', 'aim-transparency' ), 'basicAI' => __( 'AI', 'aim-transparency' ), 'algorithmicMedia' => __( 'Alg', 'aim-transparency' ), 'digitalCapture' => __( 'Cam', 'aim-transparency' ), 'digitalCreation' => __( 'Hum', 'aim-transparency' ), 'composite' => __( 'Cmp', 'aim-transparency' ), ); printf( '%s', esc_attr( AICL_Plugin::label_for( $token ) ), esc_html( isset( $short[ $token ] ) ? $short[ $token ] : 'AI' ) ); } /** * Enqueues the inline CSS that keeps the "AI" column narrow so it does not crowd the title column. * * @since 1.0.0 * * @param string $hook The current admin page hook suffix. * @return void */ public function column_style( $hook ) { if ( 'upload.php' !== $hook ) { return; } wp_register_style( 'aicl-media-column', false, array(), AICL_VERSION ); wp_enqueue_style( 'aicl-media-column' ); wp_add_inline_style( 'aicl-media-column', '.wp-list-table .column-aicl{width:60px}.wp-list-table th.column-aicl,.wp-list-table td.column-aicl{white-space:nowrap}' ); } /** * Adds the bulk-flag actions to the Media Library list view dropdown. * * @since 1.0.0 * * @param array $actions The existing bulk actions keyed by action slug. * @return array The bulk actions with the AI-flag actions appended. */ public function bulk_actions( $actions ) { $actions['aicl_mark_generated'] = __( 'Mark as AI Generated', 'aim-transparency' ); $actions['aicl_mark_edited'] = __( 'Mark as AI Modified', 'aim-transparency' ); $actions['aicl_mark_basic'] = __( 'Mark as AI (basic)', 'aim-transparency' ); $actions['aicl_mark_algo'] = __( 'Mark as Algorithmic (non-AI)', 'aim-transparency' ); $actions['aicl_mark_capture'] = __( 'Mark as Camera Photo', 'aim-transparency' ); $actions['aicl_mark_creation'] = __( 'Mark as Human Created', 'aim-transparency' ); $actions['aicl_mark_composite'] = __( 'Mark as Composite (non-AI)', 'aim-transparency' ); $actions['aicl_clear'] = __( 'Clear AI flag', 'aim-transparency' ); return $actions; } /** * Applies the selected bulk AI-flag action to the chosen attachments. * * @since 1.0.0 * * @param string $redirect The redirect URL to return after handling the action. * @param string $action The bulk action slug that was selected. * @param int[] $ids The attachment IDs selected for the bulk action. * @return string The redirect URL, with the processed count appended when any rows changed. */ public function handle_bulk( $redirect, $action, $ids ) { $map = array( 'aicl_mark_generated' => 'trainedAlgorithmicMedia', 'aicl_mark_edited' => 'compositeWithTrainedAlgorithmicMedia', 'aicl_mark_basic' => 'basicAI', 'aicl_mark_algo' => 'algorithmicMedia', 'aicl_mark_capture' => 'digitalCapture', 'aicl_mark_creation' => 'digitalCreation', 'aicl_mark_composite' => 'composite', ); $count = 0; foreach ( array_map( 'intval', (array) $ids ) as $id ) { if ( ! current_user_can( 'edit_post', $id ) ) { continue; } if ( 'aicl_clear' === $action ) { delete_post_meta( $id, AICL_Plugin::META_KEY ); delete_post_meta( $id, AICL_Plugin::META_EMBED ); ++$count; } elseif ( isset( $map[ $action ] ) ) { update_post_meta( $id, AICL_Plugin::META_KEY, $map[ $action ] ); AICL_Embed::embed_attachment( $id ); ++$count; } } if ( $count ) { AICL_Plugin::flush_flagged_count(); $redirect = add_query_arg( 'aicl_done', $count, $redirect ); } return $redirect; } /** * Displays the admin notice summarizing how many images a bulk action updated. * * @since 1.0.0 * * @return void */ public function bulk_notice() { if ( isset( $_GET['aicl_done'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only admin notice after our own add_query_arg redirect; value is cast to int and escaped below. $n = absint( wp_unslash( $_GET['aicl_done'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- see above. printf( '

%s

', /* translators: %d is the number of images updated. */ esc_html( sprintf( _n( '%d image updated.', '%d images updated.', $n, 'aim-transparency' ), $n ) ) ); } } }