Plugin Directory

Changeset 3764831


Ignore:
Timestamp:
10/25/2026 03:31:21 PM (less than one hour ago)
Author:
stphnwlkr
Message:

Release media-bridge-for-etch 2.7.1

Location:
media-bridge-for-etch
Files:
4 added
24 edited
1 copied

Legend:

Unmodified
Added
Removed
  • media-bridge-for-etch/tags/2.7.1/assets/gallery-lightbox.js

    r3737815 r3764831  
     1/**
     2 * Original editable gallery/lightbox source for Uplink Media Bridge for Etch.
     3 * This file is handwritten, not generated or minified. No build step is required.
     4 * Source and third-party build information are documented in readme.txt.
     5 */
    16( function () {
    27        'use strict';
    … …  
    260265                document.body.appendChild( overlay );
    261266
    262                 active = { controlsTimer: null, controlsPlaying: false, keyboardControls: false, slideshow: false, playing: false, playbackTimer: null, slideshowFullscreen: false, interval: Math.max( 2000, Math.min( 30000, Number( gallery.dataset.uplinkMbeLightboxInterval ) || 5000 ) ), loop: gallery.dataset.uplinkMbeLightboxLoop !== 'false', startSlideshow: startSlideshowButton, play, slidePrevious, slideNext, exifWrap, exifButton, exifPanel, gallery, items, index, overlay, dialog, media, image, title, caption, info, counter, live, thumbnails, previous, next, close, fullscreenButton, zoomOut, zoomIn, zoomReset, resizeObserver: null };
     267                active = {
     268                        controlsTimer: null,
     269                        controlsPlaying: false,
     270                        keyboardControls: false,
     271                        slideshow: false,
     272                        playing: false,
     273                        playbackTimer: null,
     274                        slideshowFullscreen: false,
     275                        interval: Math.max( 2000, Math.min( 30000, Number( gallery.dataset.uplinkMbeLightboxInterval ) || 5000 ) ),
     276                        loop: gallery.dataset.uplinkMbeLightboxLoop !== 'false',
     277                        startSlideshow: startSlideshowButton,
     278                        play,
     279                        slidePrevious,
     280                        slideNext,
     281                        exifWrap,
     282                        exifButton,
     283                        exifPanel,
     284                        gallery,
     285                        items,
     286                        index,
     287                        overlay,
     288                        dialog,
     289                        media,
     290                        image,
     291                        title,
     292                        caption,
     293                        info,
     294                        counter,
     295                        live,
     296                        thumbnails,
     297                        previous,
     298                        next,
     299                        close,
     300                        fullscreenButton,
     301                        zoomOut,
     302                        zoomIn,
     303                        zoomReset,
     304                        resizeObserver: null,
     305                };
    263306                // Activity reveals controls without restarting the independent image timer.
    264307                for ( const type of [ 'pointermove', 'pointerdown' ] ) dialog.addEventListener( type, () => {
  • media-bridge-for-etch/tags/2.7.1/assets/image-conversion.css

    r3759083 r3764831  
    1010.uplink-mbe-conversion-card h4 {margin:0 0 14px!important;font-size:16px!important;line-height:1.3;color:inherit!important}
    1111.uplink-mbe-format-preview {min-width:0;overflow:hidden;border-radius:7px;background:var(--conversion-surface)}
    12 .uplink-mbe-format-preview>.uplink-mbe-preview-placeholder {display:block;width:100%;aspect-ratio:4/3;object-fit:contain;margin:0;opacity:.7}
     12.uplink-mbe-format-preview>.uplink-mbe-preview-placeholder {display:block;width:100%;aspect-ratio:4/3;object-fit:contain;margin:0}
    1313.uplink-mbe-format-stats {display:flex;justify-content:space-between;align-items:baseline;gap:8px;flex-wrap:wrap;min-height:24px;margin:14px 0;font-size:13px;font-variant-numeric:tabular-nums;color:var(--conversion-muted)}
    1414.uplink-mbe-format-stats strong {font-size:18px;font-weight:650;color:var(--conversion-text)}
  • media-bridge-for-etch/tags/2.7.1/assets/media-modal.js

    r3737066 r3764831  
    231231                        }
    232232                        handled = true;
    233                         attachment.off( 'change:uploading change:id', complete );
     233                        attachment.off( 'change:uploading change:id change sync', complete );
    234234                        refreshAfterUpload( frame, attachment );
    235235                };
    236236
    237                 attachment.on( 'change:uploading change:id', complete );
     237                // WordPress 7.2's browser pipeline clears uploading silently before its
     238                // final fetch. Also observe its sync and fallback model update.
     239                attachment.on( 'change:uploading change:id change sync', complete );
    238240                complete();
    239241        }
  • media-bridge-for-etch/tags/2.7.1/assets/native-collections.js

    r3759083 r3764831  
    99        let uploadCollection = 0;
    1010        const uploaders = new Set();
     11        const libraryViews = new Set();
     12        const completedUploads = new WeakSet();
     13        let uploadRefreshTimer = null;
     14        let uploadRefreshVersion = 0;
     15
     16        // Taxonomy-filtered WordPress queries do not observe the global upload queue.
     17        // Requery the current view after uploads finish, including Cimo-processed files.
     18        function scheduleLibraryRefresh() {
     19                window.clearTimeout( uploadRefreshTimer );
     20                uploadRefreshTimer = window.setTimeout( () => {
     21                        for ( const view of libraryViews ) {
     22                                if ( ! view.el?.isConnected ) { libraryViews.delete( view ); continue; }
     23                                if ( view.uplinkMbeNativeListMode ) continue;
     24                                view.collection?.props?.set( 'uplink_mbe_refresh', `${ Date.now() }-${ ++uploadRefreshVersion }` );
     25                                const panel = view.uplinkMbeNativeCollectionsPanel;
     26                                if ( panel?.isConnected ) refreshCollectionState( view, panel ).catch( () => {} );
     27                        }
     28                }, 150 );
     29        }
     30
     31        // WordPress 7.2 REST uploads do not send Plupload multipart parameters.
     32        // Capture the selected destination now and verify it through our nonce-protected
     33        // assignment endpoint once the final attachment model has completed uploading.
     34        function watchUpload( attachment ) {
     35                const destination = uploadCollection;
     36                const complete = async () => {
     37                        if ( ! attachment.get( 'id' ) || attachment.get( 'uploading' ) || completedUploads.has( attachment ) ) return;
     38                        completedUploads.add( attachment );
     39                        attachment.off( 'change sync', complete );
     40                        try {
     41                                if ( destination && config.canAssign ) await request( 'uplink_mbe_assign_media', {
     42                                        media_ids: JSON.stringify( [ Number( attachment.get( 'id' ) ) ] ), collection: destination, mode: 'add',
     43                                } );
     44                        } catch ( error ) {
     45                                window.uplinkMbeToast?.( error.message, 'error' );
     46                        } finally {
     47                                scheduleLibraryRefresh();
     48                        }
     49                };
     50                attachment.on( 'change sync', complete );
     51                complete();
     52        }
     53        wp.Uploader?.queue?.on( 'add', watchUpload );
     54        wp.Uploader?.queue?.each( watchUpload );
     55        wp.media.model?.Attachments?.all?.on( 'destroy', scheduleLibraryRefresh );
     56        $( document ).ajaxSuccess( ( event, xhr, settings, response ) => {
     57                const action = typeof settings.data === 'string' ? new URLSearchParams( settings.data ).get( 'action' ) : settings.data?.action;
     58                if ( [ 'save-attachment-compat', 'save-attachment', 'delete-post', 'delete-attachment' ].includes( action ) && response?.success !== false ) scheduleLibraryRefresh();
     59        } );
    1160        const childrenByParent = new Map();
    1261        const collapsedCollections = new Set();
    … …  
    886935                        return;
    887936                }
     937                libraryViews.add( view );
    888938                if ( ! view.el.isConnected ) {
    889939                        if ( ! view.uplinkMbeNativeCollectionsPending ) {
    … …  
    12011251                if ( window.wpUploaderInit?.multipart_params ) {
    12021252                        window.wpUploaderInit.multipart_params.uplink_mbe_collection = uploadCollection;
     1253                        window.wpUploaderInit.multipart_params.uplink_mbe_collection_nonce = config.collectionNonce;
    12031254                }
    12041255                if ( window.uploader?.settings?.multipart_params ) {
    12051256                        window.uploader.settings.multipart_params.uplink_mbe_collection = uploadCollection;
    1206                 }
    1207                 uploaders.forEach( ( uploader ) => uploader.param?.( 'uplink_mbe_collection', uploadCollection ) );
     1257                        window.uploader.settings.multipart_params.uplink_mbe_collection_nonce = config.collectionNonce;
     1258                }
     1259                uploaders.forEach( ( uploader ) => {
     1260                        uploader.param?.( 'uplink_mbe_collection', uploadCollection );
     1261                        uploader.param?.( 'uplink_mbe_collection_nonce', config.collectionNonce );
     1262                } );
    12081263        }
    12091264
    … …  
    13011356                        uploaders.add( this );
    13021357                        this.param?.( 'uplink_mbe_collection', uploadCollection );
    1303                         return originalUploaderInit.apply( this, arguments );
     1358                        this.param?.( 'uplink_mbe_collection_nonce', config.collectionNonce );
     1359                        const result = originalUploaderInit.apply( this, arguments );
     1360                        this.uploader?.bind?.( 'FileUploaded', scheduleLibraryRefresh );
     1361                        this.uploader?.bind?.( 'UploadComplete', scheduleLibraryRefresh );
     1362                        return result;
    13041363                };
    13051364        }
  • media-bridge-for-etch/tags/2.7.1/includes/class-file-replacement.php

    r3759083 r3764831  
    2020        public function assets(): void {
    2121                if ( empty( Plugin::settings()['file_replacement'] ) || ! current_user_can( 'upload_files' ) ) return;
     22                if ( is_admin() ) {
     23                        $screen = get_current_screen();
     24                        if ( ! $screen || ( ! in_array( $screen->base, array( 'upload', 'media', 'post' ), true ) && 'media_page_etch-collections' !== $screen->id ) ) return;
     25                } elseif ( 'magic' !== sanitize_key( wp_unslash( $_GET['etch'] ?? '' ) ) ) return; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only builder context.
    2226                Upload_Workspace::assets();
    2327                wp_enqueue_style( 'uplink-mbe-file-replacement', UPLINK_MBE_URL . 'assets/file-replacement.css', array(), UPLINK_MBE_ASSET_VERSION );
  • media-bridge-for-etch/tags/2.7.1/includes/class-image-conversion.php

    r3759083 r3764831  
    229229                        if ( ! $info || $info['mime'] !== self::FORMATS[ $format ] || $info[0] !== $expected['width'] || $info[1] !== $expected['height'] ) self::fail( __( 'Converted dimensions or format do not match the original image size.', 'media-bridge-for-etch' ) );
    230230                }
    231                 $temp = wp_tempnam( 'mbe-conversion' );
    232                 // phpcs:ignore Generic.PHP.ForbiddenFunctions.Found -- Upload origin, actual MIME and dimensions were validated above; keep staging outside public uploads.
    233                 if ( ! $temp || ! move_uploaded_file( $path, $temp ) ) self::fail( __( 'Could not stage the converted file.', 'media-bridge-for-etch' ) );
     231                if ( ! function_exists( 'wp_handle_upload' ) ) require_once ABSPATH . 'wp-admin/includes/file.php';
     232                // Keep uncommitted images outside public uploads while retaining WordPress upload checks.
     233                $staging_dir = static function ( $dirs ) {
     234                        $dirs['path'] = untrailingslashit( get_temp_dir() );
     235                        $dirs['basedir'] = $dirs['path'];
     236                        $dirs['subdir'] = '';
     237                        $dirs['url'] = '';
     238                        $dirs['baseurl'] = '';
     239                        $dirs['error'] = false;
     240                        return $dirs;
     241                };
     242                $upload['name'] = 'mbe-conversion-' . wp_generate_uuid4() . '.' . $format;
     243                $upload['type'] = self::FORMATS[ $format ];
     244                // WordPress cannot read JPEG XL dimensions yet. Accept only this staged file,
     245                // whose signature was checked above and whose real MIME is verified by fileinfo.
     246                $jxl_type = static function ( $checked, $file, $filename ) use ( $path, $format ) {
     247                        if ( 'jxl' === $format && $file === $path && 'jxl' === strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ) && class_exists( '\finfo' ) && 'image/jxl' === ( new \finfo( FILEINFO_MIME_TYPE ) )->file( $file ) ) {
     248                                return array( 'ext' => 'jxl', 'type' => 'image/jxl', 'proper_filename' => false );
     249                        }
     250                        return $checked;
     251                };
     252                add_filter( 'upload_dir', $staging_dir );
     253                if ( 'jxl' === $format ) add_filter( 'wp_check_filetype_and_ext', $jxl_type, 10, 3 );
     254                try {
     255                        $handled = wp_handle_upload( $upload, array( 'test_form' => false, 'mimes' => array( $format => self::FORMATS[ $format ] ) ) );
     256                } finally {
     257                        remove_filter( 'upload_dir', $staging_dir );
     258                        if ( 'jxl' === $format ) remove_filter( 'wp_check_filetype_and_ext', $jxl_type, 10 );
     259                }
     260                if ( ! empty( $handled['error'] ) || empty( $handled['file'] ) ) self::fail( __( 'Could not stage the converted file.', 'media-bridge-for-etch' ) );
     261                $temp = $handled['file'];
    234262                if ( isset( $job['staged'][ $key ] ) ) wp_delete_file( $job['staged'][ $key ] );
    235263                $job['staged'][ $key ] = $temp; $job['format'] = $format; $job['quality'] = $quality;
  • media-bridge-for-etch/tags/2.7.1/includes/class-image-references.php

    r3757535 r3764831  
    1717                return $out;
    1818        }
    19         private static function objects( $value ): bool {
    20                 if ( is_object( $value ) ) return true;
    21                 if ( is_array( $value ) ) foreach ( $value as $item ) if ( self::objects( $item ) ) return true;
    22                 return false;
     19        /** Bound the entire traversal, including nested serialized strings and cycles. */
     20        public static function replace( string $raw, array $map ): string {
     21                if ( ! $map ) return $raw;
     22                $remaining = 10000;
     23                $safe = true;
     24                $result = self::rewrite( $raw, $map, 0, $remaining, $safe );
     25                return $safe ? $result : $raw;
    2326        }
    24         public static function replace( string $raw, array $map ): string {
     27        private static function rewrite( string $raw, array $map, int $depth, int &$remaining, bool &$safe ): string {
     28                if ( ! $safe || $depth > 64 || --$remaining < 0 ) { $safe = false; return $raw; }
    2529                if ( is_serialized( $raw ) ) {
    26                         // Never instantiate classes from database values while rewriting references.
    27                         $value = @unserialize( $raw, array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Class creation is explicitly disabled.
    28                         if ( self::objects( $value ) ) return $raw;
    29                         $walk = static function ( $item ) use ( &$walk, $map ) {
    30                                 if ( is_string( $item ) ) return self::replace( $item, $map );
    31                                 if ( is_array( $item ) ) foreach ( $item as $key => $child ) $item[ $key ] = $walk( $child );
    32                                 return $item;
    33                         };
    34                         $new = $walk( $value );
    35                         return $new === $value ? $raw : serialize( $new ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Preserve the existing database serialization format.
     30                        // Do not instantiate objects. The traversal below also rejects objects and cycles.
     31                        $value = @unserialize( $raw, array( 'allowed_classes' => false, 'max_depth' => 64 ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Bounded decoding with class creation disabled.
     32                        if ( false === $value && 'b:0;' !== $raw ) { $safe = false; return $raw; }
     33                        $changed = false;
     34                        $new = self::walk( $value, $map, $depth + 1, $remaining, $safe, $changed );
     35                        return $safe && $changed ? serialize( $new ) : $raw; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Preserve the existing database serialization format.
    3636                }
    37                 if ( ! $map ) return $raw;
    38                 $pattern = '~(?<![a-zA-Z0-9:])(?:' . implode( '|', array_map( static fn( $url ) => preg_quote( $url, '~' ), array_keys( $map ) ) ) . ')(?![a-zA-Z0-9._%+\x7e-])~';
     37                $pattern = '~(?<![a-zA-Z0-9:])(?:' . implode( '|', array_map( static fn( $url ) => preg_quote( $url, '~' ), array_keys( $map ) ) ) . ')(?![a-zA-Z0-9._%+\\x7e-])~';
    3938                return preg_replace_callback( $pattern, static fn( $m ) => $map[ $m[0] ], $raw ) ?? $raw;
     39        }
     40        private static function walk( $value, array $map, int $depth, int &$remaining, bool &$safe, bool &$changed ) {
     41                if ( ! $safe || $depth > 64 || --$remaining < 0 || is_object( $value ) ) { $safe = false; return $value; }
     42                if ( is_string( $value ) ) {
     43                        $new = self::rewrite( $value, $map, $depth + 1, $remaining, $safe );
     44                        $changed = $changed || $new !== $value;
     45                        return $new;
     46                }
     47                if ( is_array( $value ) ) {
     48                        foreach ( $value as $key => $child ) {
     49                                $value[ $key ] = self::walk( $child, $map, $depth + 1, $remaining, $safe, $changed );
     50                                if ( ! $safe ) break;
     51                        }
     52                }
     53                return $value;
    4054        }
    4155        /** Must run inside the conversion transaction. Journal supports nontransactional rollback. */
  • media-bridge-for-etch/tags/2.7.1/includes/class-native-media-library.php

    r3759083 r3764831  
    3333                add_action( 'wp_enqueue_media', array( $this, 'media_modal_assets' ) );
    3434                add_action( 'wp_enqueue_media', array( $this, 'native_collection_assets' ), 20 );
     35                add_action( 'wp_enqueue_media', array( Upload_Workspace::class, 'native_assets' ), 30 );
     36                add_action( 'admin_enqueue_scripts', array( Upload_Workspace::class, 'native_assets' ) );
    3537                add_filter( 'ajax_query_attachments_args', array( $this, 'filter_native_collection_query' ) );
    3638                add_action( 'pre_get_posts', array( $this, 'filter_native_media_list_query' ) );
    … …  
    257259                                'ajaxUrl'     => admin_url( 'admin-ajax.php' ),
    258260                                'nonce'       => wp_create_nonce( self::NONCE ),
     261                                'collectionNonce' => wp_create_nonce( 'uplink_mbe_native_collection' ),
    259262                                'collections' => $this->native_collection_data(),
    260263                                'counts'      => $this->native_collection_counts(),
    … …  
    434437
    435438                $html     = '<input type="hidden" name="attachments[' . (int) $attachment->ID . '][uplink_mbe_collections_present]" value="1">';
     439                $html    .= wp_nonce_field( 'uplink_mbe_native_collection', 'attachments[' . (int) $attachment->ID . '][uplink_mbe_collections_nonce]', false, false );
    436440                $html    .= '<div class="uplink-mbe-native-assignment-list">';
    437441                $rendered = array();
    … …  
    492496                        return $post;
    493497                }
     498                $nonce = $attachment['uplink_mbe_collections_nonce'] ?? '';
     499                if ( ! is_string( $nonce ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $nonce ) ), 'uplink_mbe_native_collection' ) || ! current_user_can( 'upload_files' ) ) return $post;
    494500                $attachment_id = absint( $post['ID'] ?? 0 );
    495501                $taxonomy      = get_taxonomy( $this->etch->taxonomy() );
    496                 if ( ! $attachment_id || ! $taxonomy || ! current_user_can( 'edit_post', $attachment_id ) || ! current_user_can( $taxonomy->cap->assign_terms ) ) {
     502                if ( ! $attachment_id || 'attachment' !== get_post_type( $attachment_id ) || ! $taxonomy || ! current_user_can( 'edit_post', $attachment_id ) || ! current_user_can( $taxonomy->cap->assign_terms ) ) {
    497503                        return $post;
    498504                }
    … …  
    513519                        return;
    514520                }
    515                 // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- WordPress verifies the upload request before creating the attachment.
     521                $nonce = isset( $_REQUEST['uplink_mbe_collection_nonce'] ) && is_string( $_REQUEST['uplink_mbe_collection_nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['uplink_mbe_collection_nonce'] ) ) : '';
     522                if ( ! wp_verify_nonce( $nonce, 'uplink_mbe_native_collection' ) || ! current_user_can( 'upload_files' ) || ! current_user_can( 'edit_post', $attachment_id ) || 'attachment' !== get_post_type( $attachment_id ) ) return;
    516523                $collection_id = absint( $_REQUEST['uplink_mbe_collection'] ?? 0 );
    517524                $taxonomy      = get_taxonomy( $this->etch->taxonomy() );
    … …  
    25582565                }
    25592566                $attachment_id = absint( $matches[1] );
     2567                if ( 'attachment' !== get_post_type( $attachment_id ) || ! current_user_can( 'upload_files' ) || ! current_user_can( 'edit_post', $attachment_id ) ) return $result;
    25602568                $file          = get_attached_file( $attachment_id );
    25612569                if ( $attachment_id && $file && is_readable( $file ) ) {
    … …  
    25702578                }
    25712579                $attachment_id = absint( $matches[1] );
     2580                if ( 'attachment' !== get_post_type( $attachment_id ) || ! current_user_can( 'upload_files' ) || ! current_user_can( 'edit_post', $attachment_id ) ) return $response;
    25722581                $original_size = absint( $this->etch_optimization_before[ $attachment_id ] ?? 0 );
    25732582                unset( $this->etch_optimization_before[ $attachment_id ] );
  • media-bridge-for-etch/tags/2.7.1/includes/class-plugin.php

    r3759083 r3764831  
    2222
    2323        public static function bootstrap(): void {
    24                 add_action( 'admin_enqueue_scripts', array( self::class, 'toast_assets' ), 1 );
    25                 add_action( 'wp_enqueue_scripts', array( self::class, 'toast_assets' ), 1 );
    2624                if ( ! self::etch_is_active() ) {
    2725                        if ( is_admin() ) {
    … …  
    3028                        return;
    3129                }
     30                add_action( 'admin_enqueue_scripts', array( self::class, 'toast_assets' ), 1 );
     31                add_action( 'wp_enqueue_scripts', array( self::class, 'toast_assets' ), 1 );
    3232                self::instance();
    3333        }
    3434
    3535        public static function toast_assets(): void {
     36                if ( is_admin() ) {
     37                        $screen = get_current_screen();
     38                        if ( ! $screen || ( ! in_array( $screen->base, array( 'upload', 'media', 'post' ), true ) && ! in_array( $screen->id, array( 'media_page_etch-collections', 'media_page_media-bridge-for-etch', 'settings_page_media-bridge-for-etch-network' ), true ) ) ) return;
     39                }
    3640                // Frontend controls only load in the authenticated Etch builder.
    3741                if ( ! is_admin() && ( ! current_user_can( 'upload_files' ) || 'magic' !== sanitize_key( wp_unslash( $_GET['etch'] ?? '' ) ) ) ) return; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    … …  
    4246
    4347        public static function activate(): void {
    44                 if ( ! self::etch_is_active() ) {
    45                         wp_die(
    46                                 esc_html__( 'Uplink Media Bridge for Etch requires the Etch plugin to be installed and active.', 'media-bridge-for-etch' ),
    47                                 esc_html__( 'Etch is required', 'media-bridge-for-etch' ),
    48                                 array( 'back_link' => true )
    49                         );
    50                 }
     48                // Activation stays silent and safe; integrations boot only while Etch is active.
    5149                self::migrate_options();
    5250        }
    … …  
    5755                }
    5856                ?>
    59                 <div hidden data-uplink-mbe-toast="error"><p><?php esc_html_e( 'Uplink Media Bridge for Etch is inactive because the required Etch plugin is not active.', 'media-bridge-for-etch' ); ?></p></div>
     57                <div class="notice notice-warning"><p><?php esc_html_e( 'Uplink Media Bridge for Etch needs Etch to enable its features. Install and activate Etch to continue.', 'media-bridge-for-etch' ); ?></p></div>
    6058                <?php
    6159        }
  • media-bridge-for-etch/tags/2.7.1/includes/class-upload-workspace.php

    r3759083 r3764831  
    2323                }
    2424                return $locations;
     25        }
     26
     27        /** Keep original uploads in the WordPress library, independently of collection controls. */
     28        public static function native_assets(): void {
     29                $settings = Plugin::settings();
     30                if ( ! current_user_can( 'upload_files' ) || ! defined( 'CIMO_FILE' ) || ( 'native' === $settings['provider'] && ! empty( $settings['default_media_screen'] ) ) ) return;
     31                $screen = get_current_screen();
     32                if ( ! $screen || ! in_array( $screen->base, array( 'upload', 'media', 'post' ), true ) ) return;
     33                wp_enqueue_style( 'uplink-mbe-native-upload', UPLINK_MBE_URL . 'assets/native-upload.css', array(), UPLINK_MBE_ASSET_VERSION );
     34                global $pagenow;
     35                $dependencies = 'media-new.php' === $pagenow ? array( 'jquery', 'plupload-handlers' ) : array( 'media-views', 'jquery' );
     36                wp_enqueue_script( 'uplink-mbe-native-upload', UPLINK_MBE_URL . 'assets/native-upload.js', $dependencies, UPLINK_MBE_ASSET_VERSION, true );
     37                wp_localize_script( 'uplink-mbe-native-upload', 'uplinkMbeNativeUpload', array(
     38                        'keepOriginals' => __( 'Keep originals', 'media-bridge-for-etch' ),
     39                        'help' => __( 'Skip Cimo compression for this upload to preserve the original file and embedded EXIF. WordPress may still generate image sizes. Resets after the upload finishes or the dialog closes.', 'media-bridge-for-etch' ),
     40                ) );
    2541        }
    2642
  • media-bridge-for-etch/tags/2.7.1/media-bridge-for-etch.php

    r3759083 r3764831  
    33 * Plugin Name: Uplink Media Bridge for Etch
    44 * Description: Manage Etch media collections in WordPress, with optional Wicked Folders or HappyFiles synchronization.
    5  * Version: 2.7.0
     5 * Version: 2.7.1
    66 * Plugin URI: https://uplinkplugins.com/articles/uplink-media-bridge-for-etch-version-2/
    77 * Author: Stephen Walker
    … …  
    1616}
    1717
    18 define( 'UPLINK_MBE_VERSION', '2.7.0' );
    19 define( 'UPLINK_MBE_ASSET_VERSION', '2.7.0.27' );
     18define( 'UPLINK_MBE_VERSION', '2.7.1' );
     19define( 'UPLINK_MBE_ASSET_VERSION', '2.7.1.3' );
    2020define( 'UPLINK_MBE_FILE', __FILE__ );
    2121define( 'UPLINK_MBE_PATH', plugin_dir_path( __FILE__ ) );
  • media-bridge-for-etch/tags/2.7.1/readme.txt

    r3759083 r3764831  
    55Tested up to: 7.1
    66Requires PHP: 8.3
    7 Stable tag: 2.7.0
     7Stable tag: 2.7.1
    88License: GPL-3.0-or-later
    99
    … …  
    263263
    264264== Changelog ==
     265
     266= 2.7.1 =
     267* Adds Keep originals to native WordPress uploads to bypass Cimo and preserve embedded EXIF, with automatic reset after the upload or closing the dialog.
     268* Repairs collection assignment and refresh after WordPress 7.2 REST uploads.
     269* Adds explicit nonces and attachment permissions to native collection saves and upload assignments.
     270* Uses WordPress upload validation for conversion staging and bounds serialized-reference traversal.
     271* Allows silent activation without Etch; integration features wait for Etch and show an administrator notice.
     272* Limits notification and replacement assets to the media/editor screens that use them.
     273* Documents editable first-party source and the steps to rebuild the plugin package.
     274
    265275
    266276= 2.7.0 =
    … …  
    748758Bundled image encoders run locally. Codec source, build and license references are in assets/vendor/jsquash/SOURCES.md. Leaflet source and license are in assets/vendor/leaflet. This release uses GPL-3.0-or-later for compatibility with the bundled Apache-2.0 codec wrappers. Third-party libraries retain their own licenses.
    749759
     760== Source code and builds ==
     761
     762The ZIP includes editable PHP, JavaScript and CSS for all first-party code. assets/gallery-lightbox.js is the original handwritten source, not a generated or minified bundle. Edit it directly; no npm, webpack, transpiler or compilation step is required. First-party assets are served directly as shipped.
     763
     764Development repository: https://github.com/stphnwlkr/media-bridge-for-etch
     765
     766To make an installable ZIP, place media-bridge-for-etch.php, uninstall.php, readme.txt, LICENSE and the includes, assets and blocks directories inside a media-bridge-for-etch folder and ZIP that folder. Development tests, reports and old builds are excluded from the runtime package. The development checkout also provides python3 tools/build.py for this packaging step.
     767
     768Bundled third-party assets retain their upstream source, build and license references in the sections below and their assets/vendor SOURCES.md files. Browser Image Compression includes readable original sources in its source map. Leaflet includes leaflet-src.js.
     769
    750770== Third-party licenses ==
    751771
  • media-bridge-for-etch/trunk/assets/gallery-lightbox.js

    r3737815 r3764831  
     1/**
     2 * Original editable gallery/lightbox source for Uplink Media Bridge for Etch.
     3 * This file is handwritten, not generated or minified. No build step is required.
     4 * Source and third-party build information are documented in readme.txt.
     5 */
    16( function () {
    27        'use strict';
    … …  
    260265                document.body.appendChild( overlay );
    261266
    262                 active = { controlsTimer: null, controlsPlaying: false, keyboardControls: false, slideshow: false, playing: false, playbackTimer: null, slideshowFullscreen: false, interval: Math.max( 2000, Math.min( 30000, Number( gallery.dataset.uplinkMbeLightboxInterval ) || 5000 ) ), loop: gallery.dataset.uplinkMbeLightboxLoop !== 'false', startSlideshow: startSlideshowButton, play, slidePrevious, slideNext, exifWrap, exifButton, exifPanel, gallery, items, index, overlay, dialog, media, image, title, caption, info, counter, live, thumbnails, previous, next, close, fullscreenButton, zoomOut, zoomIn, zoomReset, resizeObserver: null };
     267                active = {
     268                        controlsTimer: null,
     269                        controlsPlaying: false,
     270                        keyboardControls: false,
     271                        slideshow: false,
     272                        playing: false,
     273                        playbackTimer: null,
     274                        slideshowFullscreen: false,
     275                        interval: Math.max( 2000, Math.min( 30000, Number( gallery.dataset.uplinkMbeLightboxInterval ) || 5000 ) ),
     276                        loop: gallery.dataset.uplinkMbeLightboxLoop !== 'false',
     277                        startSlideshow: startSlideshowButton,
     278                        play,
     279                        slidePrevious,
     280                        slideNext,
     281                        exifWrap,
     282                        exifButton,
     283                        exifPanel,
     284                        gallery,
     285                        items,
     286                        index,
     287                        overlay,
     288                        dialog,
     289                        media,
     290                        image,
     291                        title,
     292                        caption,
     293                        info,
     294                        counter,
     295                        live,
     296                        thumbnails,
     297                        previous,
     298                        next,
     299                        close,
     300                        fullscreenButton,
     301                        zoomOut,
     302                        zoomIn,
     303                        zoomReset,
     304                        resizeObserver: null,
     305                };
    263306                // Activity reveals controls without restarting the independent image timer.
    264307                for ( const type of [ 'pointermove', 'pointerdown' ] ) dialog.addEventListener( type, () => {
  • media-bridge-for-etch/trunk/assets/image-conversion.css

    r3759083 r3764831  
    1010.uplink-mbe-conversion-card h4 {margin:0 0 14px!important;font-size:16px!important;line-height:1.3;color:inherit!important}
    1111.uplink-mbe-format-preview {min-width:0;overflow:hidden;border-radius:7px;background:var(--conversion-surface)}
    12 .uplink-mbe-format-preview>.uplink-mbe-preview-placeholder {display:block;width:100%;aspect-ratio:4/3;object-fit:contain;margin:0;opacity:.7}
     12.uplink-mbe-format-preview>.uplink-mbe-preview-placeholder {display:block;width:100%;aspect-ratio:4/3;object-fit:contain;margin:0}
    1313.uplink-mbe-format-stats {display:flex;justify-content:space-between;align-items:baseline;gap:8px;flex-wrap:wrap;min-height:24px;margin:14px 0;font-size:13px;font-variant-numeric:tabular-nums;color:var(--conversion-muted)}
    1414.uplink-mbe-format-stats strong {font-size:18px;font-weight:650;color:var(--conversion-text)}
  • media-bridge-for-etch/trunk/assets/media-modal.js

    r3737066 r3764831  
    231231                        }
    232232                        handled = true;
    233                         attachment.off( 'change:uploading change:id', complete );
     233                        attachment.off( 'change:uploading change:id change sync', complete );
    234234                        refreshAfterUpload( frame, attachment );
    235235                };
    236236
    237                 attachment.on( 'change:uploading change:id', complete );
     237                // WordPress 7.2's browser pipeline clears uploading silently before its
     238                // final fetch. Also observe its sync and fallback model update.
     239                attachment.on( 'change:uploading change:id change sync', complete );
    238240                complete();
    239241        }
  • media-bridge-for-etch/trunk/assets/native-collections.js

    r3759083 r3764831  
    99        let uploadCollection = 0;
    1010        const uploaders = new Set();
     11        const libraryViews = new Set();
     12        const completedUploads = new WeakSet();
     13        let uploadRefreshTimer = null;
     14        let uploadRefreshVersion = 0;
     15
     16        // Taxonomy-filtered WordPress queries do not observe the global upload queue.
     17        // Requery the current view after uploads finish, including Cimo-processed files.
     18        function scheduleLibraryRefresh() {
     19                window.clearTimeout( uploadRefreshTimer );
     20                uploadRefreshTimer = window.setTimeout( () => {
     21                        for ( const view of libraryViews ) {
     22                                if ( ! view.el?.isConnected ) { libraryViews.delete( view ); continue; }
     23                                if ( view.uplinkMbeNativeListMode ) continue;
     24                                view.collection?.props?.set( 'uplink_mbe_refresh', `${ Date.now() }-${ ++uploadRefreshVersion }` );
     25                                const panel = view.uplinkMbeNativeCollectionsPanel;
     26                                if ( panel?.isConnected ) refreshCollectionState( view, panel ).catch( () => {} );
     27                        }
     28                }, 150 );
     29        }
     30
     31        // WordPress 7.2 REST uploads do not send Plupload multipart parameters.
     32        // Capture the selected destination now and verify it through our nonce-protected
     33        // assignment endpoint once the final attachment model has completed uploading.
     34        function watchUpload( attachment ) {
     35                const destination = uploadCollection;
     36                const complete = async () => {
     37                        if ( ! attachment.get( 'id' ) || attachment.get( 'uploading' ) || completedUploads.has( attachment ) ) return;
     38                        completedUploads.add( attachment );
     39                        attachment.off( 'change sync', complete );
     40                        try {
     41                                if ( destination && config.canAssign ) await request( 'uplink_mbe_assign_media', {
     42                                        media_ids: JSON.stringify( [ Number( attachment.get( 'id' ) ) ] ), collection: destination, mode: 'add',
     43                                } );
     44                        } catch ( error ) {
     45                                window.uplinkMbeToast?.( error.message, 'error' );
     46                        } finally {
     47                                scheduleLibraryRefresh();
     48                        }
     49                };
     50                attachment.on( 'change sync', complete );
     51                complete();
     52        }
     53        wp.Uploader?.queue?.on( 'add', watchUpload );
     54        wp.Uploader?.queue?.each( watchUpload );
     55        wp.media.model?.Attachments?.all?.on( 'destroy', scheduleLibraryRefresh );
     56        $( document ).ajaxSuccess( ( event, xhr, settings, response ) => {
     57                const action = typeof settings.data === 'string' ? new URLSearchParams( settings.data ).get( 'action' ) : settings.data?.action;
     58                if ( [ 'save-attachment-compat', 'save-attachment', 'delete-post', 'delete-attachment' ].includes( action ) && response?.success !== false ) scheduleLibraryRefresh();
     59        } );
    1160        const childrenByParent = new Map();
    1261        const collapsedCollections = new Set();
    … …  
    886935                        return;
    887936                }
     937                libraryViews.add( view );
    888938                if ( ! view.el.isConnected ) {
    889939                        if ( ! view.uplinkMbeNativeCollectionsPending ) {
    … …  
    12011251                if ( window.wpUploaderInit?.multipart_params ) {
    12021252                        window.wpUploaderInit.multipart_params.uplink_mbe_collection = uploadCollection;
     1253                        window.wpUploaderInit.multipart_params.uplink_mbe_collection_nonce = config.collectionNonce;
    12031254                }
    12041255                if ( window.uploader?.settings?.multipart_params ) {
    12051256                        window.uploader.settings.multipart_params.uplink_mbe_collection = uploadCollection;
    1206                 }
    1207                 uploaders.forEach( ( uploader ) => uploader.param?.( 'uplink_mbe_collection', uploadCollection ) );
     1257                        window.uploader.settings.multipart_params.uplink_mbe_collection_nonce = config.collectionNonce;
     1258                }
     1259                uploaders.forEach( ( uploader ) => {
     1260                        uploader.param?.( 'uplink_mbe_collection', uploadCollection );
     1261                        uploader.param?.( 'uplink_mbe_collection_nonce', config.collectionNonce );
     1262                } );
    12081263        }
    12091264
    … …  
    13011356                        uploaders.add( this );
    13021357                        this.param?.( 'uplink_mbe_collection', uploadCollection );
    1303                         return originalUploaderInit.apply( this, arguments );
     1358                        this.param?.( 'uplink_mbe_collection_nonce', config.collectionNonce );
     1359                        const result = originalUploaderInit.apply( this, arguments );
     1360                        this.uploader?.bind?.( 'FileUploaded', scheduleLibraryRefresh );
     1361                        this.uploader?.bind?.( 'UploadComplete', scheduleLibraryRefresh );
     1362                        return result;
    13041363                };
    13051364        }
  • media-bridge-for-etch/trunk/includes/class-file-replacement.php

    r3759083 r3764831  
    2020        public function assets(): void {
    2121                if ( empty( Plugin::settings()['file_replacement'] ) || ! current_user_can( 'upload_files' ) ) return;
     22                if ( is_admin() ) {
     23                        $screen = get_current_screen();
     24                        if ( ! $screen || ( ! in_array( $screen->base, array( 'upload', 'media', 'post' ), true ) && 'media_page_etch-collections' !== $screen->id ) ) return;
     25                } elseif ( 'magic' !== sanitize_key( wp_unslash( $_GET['etch'] ?? '' ) ) ) return; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only builder context.
    2226                Upload_Workspace::assets();
    2327                wp_enqueue_style( 'uplink-mbe-file-replacement', UPLINK_MBE_URL . 'assets/file-replacement.css', array(), UPLINK_MBE_ASSET_VERSION );
  • media-bridge-for-etch/trunk/includes/class-image-conversion.php

    r3759083 r3764831  
    229229                        if ( ! $info || $info['mime'] !== self::FORMATS[ $format ] || $info[0] !== $expected['width'] || $info[1] !== $expected['height'] ) self::fail( __( 'Converted dimensions or format do not match the original image size.', 'media-bridge-for-etch' ) );
    230230                }
    231                 $temp = wp_tempnam( 'mbe-conversion' );
    232                 // phpcs:ignore Generic.PHP.ForbiddenFunctions.Found -- Upload origin, actual MIME and dimensions were validated above; keep staging outside public uploads.
    233                 if ( ! $temp || ! move_uploaded_file( $path, $temp ) ) self::fail( __( 'Could not stage the converted file.', 'media-bridge-for-etch' ) );
     231                if ( ! function_exists( 'wp_handle_upload' ) ) require_once ABSPATH . 'wp-admin/includes/file.php';
     232                // Keep uncommitted images outside public uploads while retaining WordPress upload checks.
     233                $staging_dir = static function ( $dirs ) {
     234                        $dirs['path'] = untrailingslashit( get_temp_dir() );
     235                        $dirs['basedir'] = $dirs['path'];
     236                        $dirs['subdir'] = '';
     237                        $dirs['url'] = '';
     238                        $dirs['baseurl'] = '';
     239                        $dirs['error'] = false;
     240                        return $dirs;
     241                };
     242                $upload['name'] = 'mbe-conversion-' . wp_generate_uuid4() . '.' . $format;
     243                $upload['type'] = self::FORMATS[ $format ];
     244                // WordPress cannot read JPEG XL dimensions yet. Accept only this staged file,
     245                // whose signature was checked above and whose real MIME is verified by fileinfo.
     246                $jxl_type = static function ( $checked, $file, $filename ) use ( $path, $format ) {
     247                        if ( 'jxl' === $format && $file === $path && 'jxl' === strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ) && class_exists( '\finfo' ) && 'image/jxl' === ( new \finfo( FILEINFO_MIME_TYPE ) )->file( $file ) ) {
     248                                return array( 'ext' => 'jxl', 'type' => 'image/jxl', 'proper_filename' => false );
     249                        }
     250                        return $checked;
     251                };
     252                add_filter( 'upload_dir', $staging_dir );
     253                if ( 'jxl' === $format ) add_filter( 'wp_check_filetype_and_ext', $jxl_type, 10, 3 );
     254                try {
     255                        $handled = wp_handle_upload( $upload, array( 'test_form' => false, 'mimes' => array( $format => self::FORMATS[ $format ] ) ) );
     256                } finally {
     257                        remove_filter( 'upload_dir', $staging_dir );
     258                        if ( 'jxl' === $format ) remove_filter( 'wp_check_filetype_and_ext', $jxl_type, 10 );
     259                }
     260                if ( ! empty( $handled['error'] ) || empty( $handled['file'] ) ) self::fail( __( 'Could not stage the converted file.', 'media-bridge-for-etch' ) );
     261                $temp = $handled['file'];
    234262                if ( isset( $job['staged'][ $key ] ) ) wp_delete_file( $job['staged'][ $key ] );
    235263                $job['staged'][ $key ] = $temp; $job['format'] = $format; $job['quality'] = $quality;
  • media-bridge-for-etch/trunk/includes/class-image-references.php

    r3757535 r3764831  
    1717                return $out;
    1818        }
    19         private static function objects( $value ): bool {
    20                 if ( is_object( $value ) ) return true;
    21                 if ( is_array( $value ) ) foreach ( $value as $item ) if ( self::objects( $item ) ) return true;
    22                 return false;
     19        /** Bound the entire traversal, including nested serialized strings and cycles. */
     20        public static function replace( string $raw, array $map ): string {
     21                if ( ! $map ) return $raw;
     22                $remaining = 10000;
     23                $safe = true;
     24                $result = self::rewrite( $raw, $map, 0, $remaining, $safe );
     25                return $safe ? $result : $raw;
    2326        }
    24         public static function replace( string $raw, array $map ): string {
     27        private static function rewrite( string $raw, array $map, int $depth, int &$remaining, bool &$safe ): string {
     28                if ( ! $safe || $depth > 64 || --$remaining < 0 ) { $safe = false; return $raw; }
    2529                if ( is_serialized( $raw ) ) {
    26                         // Never instantiate classes from database values while rewriting references.
    27                         $value = @unserialize( $raw, array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Class creation is explicitly disabled.
    28                         if ( self::objects( $value ) ) return $raw;
    29                         $walk = static function ( $item ) use ( &$walk, $map ) {
    30                                 if ( is_string( $item ) ) return self::replace( $item, $map );
    31                                 if ( is_array( $item ) ) foreach ( $item as $key => $child ) $item[ $key ] = $walk( $child );
    32                                 return $item;
    33                         };
    34                         $new = $walk( $value );
    35                         return $new === $value ? $raw : serialize( $new ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Preserve the existing database serialization format.
     30                        // Do not instantiate objects. The traversal below also rejects objects and cycles.
     31                        $value = @unserialize( $raw, array( 'allowed_classes' => false, 'max_depth' => 64 ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Bounded decoding with class creation disabled.
     32                        if ( false === $value && 'b:0;' !== $raw ) { $safe = false; return $raw; }
     33                        $changed = false;
     34                        $new = self::walk( $value, $map, $depth + 1, $remaining, $safe, $changed );
     35                        return $safe && $changed ? serialize( $new ) : $raw; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Preserve the existing database serialization format.
    3636                }
    37                 if ( ! $map ) return $raw;
    38                 $pattern = '~(?<![a-zA-Z0-9:])(?:' . implode( '|', array_map( static fn( $url ) => preg_quote( $url, '~' ), array_keys( $map ) ) ) . ')(?![a-zA-Z0-9._%+\x7e-])~';
     37                $pattern = '~(?<![a-zA-Z0-9:])(?:' . implode( '|', array_map( static fn( $url ) => preg_quote( $url, '~' ), array_keys( $map ) ) ) . ')(?![a-zA-Z0-9._%+\\x7e-])~';
    3938                return preg_replace_callback( $pattern, static fn( $m ) => $map[ $m[0] ], $raw ) ?? $raw;
     39        }
     40        private static function walk( $value, array $map, int $depth, int &$remaining, bool &$safe, bool &$changed ) {
     41                if ( ! $safe || $depth > 64 || --$remaining < 0 || is_object( $value ) ) { $safe = false; return $value; }
     42                if ( is_string( $value ) ) {
     43                        $new = self::rewrite( $value, $map, $depth + 1, $remaining, $safe );
     44                        $changed = $changed || $new !== $value;
     45                        return $new;
     46                }
     47                if ( is_array( $value ) ) {
     48                        foreach ( $value as $key => $child ) {
     49                                $value[ $key ] = self::walk( $child, $map, $depth + 1, $remaining, $safe, $changed );
     50                                if ( ! $safe ) break;
     51                        }
     52                }
     53                return $value;
    4054        }
    4155        /** Must run inside the conversion transaction. Journal supports nontransactional rollback. */
  • media-bridge-for-etch/trunk/includes/class-native-media-library.php

    r3759083 r3764831  
    3333                add_action( 'wp_enqueue_media', array( $this, 'media_modal_assets' ) );
    3434                add_action( 'wp_enqueue_media', array( $this, 'native_collection_assets' ), 20 );
     35                add_action( 'wp_enqueue_media', array( Upload_Workspace::class, 'native_assets' ), 30 );
     36                add_action( 'admin_enqueue_scripts', array( Upload_Workspace::class, 'native_assets' ) );
    3537                add_filter( 'ajax_query_attachments_args', array( $this, 'filter_native_collection_query' ) );
    3638                add_action( 'pre_get_posts', array( $this, 'filter_native_media_list_query' ) );
    … …  
    257259                                'ajaxUrl'     => admin_url( 'admin-ajax.php' ),
    258260                                'nonce'       => wp_create_nonce( self::NONCE ),
     261                                'collectionNonce' => wp_create_nonce( 'uplink_mbe_native_collection' ),
    259262                                'collections' => $this->native_collection_data(),
    260263                                'counts'      => $this->native_collection_counts(),
    … …  
    434437
    435438                $html     = '<input type="hidden" name="attachments[' . (int) $attachment->ID . '][uplink_mbe_collections_present]" value="1">';
     439                $html    .= wp_nonce_field( 'uplink_mbe_native_collection', 'attachments[' . (int) $attachment->ID . '][uplink_mbe_collections_nonce]', false, false );
    436440                $html    .= '<div class="uplink-mbe-native-assignment-list">';
    437441                $rendered = array();
    … …  
    492496                        return $post;
    493497                }
     498                $nonce = $attachment['uplink_mbe_collections_nonce'] ?? '';
     499                if ( ! is_string( $nonce ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $nonce ) ), 'uplink_mbe_native_collection' ) || ! current_user_can( 'upload_files' ) ) return $post;
    494500                $attachment_id = absint( $post['ID'] ?? 0 );
    495501                $taxonomy      = get_taxonomy( $this->etch->taxonomy() );
    496                 if ( ! $attachment_id || ! $taxonomy || ! current_user_can( 'edit_post', $attachment_id ) || ! current_user_can( $taxonomy->cap->assign_terms ) ) {
     502                if ( ! $attachment_id || 'attachment' !== get_post_type( $attachment_id ) || ! $taxonomy || ! current_user_can( 'edit_post', $attachment_id ) || ! current_user_can( $taxonomy->cap->assign_terms ) ) {
    497503                        return $post;
    498504                }
    … …  
    513519                        return;
    514520                }
    515                 // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- WordPress verifies the upload request before creating the attachment.
     521                $nonce = isset( $_REQUEST['uplink_mbe_collection_nonce'] ) && is_string( $_REQUEST['uplink_mbe_collection_nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['uplink_mbe_collection_nonce'] ) ) : '';
     522                if ( ! wp_verify_nonce( $nonce, 'uplink_mbe_native_collection' ) || ! current_user_can( 'upload_files' ) || ! current_user_can( 'edit_post', $attachment_id ) || 'attachment' !== get_post_type( $attachment_id ) ) return;
    516523                $collection_id = absint( $_REQUEST['uplink_mbe_collection'] ?? 0 );
    517524                $taxonomy      = get_taxonomy( $this->etch->taxonomy() );
    … …  
    25582565                }
    25592566                $attachment_id = absint( $matches[1] );
     2567                if ( 'attachment' !== get_post_type( $attachment_id ) || ! current_user_can( 'upload_files' ) || ! current_user_can( 'edit_post', $attachment_id ) ) return $result;
    25602568                $file          = get_attached_file( $attachment_id );
    25612569                if ( $attachment_id && $file && is_readable( $file ) ) {
    … …  
    25702578                }
    25712579                $attachment_id = absint( $matches[1] );
     2580                if ( 'attachment' !== get_post_type( $attachment_id ) || ! current_user_can( 'upload_files' ) || ! current_user_can( 'edit_post', $attachment_id ) ) return $response;
    25722581                $original_size = absint( $this->etch_optimization_before[ $attachment_id ] ?? 0 );
    25732582                unset( $this->etch_optimization_before[ $attachment_id ] );
  • media-bridge-for-etch/trunk/includes/class-plugin.php

    r3759083 r3764831  
    2222
    2323        public static function bootstrap(): void {
    24                 add_action( 'admin_enqueue_scripts', array( self::class, 'toast_assets' ), 1 );
    25                 add_action( 'wp_enqueue_scripts', array( self::class, 'toast_assets' ), 1 );
    2624                if ( ! self::etch_is_active() ) {
    2725                        if ( is_admin() ) {
    … …  
    3028                        return;
    3129                }
     30                add_action( 'admin_enqueue_scripts', array( self::class, 'toast_assets' ), 1 );
     31                add_action( 'wp_enqueue_scripts', array( self::class, 'toast_assets' ), 1 );
    3232                self::instance();
    3333        }
    3434
    3535        public static function toast_assets(): void {
     36                if ( is_admin() ) {
     37                        $screen = get_current_screen();
     38                        if ( ! $screen || ( ! in_array( $screen->base, array( 'upload', 'media', 'post' ), true ) && ! in_array( $screen->id, array( 'media_page_etch-collections', 'media_page_media-bridge-for-etch', 'settings_page_media-bridge-for-etch-network' ), true ) ) ) return;
     39                }
    3640                // Frontend controls only load in the authenticated Etch builder.
    3741                if ( ! is_admin() && ( ! current_user_can( 'upload_files' ) || 'magic' !== sanitize_key( wp_unslash( $_GET['etch'] ?? '' ) ) ) ) return; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    … …  
    4246
    4347        public static function activate(): void {
    44                 if ( ! self::etch_is_active() ) {
    45                         wp_die(
    46                                 esc_html__( 'Uplink Media Bridge for Etch requires the Etch plugin to be installed and active.', 'media-bridge-for-etch' ),
    47                                 esc_html__( 'Etch is required', 'media-bridge-for-etch' ),
    48                                 array( 'back_link' => true )
    49                         );
    50                 }
     48                // Activation stays silent and safe; integrations boot only while Etch is active.
    5149                self::migrate_options();
    5250        }
    … …  
    5755                }
    5856                ?>
    59                 <div hidden data-uplink-mbe-toast="error"><p><?php esc_html_e( 'Uplink Media Bridge for Etch is inactive because the required Etch plugin is not active.', 'media-bridge-for-etch' ); ?></p></div>
     57                <div class="notice notice-warning"><p><?php esc_html_e( 'Uplink Media Bridge for Etch needs Etch to enable its features. Install and activate Etch to continue.', 'media-bridge-for-etch' ); ?></p></div>
    6058                <?php
    6159        }
  • media-bridge-for-etch/trunk/includes/class-upload-workspace.php

    r3759083 r3764831  
    2323                }
    2424                return $locations;
     25        }
     26
     27        /** Keep original uploads in the WordPress library, independently of collection controls. */
     28        public static function native_assets(): void {
     29                $settings = Plugin::settings();
     30                if ( ! current_user_can( 'upload_files' ) || ! defined( 'CIMO_FILE' ) || ( 'native' === $settings['provider'] && ! empty( $settings['default_media_screen'] ) ) ) return;
     31                $screen = get_current_screen();
     32                if ( ! $screen || ! in_array( $screen->base, array( 'upload', 'media', 'post' ), true ) ) return;
     33                wp_enqueue_style( 'uplink-mbe-native-upload', UPLINK_MBE_URL . 'assets/native-upload.css', array(), UPLINK_MBE_ASSET_VERSION );
     34                global $pagenow;
     35                $dependencies = 'media-new.php' === $pagenow ? array( 'jquery', 'plupload-handlers' ) : array( 'media-views', 'jquery' );
     36                wp_enqueue_script( 'uplink-mbe-native-upload', UPLINK_MBE_URL . 'assets/native-upload.js', $dependencies, UPLINK_MBE_ASSET_VERSION, true );
     37                wp_localize_script( 'uplink-mbe-native-upload', 'uplinkMbeNativeUpload', array(
     38                        'keepOriginals' => __( 'Keep originals', 'media-bridge-for-etch' ),
     39                        'help' => __( 'Skip Cimo compression for this upload to preserve the original file and embedded EXIF. WordPress may still generate image sizes. Resets after the upload finishes or the dialog closes.', 'media-bridge-for-etch' ),
     40                ) );
    2541        }
    2642
  • media-bridge-for-etch/trunk/media-bridge-for-etch.php

    r3759083 r3764831  
    33 * Plugin Name: Uplink Media Bridge for Etch
    44 * Description: Manage Etch media collections in WordPress, with optional Wicked Folders or HappyFiles synchronization.
    5  * Version: 2.7.0
     5 * Version: 2.7.1
    66 * Plugin URI: https://uplinkplugins.com/articles/uplink-media-bridge-for-etch-version-2/
    77 * Author: Stephen Walker
    … …  
    1616}
    1717
    18 define( 'UPLINK_MBE_VERSION', '2.7.0' );
    19 define( 'UPLINK_MBE_ASSET_VERSION', '2.7.0.27' );
     18define( 'UPLINK_MBE_VERSION', '2.7.1' );
     19define( 'UPLINK_MBE_ASSET_VERSION', '2.7.1.3' );
    2020define( 'UPLINK_MBE_FILE', __FILE__ );
    2121define( 'UPLINK_MBE_PATH', plugin_dir_path( __FILE__ ) );
  • media-bridge-for-etch/trunk/readme.txt

    r3759083 r3764831  
    55Tested up to: 7.1
    66Requires PHP: 8.3
    7 Stable tag: 2.7.0
     7Stable tag: 2.7.1
    88License: GPL-3.0-or-later
    99
    … …  
    263263
    264264== Changelog ==
     265
     266= 2.7.1 =
     267* Adds Keep originals to native WordPress uploads to bypass Cimo and preserve embedded EXIF, with automatic reset after the upload or closing the dialog.
     268* Repairs collection assignment and refresh after WordPress 7.2 REST uploads.
     269* Adds explicit nonces and attachment permissions to native collection saves and upload assignments.
     270* Uses WordPress upload validation for conversion staging and bounds serialized-reference traversal.
     271* Allows silent activation without Etch; integration features wait for Etch and show an administrator notice.
     272* Limits notification and replacement assets to the media/editor screens that use them.
     273* Documents editable first-party source and the steps to rebuild the plugin package.
     274
    265275
    266276= 2.7.0 =
    … …  
    748758Bundled image encoders run locally. Codec source, build and license references are in assets/vendor/jsquash/SOURCES.md. Leaflet source and license are in assets/vendor/leaflet. This release uses GPL-3.0-or-later for compatibility with the bundled Apache-2.0 codec wrappers. Third-party libraries retain their own licenses.
    749759
     760== Source code and builds ==
     761
     762The ZIP includes editable PHP, JavaScript and CSS for all first-party code. assets/gallery-lightbox.js is the original handwritten source, not a generated or minified bundle. Edit it directly; no npm, webpack, transpiler or compilation step is required. First-party assets are served directly as shipped.
     763
     764Development repository: https://github.com/stphnwlkr/media-bridge-for-etch
     765
     766To make an installable ZIP, place media-bridge-for-etch.php, uninstall.php, readme.txt, LICENSE and the includes, assets and blocks directories inside a media-bridge-for-etch folder and ZIP that folder. Development tests, reports and old builds are excluded from the runtime package. The development checkout also provides python3 tools/build.py for this packaging step.
     767
     768Bundled third-party assets retain their upstream source, build and license references in the sections below and their assets/vendor SOURCES.md files. Browser Image Compression includes readable original sources in its source map. Leaflet includes leaflet-src.js.
     769
    750770== Third-party licenses ==
    751771
Note: See TracChangeset for help on using the changeset viewer.