' . 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_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(
'',
/* translators: %d is the number of images updated. */
esc_html( sprintf( _n( '%d image updated.', '%d images updated.', $n, 'aim-transparency' ), $n ) )
);
}
}
}