Plugin Directory

Changeset 3787850


Ignore:
Timestamp:
11/06/2026 12:01:11 PM (4 hours ago)
Author:
mnecas
Message:

Release 1.1.0

Location:
originmark-ai
Files:
4 edited
1 copied

Legend:

Unmodified
Added
Removed
  • originmark-ai/tags/1.1.0/originmark-ai.php

    r3661021 r3787850  
    33 * Plugin Name: OriginMark AI
    44 * Description: Lets you mark AI-generated content on your site with a visible "AI Generated" label. Currently supports images (size, position and offset settings); more content types are planned.
    5  * Version: 1.0
     5 * Version: 1.1.0
     6 * Requires at least: 5.8
     7 * Requires PHP: 8.1
    68 * Author: mnecas
    79 * License: GPLv2 or later
     10 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
    811 * Donate link: https://buymeacoffee.com/mnecas
    912 * Text Domain: originmark-ai
    … …  
    1114
    1215if ( ! defined( 'ABSPATH' ) ) exit;
     16
     17const ORIGINMARK_DB_VERSION      = 2;
     18const ORIGINMARK_META_KEY        = '_originmark_ai_generated';
     19const ORIGINMARK_LEGACY_META_KEY = '_is_ai_generated';
     20
     21function originmark_register_meta() {
     22    register_post_meta( 'attachment', ORIGINMARK_META_KEY, array(
     23        'type'          => 'boolean',
     24        'single'        => true,
     25        'default'       => false,
     26        'show_in_rest'  => true,
     27        'auth_callback' => static fn( $allowed, $meta_key, $post_id ) => current_user_can( 'edit_post', $post_id ),
     28    ) );
     29}
     30add_action( 'init', 'originmark_register_meta' );
     31
     32function originmark_maybe_upgrade() {
     33    if ( (int) get_option( 'originmark_db_version', 1 ) >= ORIGINMARK_DB_VERSION ) {
     34        return;
     35    }
     36
     37    global $wpdb;
     38
     39    $result = $wpdb->query(
     40        $wpdb->prepare(
     41            "INSERT INTO {$wpdb->postmeta} ( post_id, meta_key, meta_value )
     42             SELECT old_meta.post_id, %s, '1'
     43             FROM {$wpdb->postmeta} AS old_meta
     44             INNER JOIN {$wpdb->posts} AS p ON p.ID = old_meta.post_id
     45             LEFT JOIN {$wpdb->postmeta} AS new_meta ON new_meta.post_id = old_meta.post_id AND new_meta.meta_key = %s
     46             WHERE old_meta.meta_key = %s
     47               AND old_meta.meta_value = '1'
     48               AND p.post_type = 'attachment'
     49               AND p.post_mime_type LIKE %s
     50               AND new_meta.meta_id IS NULL",
     51            ORIGINMARK_META_KEY,
     52            ORIGINMARK_META_KEY,
     53            ORIGINMARK_LEGACY_META_KEY,
     54            $wpdb->esc_like( 'image/' ) . '%'
     55        )
     56    );
     57
     58    if ( false === $result ) {
     59        return;
     60    }
     61
     62    if ( function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_group' ) ) {
     63        wp_cache_flush_group( 'post_meta' );
     64    }
     65
     66    update_option( 'originmark_db_version', ORIGINMARK_DB_VERSION, true );
     67}
     68add_action( 'init', 'originmark_maybe_upgrade', 20 );
     69
     70function originmark_is_ai_generated( int $attachment_id ): bool {
     71    return '1' === (string) get_post_meta( $attachment_id, ORIGINMARK_META_KEY, true )
     72        || '1' === get_post_meta( $attachment_id, ORIGINMARK_LEGACY_META_KEY, true );
     73}
    1374
    1475
    … …  
    182243// knihovna medii
    183244function originmark_add_media_field( $form_fields, $post ) {
    184     $is_ai = get_post_meta( $post->ID, '_is_ai_generated', true );
     245    if ( ! wp_attachment_is_image( $post ) ) {
     246        return $form_fields;
     247    }
     248
     249    $is_ai = originmark_is_ai_generated( $post->ID );
    185250
    186251    $form_fields['is_ai_generated'] = array(
    … …  
    195260            </div>',
    196261            $post->ID,
    197             checked( $is_ai, '1', false ),
     262            checked( $is_ai, true, false ),
    198263            esc_html__( 'Mark image as created by AI', 'originmark-ai' )
    199264        ),
    … …  
    203268add_filter( 'attachment_fields_to_edit', 'originmark_add_media_field', 10, 2 );
    204269
    205 /**
    206  * Uložení z media
    207  */
    208 function originmark_save_media_field( $post_id ) {
    209     if ( ! isset( $_POST['attachments'][ $post_id ] ) ) {
    210         return;
    211     }
    212 
    213     $attachment_data = $_POST['attachments'][ $post_id ];
    214 
    215     if ( isset( $attachment_data['is_ai_generated'] ) ) {
    216         update_post_meta( $post_id, '_is_ai_generated', '1' );
     270function originmark_save_media_field( $post, $attachment_data ) {
     271    $attachment_id = (int) ( $post['ID'] ?? $post['post_ID'] ?? 0 );
     272
     273    if ( ! $attachment_id || ! wp_attachment_is_image( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) ) {
     274        return $post;
     275    }
     276
     277    if ( ! empty( $attachment_data['is_ai_generated'] ) ) {
     278        update_post_meta( $attachment_id, ORIGINMARK_META_KEY, '1' );
    217279    } else {
    218         delete_post_meta( $post_id, '_is_ai_generated' );
    219     }
    220 }
    221 add_action( 'edit_attachment', 'originmark_save_media_field' );
    222 
    223 /**
    224  * Uložení z modálního okna Media Library
    225  */
    226 function originmark_save_media_field_ajax( $post, $attachment_data ) {
    227     if ( isset( $attachment_data['is_ai_generated'] ) ) {
    228         update_post_meta( $post['ID'], '_is_ai_generated', '1' );
    229     } else {
    230         delete_post_meta( $post['ID'], '_is_ai_generated' );
    231     }
     280        delete_post_meta( $attachment_id, ORIGINMARK_META_KEY );
     281        delete_post_meta( $attachment_id, ORIGINMARK_LEGACY_META_KEY );
     282    }
     283
    232284    return $post;
    233285}
    234 add_filter( 'attachment_fields_to_save', 'originmark_save_media_field_ajax', 10, 2 );
    235 
    236 
    237 // 4. generovani struktury
    238 function originmark_inject_badge( $html ) {
    239     $chosen_icon = get_option( 'originmark_selected_icon', 'label_ai_generated_white_transparent.svg' );
    240     $icon_size   = get_option( 'originmark_icon_size', '100' );
    241     $icon_pos    = get_option( 'originmark_icon_position', 'bottom_right' );
    242     $icon_offset = get_option( 'originmark_icon_offset', '15' );
    243 
    244     // Obrana do hloubky - i kdyby v DB byla stará/nevalidní hodnota, znovu ji zde ošetříme.
    245     $chosen_icon = originmark_sanitize_icon( $chosen_icon );
    246     $icon_size   = originmark_sanitize_size( $icon_size );
    247     $icon_pos    = originmark_sanitize_position( $icon_pos );
    248     $icon_offset = originmark_sanitize_offset( $icon_offset );
    249 
    250     $icon_url = plugins_url( $chosen_icon, __FILE__ );
    251 
    252     $icon_offset_attr = esc_attr( $icon_offset );
    253     $icon_size_attr   = esc_attr( $icon_size );
    254 
    255     switch ( $icon_pos ) {
    256         case 'top_left':
    257             $position_styles = 'top: ' . $icon_offset_attr . 'px; left: ' . $icon_offset_attr . 'px;';
    258             break;
    259         case 'top_right':
    260             $position_styles = 'top: ' . $icon_offset_attr . 'px; right: ' . $icon_offset_attr . 'px;';
    261             break;
    262         case 'bottom_left':
    263             $position_styles = 'bottom: ' . $icon_offset_attr . 'px; left: ' . $icon_offset_attr . 'px;';
    264             break;
    265         case 'bottom_right':
    266         default:
    267             $position_styles = 'bottom: ' . $icon_offset_attr . 'px; right: ' . $icon_offset_attr . 'px;';
    268             break;
    269     }
    270 
    271     return '<div class="ai-image-wrapper" style="position: relative; display: block; max-width: 100%; width: fit-content; line-height: 0;">'
    272           . $html
    273           . '<img class="ai-badge-svg" src="' . esc_url( $icon_url ) . '" alt="' . esc_attr__( 'AI Generated', 'originmark-ai' ) . '" style="position: absolute; ' . $position_styles . ' width: ' . $icon_size_attr . 'px; height: auto; pointer-events: none; z-index: 10; filter: drop-shadow(0px 2px 4px rgba(0,0,0,0.3));" />'
    274           . '</div>';
    275 }
    276 
    277 
    278 // Zobrazení na webu na základě ID obrázku
     286add_filter( 'attachment_fields_to_save', 'originmark_save_media_field', 10, 2 );
     287
     288
     289function originmark_get_badge_markup(): string {
     290    static $markup = null;
     291
     292    if ( null !== $markup ) {
     293        return $markup;
     294    }
     295
     296    $chosen_icon = originmark_sanitize_icon( get_option( 'originmark_selected_icon', 'label_ai_generated_white_transparent.svg' ) );
     297    $icon_size   = originmark_sanitize_size( get_option( 'originmark_icon_size', 100 ) );
     298    $icon_pos    = originmark_sanitize_position( get_option( 'originmark_icon_position', 'bottom_right' ) );
     299    $icon_offset = originmark_sanitize_offset( get_option( 'originmark_icon_offset', 15 ) );
     300
     301    $position_styles = match ( $icon_pos ) {
     302        'top_left'    => "top: {$icon_offset}px; left: {$icon_offset}px;",
     303        'top_right'   => "top: {$icon_offset}px; right: {$icon_offset}px;",
     304        'bottom_left' => "bottom: {$icon_offset}px; left: {$icon_offset}px;",
     305        default       => "bottom: {$icon_offset}px; right: {$icon_offset}px;",
     306    };
     307
     308    $style = "position: absolute; {$position_styles} width: {$icon_size}px; height: auto; pointer-events: none; z-index: 10; filter: drop-shadow(0px 2px 4px rgba(0,0,0,0.3));";
     309
     310    $markup = sprintf(
     311        '<img class="ai-badge-svg" src="%s" alt="%s" style="%s" />',
     312        esc_url( plugins_url( $chosen_icon, __FILE__ ) ),
     313        esc_attr__( 'AI Generated', 'originmark-ai' ),
     314        esc_attr( $style )
     315    );
     316
     317    return $markup;
     318}
     319
     320function originmark_prepare_image_html( string $html ): array {
     321    if ( ! preg_match( '/<img\b[^>]*>/i', $html, $img_match, PREG_OFFSET_CAPTURE ) ) {
     322        return array( $html, '' );
     323    }
     324
     325    [ $img_tag, $offset ] = $img_match[0];
     326    $alignment = '';
     327
     328    $new_tag = preg_replace_callback(
     329        '/\sclass=(["\'])(.*?)\1/i',
     330        function ( array $m ) use ( &$alignment ): string {
     331            $classes = preg_split( '/\s+/', trim( $m[2] ), -1, PREG_SPLIT_NO_EMPTY );
     332            $found   = array_intersect( $classes, array( 'alignleft', 'alignright', 'aligncenter' ) );
     333
     334            $alignment = (string) reset( $found );
     335
     336            return ' class=' . $m[1] . implode( ' ', array_diff( $classes, $found ) ) . $m[1];
     337        },
     338        $img_tag,
     339        1
     340    );
     341
     342    $new_tag = preg_replace( '/^<img\b/i', '<img data-originmark="1"', $new_tag );
     343
     344    return array( substr_replace( $html, $new_tag, $offset, strlen( $img_tag ) ), $alignment );
     345}
     346
     347function originmark_inject_badge( string $html ): string {
     348    [ $html, $alignment ] = originmark_prepare_image_html( $html );
     349
     350    $wrapper_style = match ( $alignment ) {
     351        'aligncenter' => 'display: block; width: fit-content; margin-inline: auto;',
     352        'alignleft'   => 'display: inline-block; float: left;',
     353        'alignright'  => 'display: inline-block; float: right;',
     354        default       => 'display: inline-block; vertical-align: bottom;',
     355    };
     356
     357    return sprintf(
     358        '<span class="%s" style="%s">%s%s</span>',
     359        esc_attr( trim( 'ai-image-wrapper ' . $alignment ) ),
     360        esc_attr( 'position: relative; max-width: 100%; line-height: 0; ' . $wrapper_style ),
     361        $html,
     362        originmark_get_badge_markup()
     363    );
     364}
     365
     366function originmark_maybe_wrap_image( array $matches ): string {
     367    $html = $matches[0];
     368
     369    if ( str_contains( $html, 'data-originmark' ) ) {
     370        return $html;
     371    }
     372
     373    if ( ! preg_match( '/<img\b[^>]*\bwp-image-(\d+)\b/i', $html, $id_match ) ) {
     374        return $html;
     375    }
     376
     377    if ( ! originmark_is_ai_generated( (int) $id_match[1] ) ) {
     378        return $html;
     379    }
     380
     381    return originmark_inject_badge( $html );
     382}
     383
    279384function originmark_filter_content_images( $content ) {
    280     if ( is_admin() || is_feed() ) {
     385    if ( is_admin() || is_feed() || ! is_string( $content ) || ! str_contains( $content, '<img' ) ) {
    281386        return $content;
    282387    }
    283388
    284     if ( preg_match_all( '/<img[^>]+>/i', $content, $matches ) ) {
    285         foreach ( $matches[0] as $img_html ) {
    286             $has_badge = false;
    287 
    288             // Kontrola přes ID obrázku zapsané ve třídě wp-image-XYZ
    289             if ( preg_match( '/wp-image-([0-9]+)/', $img_html, $id_match ) ) {
    290                 $attachment_id = intval( $id_match[1] );
    291                 if ( get_post_meta( $attachment_id, '_is_ai_generated', true ) === '1' ) {
    292                     $has_badge = true;
    293                 }
    294             }
    295 
    296             if ( $has_badge ) {
    297                 $modified_img = originmark_inject_badge( $img_html );
    298                 $content = str_replace( $img_html, $modified_img, $content );
    299             }
    300         }
    301     }
    302     return $content;
     389    return preg_replace_callback(
     390        '/<picture\b[^>]*>.*?<\/picture>|<img\b[^>]*>/is',
     391        'originmark_maybe_wrap_image',
     392        $content
     393    ) ?? $content;
    303394}
    304395add_filter( 'the_content', 'originmark_filter_content_images', 20 );
  • originmark-ai/tags/1.1.0/readme.txt

    r3661053 r3787850  
    55Requires at least: 5.8
    66Tested up to: 7.0
    7 Requires PHP: 7.4
    8 Stable tag: 1.0
     7Requires PHP: 8.1
     8Stable tag: 1.1.0
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    6464== Changelog ==
    6565
     66= 1.1.0 =
     67* Fix: an image used more than once in a post no longer gets a duplicate badge.
     68* Fix: badge wrapper no longer breaks <picture> elements, paragraphs or left/right/center alignment.
     69* Fix: the "AI Generated" checkbox is shown only for images.
     70* Security: saving the checkbox now relies only on the core attachment save flow with a capability check.
     71* Change: the AI flag is stored under a prefixed meta key; existing data is migrated automatically.
     72* Requires PHP 8.1 or newer.
     73
    6674= 1.0 =
    6775* Initial release.
    … …  
    6977== Upgrade Notice ==
    7078
     79= 1.1.0 =
     80Bug fixes for badge rendering. Requires PHP 8.1+. Existing AI flags are migrated automatically.
     81
    7182= 1.0 =
    7283Initial release.
  • originmark-ai/trunk/originmark-ai.php

    r3661021 r3787850  
    33 * Plugin Name: OriginMark AI
    44 * Description: Lets you mark AI-generated content on your site with a visible "AI Generated" label. Currently supports images (size, position and offset settings); more content types are planned.
    5  * Version: 1.0
     5 * Version: 1.1.0
     6 * Requires at least: 5.8
     7 * Requires PHP: 8.1
    68 * Author: mnecas
    79 * License: GPLv2 or later
     10 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
    811 * Donate link: https://buymeacoffee.com/mnecas
    912 * Text Domain: originmark-ai
    … …  
    1114
    1215if ( ! defined( 'ABSPATH' ) ) exit;
     16
     17const ORIGINMARK_DB_VERSION      = 2;
     18const ORIGINMARK_META_KEY        = '_originmark_ai_generated';
     19const ORIGINMARK_LEGACY_META_KEY = '_is_ai_generated';
     20
     21function originmark_register_meta() {
     22    register_post_meta( 'attachment', ORIGINMARK_META_KEY, array(
     23        'type'          => 'boolean',
     24        'single'        => true,
     25        'default'       => false,
     26        'show_in_rest'  => true,
     27        'auth_callback' => static fn( $allowed, $meta_key, $post_id ) => current_user_can( 'edit_post', $post_id ),
     28    ) );
     29}
     30add_action( 'init', 'originmark_register_meta' );
     31
     32function originmark_maybe_upgrade() {
     33    if ( (int) get_option( 'originmark_db_version', 1 ) >= ORIGINMARK_DB_VERSION ) {
     34        return;
     35    }
     36
     37    global $wpdb;
     38
     39    $result = $wpdb->query(
     40        $wpdb->prepare(
     41            "INSERT INTO {$wpdb->postmeta} ( post_id, meta_key, meta_value )
     42             SELECT old_meta.post_id, %s, '1'
     43             FROM {$wpdb->postmeta} AS old_meta
     44             INNER JOIN {$wpdb->posts} AS p ON p.ID = old_meta.post_id
     45             LEFT JOIN {$wpdb->postmeta} AS new_meta ON new_meta.post_id = old_meta.post_id AND new_meta.meta_key = %s
     46             WHERE old_meta.meta_key = %s
     47               AND old_meta.meta_value = '1'
     48               AND p.post_type = 'attachment'
     49               AND p.post_mime_type LIKE %s
     50               AND new_meta.meta_id IS NULL",
     51            ORIGINMARK_META_KEY,
     52            ORIGINMARK_META_KEY,
     53            ORIGINMARK_LEGACY_META_KEY,
     54            $wpdb->esc_like( 'image/' ) . '%'
     55        )
     56    );
     57
     58    if ( false === $result ) {
     59        return;
     60    }
     61
     62    if ( function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_group' ) ) {
     63        wp_cache_flush_group( 'post_meta' );
     64    }
     65
     66    update_option( 'originmark_db_version', ORIGINMARK_DB_VERSION, true );
     67}
     68add_action( 'init', 'originmark_maybe_upgrade', 20 );
     69
     70function originmark_is_ai_generated( int $attachment_id ): bool {
     71    return '1' === (string) get_post_meta( $attachment_id, ORIGINMARK_META_KEY, true )
     72        || '1' === get_post_meta( $attachment_id, ORIGINMARK_LEGACY_META_KEY, true );
     73}
    1374
    1475
    … …  
    182243// knihovna medii
    183244function originmark_add_media_field( $form_fields, $post ) {
    184     $is_ai = get_post_meta( $post->ID, '_is_ai_generated', true );
     245    if ( ! wp_attachment_is_image( $post ) ) {
     246        return $form_fields;
     247    }
     248
     249    $is_ai = originmark_is_ai_generated( $post->ID );
    185250
    186251    $form_fields['is_ai_generated'] = array(
    … …  
    195260            </div>',
    196261            $post->ID,
    197             checked( $is_ai, '1', false ),
     262            checked( $is_ai, true, false ),
    198263            esc_html__( 'Mark image as created by AI', 'originmark-ai' )
    199264        ),
    … …  
    203268add_filter( 'attachment_fields_to_edit', 'originmark_add_media_field', 10, 2 );
    204269
    205 /**
    206  * Uložení z media
    207  */
    208 function originmark_save_media_field( $post_id ) {
    209     if ( ! isset( $_POST['attachments'][ $post_id ] ) ) {
    210         return;
    211     }
    212 
    213     $attachment_data = $_POST['attachments'][ $post_id ];
    214 
    215     if ( isset( $attachment_data['is_ai_generated'] ) ) {
    216         update_post_meta( $post_id, '_is_ai_generated', '1' );
     270function originmark_save_media_field( $post, $attachment_data ) {
     271    $attachment_id = (int) ( $post['ID'] ?? $post['post_ID'] ?? 0 );
     272
     273    if ( ! $attachment_id || ! wp_attachment_is_image( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) ) {
     274        return $post;
     275    }
     276
     277    if ( ! empty( $attachment_data['is_ai_generated'] ) ) {
     278        update_post_meta( $attachment_id, ORIGINMARK_META_KEY, '1' );
    217279    } else {
    218         delete_post_meta( $post_id, '_is_ai_generated' );
    219     }
    220 }
    221 add_action( 'edit_attachment', 'originmark_save_media_field' );
    222 
    223 /**
    224  * Uložení z modálního okna Media Library
    225  */
    226 function originmark_save_media_field_ajax( $post, $attachment_data ) {
    227     if ( isset( $attachment_data['is_ai_generated'] ) ) {
    228         update_post_meta( $post['ID'], '_is_ai_generated', '1' );
    229     } else {
    230         delete_post_meta( $post['ID'], '_is_ai_generated' );
    231     }
     280        delete_post_meta( $attachment_id, ORIGINMARK_META_KEY );
     281        delete_post_meta( $attachment_id, ORIGINMARK_LEGACY_META_KEY );
     282    }
     283
    232284    return $post;
    233285}
    234 add_filter( 'attachment_fields_to_save', 'originmark_save_media_field_ajax', 10, 2 );
    235 
    236 
    237 // 4. generovani struktury
    238 function originmark_inject_badge( $html ) {
    239     $chosen_icon = get_option( 'originmark_selected_icon', 'label_ai_generated_white_transparent.svg' );
    240     $icon_size   = get_option( 'originmark_icon_size', '100' );
    241     $icon_pos    = get_option( 'originmark_icon_position', 'bottom_right' );
    242     $icon_offset = get_option( 'originmark_icon_offset', '15' );
    243 
    244     // Obrana do hloubky - i kdyby v DB byla stará/nevalidní hodnota, znovu ji zde ošetříme.
    245     $chosen_icon = originmark_sanitize_icon( $chosen_icon );
    246     $icon_size   = originmark_sanitize_size( $icon_size );
    247     $icon_pos    = originmark_sanitize_position( $icon_pos );
    248     $icon_offset = originmark_sanitize_offset( $icon_offset );
    249 
    250     $icon_url = plugins_url( $chosen_icon, __FILE__ );
    251 
    252     $icon_offset_attr = esc_attr( $icon_offset );
    253     $icon_size_attr   = esc_attr( $icon_size );
    254 
    255     switch ( $icon_pos ) {
    256         case 'top_left':
    257             $position_styles = 'top: ' . $icon_offset_attr . 'px; left: ' . $icon_offset_attr . 'px;';
    258             break;
    259         case 'top_right':
    260             $position_styles = 'top: ' . $icon_offset_attr . 'px; right: ' . $icon_offset_attr . 'px;';
    261             break;
    262         case 'bottom_left':
    263             $position_styles = 'bottom: ' . $icon_offset_attr . 'px; left: ' . $icon_offset_attr . 'px;';
    264             break;
    265         case 'bottom_right':
    266         default:
    267             $position_styles = 'bottom: ' . $icon_offset_attr . 'px; right: ' . $icon_offset_attr . 'px;';
    268             break;
    269     }
    270 
    271     return '<div class="ai-image-wrapper" style="position: relative; display: block; max-width: 100%; width: fit-content; line-height: 0;">'
    272           . $html
    273           . '<img class="ai-badge-svg" src="' . esc_url( $icon_url ) . '" alt="' . esc_attr__( 'AI Generated', 'originmark-ai' ) . '" style="position: absolute; ' . $position_styles . ' width: ' . $icon_size_attr . 'px; height: auto; pointer-events: none; z-index: 10; filter: drop-shadow(0px 2px 4px rgba(0,0,0,0.3));" />'
    274           . '</div>';
    275 }
    276 
    277 
    278 // Zobrazení na webu na základě ID obrázku
     286add_filter( 'attachment_fields_to_save', 'originmark_save_media_field', 10, 2 );
     287
     288
     289function originmark_get_badge_markup(): string {
     290    static $markup = null;
     291
     292    if ( null !== $markup ) {
     293        return $markup;
     294    }
     295
     296    $chosen_icon = originmark_sanitize_icon( get_option( 'originmark_selected_icon', 'label_ai_generated_white_transparent.svg' ) );
     297    $icon_size   = originmark_sanitize_size( get_option( 'originmark_icon_size', 100 ) );
     298    $icon_pos    = originmark_sanitize_position( get_option( 'originmark_icon_position', 'bottom_right' ) );
     299    $icon_offset = originmark_sanitize_offset( get_option( 'originmark_icon_offset', 15 ) );
     300
     301    $position_styles = match ( $icon_pos ) {
     302        'top_left'    => "top: {$icon_offset}px; left: {$icon_offset}px;",
     303        'top_right'   => "top: {$icon_offset}px; right: {$icon_offset}px;",
     304        'bottom_left' => "bottom: {$icon_offset}px; left: {$icon_offset}px;",
     305        default       => "bottom: {$icon_offset}px; right: {$icon_offset}px;",
     306    };
     307
     308    $style = "position: absolute; {$position_styles} width: {$icon_size}px; height: auto; pointer-events: none; z-index: 10; filter: drop-shadow(0px 2px 4px rgba(0,0,0,0.3));";
     309
     310    $markup = sprintf(
     311        '<img class="ai-badge-svg" src="%s" alt="%s" style="%s" />',
     312        esc_url( plugins_url( $chosen_icon, __FILE__ ) ),
     313        esc_attr__( 'AI Generated', 'originmark-ai' ),
     314        esc_attr( $style )
     315    );
     316
     317    return $markup;
     318}
     319
     320function originmark_prepare_image_html( string $html ): array {
     321    if ( ! preg_match( '/<img\b[^>]*>/i', $html, $img_match, PREG_OFFSET_CAPTURE ) ) {
     322        return array( $html, '' );
     323    }
     324
     325    [ $img_tag, $offset ] = $img_match[0];
     326    $alignment = '';
     327
     328    $new_tag = preg_replace_callback(
     329        '/\sclass=(["\'])(.*?)\1/i',
     330        function ( array $m ) use ( &$alignment ): string {
     331            $classes = preg_split( '/\s+/', trim( $m[2] ), -1, PREG_SPLIT_NO_EMPTY );
     332            $found   = array_intersect( $classes, array( 'alignleft', 'alignright', 'aligncenter' ) );
     333
     334            $alignment = (string) reset( $found );
     335
     336            return ' class=' . $m[1] . implode( ' ', array_diff( $classes, $found ) ) . $m[1];
     337        },
     338        $img_tag,
     339        1
     340    );
     341
     342    $new_tag = preg_replace( '/^<img\b/i', '<img data-originmark="1"', $new_tag );
     343
     344    return array( substr_replace( $html, $new_tag, $offset, strlen( $img_tag ) ), $alignment );
     345}
     346
     347function originmark_inject_badge( string $html ): string {
     348    [ $html, $alignment ] = originmark_prepare_image_html( $html );
     349
     350    $wrapper_style = match ( $alignment ) {
     351        'aligncenter' => 'display: block; width: fit-content; margin-inline: auto;',
     352        'alignleft'   => 'display: inline-block; float: left;',
     353        'alignright'  => 'display: inline-block; float: right;',
     354        default       => 'display: inline-block; vertical-align: bottom;',
     355    };
     356
     357    return sprintf(
     358        '<span class="%s" style="%s">%s%s</span>',
     359        esc_attr( trim( 'ai-image-wrapper ' . $alignment ) ),
     360        esc_attr( 'position: relative; max-width: 100%; line-height: 0; ' . $wrapper_style ),
     361        $html,
     362        originmark_get_badge_markup()
     363    );
     364}
     365
     366function originmark_maybe_wrap_image( array $matches ): string {
     367    $html = $matches[0];
     368
     369    if ( str_contains( $html, 'data-originmark' ) ) {
     370        return $html;
     371    }
     372
     373    if ( ! preg_match( '/<img\b[^>]*\bwp-image-(\d+)\b/i', $html, $id_match ) ) {
     374        return $html;
     375    }
     376
     377    if ( ! originmark_is_ai_generated( (int) $id_match[1] ) ) {
     378        return $html;
     379    }
     380
     381    return originmark_inject_badge( $html );
     382}
     383
    279384function originmark_filter_content_images( $content ) {
    280     if ( is_admin() || is_feed() ) {
     385    if ( is_admin() || is_feed() || ! is_string( $content ) || ! str_contains( $content, '<img' ) ) {
    281386        return $content;
    282387    }
    283388
    284     if ( preg_match_all( '/<img[^>]+>/i', $content, $matches ) ) {
    285         foreach ( $matches[0] as $img_html ) {
    286             $has_badge = false;
    287 
    288             // Kontrola přes ID obrázku zapsané ve třídě wp-image-XYZ
    289             if ( preg_match( '/wp-image-([0-9]+)/', $img_html, $id_match ) ) {
    290                 $attachment_id = intval( $id_match[1] );
    291                 if ( get_post_meta( $attachment_id, '_is_ai_generated', true ) === '1' ) {
    292                     $has_badge = true;
    293                 }
    294             }
    295 
    296             if ( $has_badge ) {
    297                 $modified_img = originmark_inject_badge( $img_html );
    298                 $content = str_replace( $img_html, $modified_img, $content );
    299             }
    300         }
    301     }
    302     return $content;
     389    return preg_replace_callback(
     390        '/<picture\b[^>]*>.*?<\/picture>|<img\b[^>]*>/is',
     391        'originmark_maybe_wrap_image',
     392        $content
     393    ) ?? $content;
    303394}
    304395add_filter( 'the_content', 'originmark_filter_content_images', 20 );
  • originmark-ai/trunk/readme.txt

    r3661053 r3787850  
    55Requires at least: 5.8
    66Tested up to: 7.0
    7 Requires PHP: 7.4
    8 Stable tag: 1.0
     7Requires PHP: 8.1
     8Stable tag: 1.1.0
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    6464== Changelog ==
    6565
     66= 1.1.0 =
     67* Fix: an image used more than once in a post no longer gets a duplicate badge.
     68* Fix: badge wrapper no longer breaks <picture> elements, paragraphs or left/right/center alignment.
     69* Fix: the "AI Generated" checkbox is shown only for images.
     70* Security: saving the checkbox now relies only on the core attachment save flow with a capability check.
     71* Change: the AI flag is stored under a prefixed meta key; existing data is migrated automatically.
     72* Requires PHP 8.1 or newer.
     73
    6674= 1.0 =
    6775* Initial release.
    … …  
    6977== Upgrade Notice ==
    7078
     79= 1.1.0 =
     80Bug fixes for badge rendering. Requires PHP 8.1+. Existing AI flags are migrated automatically.
     81
    7182= 1.0 =
    7283Initial release.
Note: See TracChangeset for help on using the changeset viewer.