tab_page_slug( self::TAB_SLUG ); register_setting( $page_slug, Mime_Types_Store::OPTION, array( 'type' => 'array', 'sanitize_callback' => array( $this, 'sanitize_option' ), 'default' => array(), ) ); add_settings_section( 'acrossai_core_upload_media', __( 'Upload Media Abilities', 'acrossai-abilities-manager' ), array( $this, 'render_upload_media_section' ), $page_slug ); add_settings_field( 'acrossai_core_mimes_currently_allowed', __( 'Allowed for upload-media', 'acrossai-abilities-manager' ), array( $this, 'render_currently_allowed_field' ), $page_slug, 'acrossai_core_upload_media' ); add_settings_field( 'acrossai_core_mimes_custom', __( 'Add file types', 'acrossai-abilities-manager' ), array( $this, 'render_custom_field' ), $page_slug, 'acrossai_core_upload_media' ); } /** * Sanitize callback for the wp_option. Parses the textarea into the * canonical ext=>mime map, hands it to Mime_Types_Store, and surfaces * per-entry rejections as settings_errors so the admin sees exactly * what was dropped. * * @param mixed $input Raw value from $_POST[ Mime_Types_Store::OPTION ]. * @return array */ public function sanitize_option( $input ): array { if ( ! is_array( $input ) ) { return array(); } // The Settings API sanitize callback fires once with the raw form // value (`[ 'custom' => "ext = mime\n..." ]`) and a second time with // the already-normalized `ext => mime` map when update_option() // delegates to add_option() (which happens whenever the stored value // equals the registered `default`). Both shapes must be accepted or // the second pass clobbers our own output back to `[]`. if ( isset( $input['custom'] ) && is_string( $input['custom'] ) ) { $additions = array(); foreach ( preg_split( '/\r\n|\r|\n/', $input['custom'] ) as $line_no => $line ) { $line = trim( $line ); if ( '' === $line ) { continue; } if ( ! str_contains( $line, '=' ) ) { add_settings_error( Mime_Types_Store::OPTION, 'acrossai_core_mimes_bad_line_' . $line_no, sprintf( /* translators: %s: the offending line */ esc_html__( 'Skipped custom line "%s": expected "ext = mime/type".', 'acrossai-abilities-manager' ), esc_html( $line ) ), 'error' ); continue; } list( $ext, $mime ) = array_map( 'trim', explode( '=', $line, 2 ) ); $additions[ $ext ] = $mime; } } else { $additions = $input; } // Use validate() (not set()) here: we're already inside WordPress's // update_option() → sanitize_option_{OPTION} filter, so calling set() // would re-enter that same filter and OOM the site. Return the // validated value; WordPress persists it via the normal Settings API // round-trip after this callback returns. $result = Mime_Types_Store::validate( $additions ); foreach ( $result['skipped'] as $entry ) { add_settings_error( Mime_Types_Store::OPTION, 'acrossai_core_mimes_skipped_' . md5( $entry['ext'] . '|' . $entry['mime'] ), sprintf( /* translators: 1: extension, 2: mime type, 3: rejection reason */ esc_html__( 'Skipped "%1$s = %2$s": %3$s', 'acrossai-abilities-manager' ), esc_html( $entry['ext'] ), esc_html( $entry['mime'] ), esc_html( $entry['reason'] ) ), 'error' ); } return $result['stored']; } /** * Render the section intro copy for the Upload Media MIME-types block. * * @return void */ public function render_upload_media_section(): void { $connectors_url = admin_url( 'options-connectors.php' ); echo '
'; echo '

'; esc_html_e( 'Generate images with AI', 'acrossai-abilities-manager' ); echo '

'; echo '

'; printf( /* translators: %s: the ability slug, wrapped in a tag. */ wp_kses( __( 'The %s ability turns a text prompt into a real image and adds it to the Media Library in one step — processed exactly like an uploaded file, so all the standard image sizes (thumbnail, medium, large and any your theme or plugins register) are generated too. It can also attach the image to a post and set it as the post\'s featured image.', 'acrossai-abilities-manager' ), array( 'code' => array() ) ), 'media/generate-image' ); echo '

'; echo '

'; esc_html_e( 'What it needs', 'acrossai-abilities-manager' ); echo '

'; echo '
    '; echo '
  • '; esc_html_e( 'WordPress 7.0 or newer — it uses the AI client that ships inside WordPress, so no additional plugin is required.', 'acrossai-abilities-manager' ); echo '
  • '; echo '
  • '; printf( /* translators: %s: URL of the Connectors settings screen. */ wp_kses( __( 'An AI connector with an API key, set up under Settings → Connectors. It works with whichever provider you have a key for — the built-in connectors include Anthropic, Google, OpenAI and Cloudflare Workers AI (which has a free daily allowance), and more can be added — so you are not tied to any single provider or key.', 'acrossai-abilities-manager' ), array( 'a' => array( 'href' => array() ) ) ), esc_url( $connectors_url ) ); echo '
  • '; echo '
  • '; esc_html_e( 'A connector that actually offers image generation. Not every provider or model can create images — text-only ones (for example a chat-only model such as Anthropic / Claude) cannot — so pick one with image models, such as Google, OpenAI or Cloudflare Workers AI. The ability picks a configured image-capable model for you, or you can name a provider and model yourself.', 'acrossai-abilities-manager' ); echo '
  • '; echo '
'; echo '

'; printf( /* translators: %s: URL of the Connectors settings screen. */ wp_kses( __( 'Each run makes a paid request to your connector, so the ability asks for a confirmation (confirm: true) before it generates. If no image-capable connector is set up, it returns a clear message pointing to Settings → Connectors instead of failing — nothing is charged.', 'acrossai-abilities-manager' ), array( 'code' => array(), 'a' => array( 'href' => array() ) ) ), esc_url( $connectors_url ) ); echo '

'; echo '
'; echo '

'; esc_html_e( 'The settings below control the file types the upload-media ability will accept. Entries here are merged into WordPress\'s allowlist only during upload-media calls — regular Media Library uploads via wp-admin are NOT affected. Add-only for core defaults; entries you add here can be removed from the same list.', 'acrossai-abilities-manager' ); echo '

'; } /** * Render the "currently allowed" read-only MIME-types field. * * @return void */ public function render_currently_allowed_field(): void { // Merge WP core defaults + other plugins' filters with this plugin's // extras to show what upload-media will actually accept. $effective = Mime_Types_Store::filter_upload_mimes( get_allowed_mime_types() ); $mimes = array_unique( array_values( $effective ) ); sort( $mimes ); echo '' . esc_html( implode( ', ', $mimes ) ) . ''; } /** * Render the "custom MIME types" textarea field. * * @return void */ public function render_custom_field(): void { $stored = Mime_Types_Store::get(); $custom_lines = array(); foreach ( $stored as $ext => $mime ) { $custom_lines[] = $ext . ' = ' . $mime; } sort( $custom_lines ); $name = sprintf( '%s[custom]', esc_attr( Mime_Types_Store::OPTION ) ); $placeholder = "svg = image/svg+xml\nwebp = image/webp\navif = image/avif"; printf( '', esc_attr( $name ), esc_attr( $placeholder ), esc_textarea( implode( "\n", $custom_lines ) ) ); ?>