' . 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 . ''; } /** * Builds the shared localization data passed to the featured-image editor scripts. * * @since 1.0.0 * * @return array{flag:string,nonce:string,options:array} Endpoint URL, REST nonce, and source-type option list. */ 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 ) ); } return array( 'flag' => esc_url_raw( home_url( '/' ) ) . '?rest_route=/aim-transparency/v1/media/flag', 'nonce' => wp_create_nonce( 'wp_rest' ), 'options' => $options, ); } /** * 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 ) { $html .= '

' . esc_html__( 'File metadata:', 'aim-transparency' ) . ' ' . '' . esc_html( $embed ) . '

'; } $form_fields['aicl_source_type'] = array( 'label' => __( 'AI content', 'aim-transparency' ), 'input' => 'html', 'html' => $html, 'helps' => __( 'EU AI Act (Art. 50) transparency: flag AI-generated or AI-edited images to add a visible badge + machine-readable metadata.', '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_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_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 ) ) ); } } }