Changeset 3764831
- Timestamp:
- 10/25/2026 03:31:21 PM (less than one hour ago)
- Location:
- media-bridge-for-etch
- Files:
-
- 4 added
- 24 edited
- 1 copied
-
tags/2.7.1 (copied) (copied from media-bridge-for-etch/trunk )
-
tags/2.7.1/assets/gallery-lightbox.js (modified) (2 diffs)
-
tags/2.7.1/assets/image-conversion.css (modified) (1 diff)
-
tags/2.7.1/assets/media-modal.js (modified) (1 diff)
-
tags/2.7.1/assets/native-collections.js (modified) (4 diffs)
-
tags/2.7.1/assets/native-upload.css (added)
-
tags/2.7.1/assets/native-upload.js (added)
-
tags/2.7.1/includes/class-file-replacement.php (modified) (1 diff)
-
tags/2.7.1/includes/class-image-conversion.php (modified) (1 diff)
-
tags/2.7.1/includes/class-image-references.php (modified) (1 diff)
-
tags/2.7.1/includes/class-native-media-library.php (modified) (7 diffs)
-
tags/2.7.1/includes/class-plugin.php (modified) (4 diffs)
-
tags/2.7.1/includes/class-upload-workspace.php (modified) (1 diff)
-
tags/2.7.1/media-bridge-for-etch.php (modified) (2 diffs)
-
tags/2.7.1/readme.txt (modified) (3 diffs)
-
trunk/assets/gallery-lightbox.js (modified) (2 diffs)
-
trunk/assets/image-conversion.css (modified) (1 diff)
-
trunk/assets/media-modal.js (modified) (1 diff)
-
trunk/assets/native-collections.js (modified) (4 diffs)
-
trunk/assets/native-upload.css (added)
-
trunk/assets/native-upload.js (added)
-
trunk/includes/class-file-replacement.php (modified) (1 diff)
-
trunk/includes/class-image-conversion.php (modified) (1 diff)
-
trunk/includes/class-image-references.php (modified) (1 diff)
-
trunk/includes/class-native-media-library.php (modified) (7 diffs)
-
trunk/includes/class-plugin.php (modified) (4 diffs)
-
trunk/includes/class-upload-workspace.php (modified) (1 diff)
-
trunk/media-bridge-for-etch.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (3 diffs)
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 */ 1 6 ( function () { 2 7 'use strict'; … … 260 265 document.body.appendChild( overlay ); 261 266 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 }; 263 306 // Activity reveals controls without restarting the independent image timer. 264 307 for ( const type of [ 'pointermove', 'pointerdown' ] ) dialog.addEventListener( type, () => { -
media-bridge-for-etch/tags/2.7.1/assets/image-conversion.css
r3759083 r3764831 10 10 .uplink-mbe-conversion-card h4 {margin:0 0 14px!important;font-size:16px!important;line-height:1.3;color:inherit!important} 11 11 .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} 13 13 .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)} 14 14 .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 231 231 } 232 232 handled = true; 233 attachment.off( 'change:uploading change:id ', complete );233 attachment.off( 'change:uploading change:id change sync', complete ); 234 234 refreshAfterUpload( frame, attachment ); 235 235 }; 236 236 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 ); 238 240 complete(); 239 241 } -
media-bridge-for-etch/tags/2.7.1/assets/native-collections.js
r3759083 r3764831 9 9 let uploadCollection = 0; 10 10 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 } ); 11 60 const childrenByParent = new Map(); 12 61 const collapsedCollections = new Set(); … … 886 935 return; 887 936 } 937 libraryViews.add( view ); 888 938 if ( ! view.el.isConnected ) { 889 939 if ( ! view.uplinkMbeNativeCollectionsPending ) { … … 1201 1251 if ( window.wpUploaderInit?.multipart_params ) { 1202 1252 window.wpUploaderInit.multipart_params.uplink_mbe_collection = uploadCollection; 1253 window.wpUploaderInit.multipart_params.uplink_mbe_collection_nonce = config.collectionNonce; 1203 1254 } 1204 1255 if ( window.uploader?.settings?.multipart_params ) { 1205 1256 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 } ); 1208 1263 } 1209 1264 … … 1301 1356 uploaders.add( this ); 1302 1357 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; 1304 1363 }; 1305 1364 } -
media-bridge-for-etch/tags/2.7.1/includes/class-file-replacement.php
r3759083 r3764831 20 20 public function assets(): void { 21 21 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. 22 26 Upload_Workspace::assets(); 23 27 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 229 229 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' ) ); 230 230 } 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']; 234 262 if ( isset( $job['staged'][ $key ] ) ) wp_delete_file( $job['staged'][ $key ] ); 235 263 $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 17 17 return $out; 18 18 } 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; 23 26 } 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; } 25 29 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. 36 36 } 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-])~'; 39 38 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; 40 54 } 41 55 /** 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 33 33 add_action( 'wp_enqueue_media', array( $this, 'media_modal_assets' ) ); 34 34 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' ) ); 35 37 add_filter( 'ajax_query_attachments_args', array( $this, 'filter_native_collection_query' ) ); 36 38 add_action( 'pre_get_posts', array( $this, 'filter_native_media_list_query' ) ); … … 257 259 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 258 260 'nonce' => wp_create_nonce( self::NONCE ), 261 'collectionNonce' => wp_create_nonce( 'uplink_mbe_native_collection' ), 259 262 'collections' => $this->native_collection_data(), 260 263 'counts' => $this->native_collection_counts(), … … 434 437 435 438 $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 ); 436 440 $html .= '<div class="uplink-mbe-native-assignment-list">'; 437 441 $rendered = array(); … … 492 496 return $post; 493 497 } 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; 494 500 $attachment_id = absint( $post['ID'] ?? 0 ); 495 501 $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 ) ) { 497 503 return $post; 498 504 } … … 513 519 return; 514 520 } 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; 516 523 $collection_id = absint( $_REQUEST['uplink_mbe_collection'] ?? 0 ); 517 524 $taxonomy = get_taxonomy( $this->etch->taxonomy() ); … … 2558 2565 } 2559 2566 $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; 2560 2568 $file = get_attached_file( $attachment_id ); 2561 2569 if ( $attachment_id && $file && is_readable( $file ) ) { … … 2570 2578 } 2571 2579 $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; 2572 2581 $original_size = absint( $this->etch_optimization_before[ $attachment_id ] ?? 0 ); 2573 2582 unset( $this->etch_optimization_before[ $attachment_id ] ); -
media-bridge-for-etch/tags/2.7.1/includes/class-plugin.php
r3759083 r3764831 22 22 23 23 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 );26 24 if ( ! self::etch_is_active() ) { 27 25 if ( is_admin() ) { … … 30 28 return; 31 29 } 30 add_action( 'admin_enqueue_scripts', array( self::class, 'toast_assets' ), 1 ); 31 add_action( 'wp_enqueue_scripts', array( self::class, 'toast_assets' ), 1 ); 32 32 self::instance(); 33 33 } 34 34 35 35 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 } 36 40 // Frontend controls only load in the authenticated Etch builder. 37 41 if ( ! is_admin() && ( ! current_user_can( 'upload_files' ) || 'magic' !== sanitize_key( wp_unslash( $_GET['etch'] ?? '' ) ) ) ) return; // phpcs:ignore WordPress.Security.NonceVerification.Recommended … … 42 46 43 47 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. 51 49 self::migrate_options(); 52 50 } … … 57 55 } 58 56 ?> 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> 60 58 <?php 61 59 } -
media-bridge-for-etch/tags/2.7.1/includes/class-upload-workspace.php
r3759083 r3764831 23 23 } 24 24 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 ) ); 25 41 } 26 42 -
media-bridge-for-etch/tags/2.7.1/media-bridge-for-etch.php
r3759083 r3764831 3 3 * Plugin Name: Uplink Media Bridge for Etch 4 4 * Description: Manage Etch media collections in WordPress, with optional Wicked Folders or HappyFiles synchronization. 5 * Version: 2.7. 05 * Version: 2.7.1 6 6 * Plugin URI: https://uplinkplugins.com/articles/uplink-media-bridge-for-etch-version-2/ 7 7 * Author: Stephen Walker … … 16 16 } 17 17 18 define( 'UPLINK_MBE_VERSION', '2.7. 0' );19 define( 'UPLINK_MBE_ASSET_VERSION', '2.7. 0.27' );18 define( 'UPLINK_MBE_VERSION', '2.7.1' ); 19 define( 'UPLINK_MBE_ASSET_VERSION', '2.7.1.3' ); 20 20 define( 'UPLINK_MBE_FILE', __FILE__ ); 21 21 define( 'UPLINK_MBE_PATH', plugin_dir_path( __FILE__ ) ); -
media-bridge-for-etch/tags/2.7.1/readme.txt
r3759083 r3764831 5 5 Tested up to: 7.1 6 6 Requires PHP: 8.3 7 Stable tag: 2.7. 07 Stable tag: 2.7.1 8 8 License: GPL-3.0-or-later 9 9 … … 263 263 264 264 == 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 265 275 266 276 = 2.7.0 = … … 748 758 Bundled 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. 749 759 760 == Source code and builds == 761 762 The 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 764 Development repository: https://github.com/stphnwlkr/media-bridge-for-etch 765 766 To 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 768 Bundled 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 750 770 == Third-party licenses == 751 771 -
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 */ 1 6 ( function () { 2 7 'use strict'; … … 260 265 document.body.appendChild( overlay ); 261 266 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 }; 263 306 // Activity reveals controls without restarting the independent image timer. 264 307 for ( const type of [ 'pointermove', 'pointerdown' ] ) dialog.addEventListener( type, () => { -
media-bridge-for-etch/trunk/assets/image-conversion.css
r3759083 r3764831 10 10 .uplink-mbe-conversion-card h4 {margin:0 0 14px!important;font-size:16px!important;line-height:1.3;color:inherit!important} 11 11 .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} 13 13 .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)} 14 14 .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 231 231 } 232 232 handled = true; 233 attachment.off( 'change:uploading change:id ', complete );233 attachment.off( 'change:uploading change:id change sync', complete ); 234 234 refreshAfterUpload( frame, attachment ); 235 235 }; 236 236 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 ); 238 240 complete(); 239 241 } -
media-bridge-for-etch/trunk/assets/native-collections.js
r3759083 r3764831 9 9 let uploadCollection = 0; 10 10 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 } ); 11 60 const childrenByParent = new Map(); 12 61 const collapsedCollections = new Set(); … … 886 935 return; 887 936 } 937 libraryViews.add( view ); 888 938 if ( ! view.el.isConnected ) { 889 939 if ( ! view.uplinkMbeNativeCollectionsPending ) { … … 1201 1251 if ( window.wpUploaderInit?.multipart_params ) { 1202 1252 window.wpUploaderInit.multipart_params.uplink_mbe_collection = uploadCollection; 1253 window.wpUploaderInit.multipart_params.uplink_mbe_collection_nonce = config.collectionNonce; 1203 1254 } 1204 1255 if ( window.uploader?.settings?.multipart_params ) { 1205 1256 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 } ); 1208 1263 } 1209 1264 … … 1301 1356 uploaders.add( this ); 1302 1357 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; 1304 1363 }; 1305 1364 } -
media-bridge-for-etch/trunk/includes/class-file-replacement.php
r3759083 r3764831 20 20 public function assets(): void { 21 21 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. 22 26 Upload_Workspace::assets(); 23 27 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 229 229 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' ) ); 230 230 } 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']; 234 262 if ( isset( $job['staged'][ $key ] ) ) wp_delete_file( $job['staged'][ $key ] ); 235 263 $job['staged'][ $key ] = $temp; $job['format'] = $format; $job['quality'] = $quality; -
media-bridge-for-etch/trunk/includes/class-image-references.php
r3757535 r3764831 17 17 return $out; 18 18 } 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; 23 26 } 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; } 25 29 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. 36 36 } 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-])~'; 39 38 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; 40 54 } 41 55 /** Must run inside the conversion transaction. Journal supports nontransactional rollback. */ -
media-bridge-for-etch/trunk/includes/class-native-media-library.php
r3759083 r3764831 33 33 add_action( 'wp_enqueue_media', array( $this, 'media_modal_assets' ) ); 34 34 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' ) ); 35 37 add_filter( 'ajax_query_attachments_args', array( $this, 'filter_native_collection_query' ) ); 36 38 add_action( 'pre_get_posts', array( $this, 'filter_native_media_list_query' ) ); … … 257 259 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 258 260 'nonce' => wp_create_nonce( self::NONCE ), 261 'collectionNonce' => wp_create_nonce( 'uplink_mbe_native_collection' ), 259 262 'collections' => $this->native_collection_data(), 260 263 'counts' => $this->native_collection_counts(), … … 434 437 435 438 $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 ); 436 440 $html .= '<div class="uplink-mbe-native-assignment-list">'; 437 441 $rendered = array(); … … 492 496 return $post; 493 497 } 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; 494 500 $attachment_id = absint( $post['ID'] ?? 0 ); 495 501 $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 ) ) { 497 503 return $post; 498 504 } … … 513 519 return; 514 520 } 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; 516 523 $collection_id = absint( $_REQUEST['uplink_mbe_collection'] ?? 0 ); 517 524 $taxonomy = get_taxonomy( $this->etch->taxonomy() ); … … 2558 2565 } 2559 2566 $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; 2560 2568 $file = get_attached_file( $attachment_id ); 2561 2569 if ( $attachment_id && $file && is_readable( $file ) ) { … … 2570 2578 } 2571 2579 $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; 2572 2581 $original_size = absint( $this->etch_optimization_before[ $attachment_id ] ?? 0 ); 2573 2582 unset( $this->etch_optimization_before[ $attachment_id ] ); -
media-bridge-for-etch/trunk/includes/class-plugin.php
r3759083 r3764831 22 22 23 23 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 );26 24 if ( ! self::etch_is_active() ) { 27 25 if ( is_admin() ) { … … 30 28 return; 31 29 } 30 add_action( 'admin_enqueue_scripts', array( self::class, 'toast_assets' ), 1 ); 31 add_action( 'wp_enqueue_scripts', array( self::class, 'toast_assets' ), 1 ); 32 32 self::instance(); 33 33 } 34 34 35 35 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 } 36 40 // Frontend controls only load in the authenticated Etch builder. 37 41 if ( ! is_admin() && ( ! current_user_can( 'upload_files' ) || 'magic' !== sanitize_key( wp_unslash( $_GET['etch'] ?? '' ) ) ) ) return; // phpcs:ignore WordPress.Security.NonceVerification.Recommended … … 42 46 43 47 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. 51 49 self::migrate_options(); 52 50 } … … 57 55 } 58 56 ?> 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> 60 58 <?php 61 59 } -
media-bridge-for-etch/trunk/includes/class-upload-workspace.php
r3759083 r3764831 23 23 } 24 24 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 ) ); 25 41 } 26 42 -
media-bridge-for-etch/trunk/media-bridge-for-etch.php
r3759083 r3764831 3 3 * Plugin Name: Uplink Media Bridge for Etch 4 4 * Description: Manage Etch media collections in WordPress, with optional Wicked Folders or HappyFiles synchronization. 5 * Version: 2.7. 05 * Version: 2.7.1 6 6 * Plugin URI: https://uplinkplugins.com/articles/uplink-media-bridge-for-etch-version-2/ 7 7 * Author: Stephen Walker … … 16 16 } 17 17 18 define( 'UPLINK_MBE_VERSION', '2.7. 0' );19 define( 'UPLINK_MBE_ASSET_VERSION', '2.7. 0.27' );18 define( 'UPLINK_MBE_VERSION', '2.7.1' ); 19 define( 'UPLINK_MBE_ASSET_VERSION', '2.7.1.3' ); 20 20 define( 'UPLINK_MBE_FILE', __FILE__ ); 21 21 define( 'UPLINK_MBE_PATH', plugin_dir_path( __FILE__ ) ); -
media-bridge-for-etch/trunk/readme.txt
r3759083 r3764831 5 5 Tested up to: 7.1 6 6 Requires PHP: 8.3 7 Stable tag: 2.7. 07 Stable tag: 2.7.1 8 8 License: GPL-3.0-or-later 9 9 … … 263 263 264 264 == 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 265 275 266 276 = 2.7.0 = … … 748 758 Bundled 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. 749 759 760 == Source code and builds == 761 762 The 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 764 Development repository: https://github.com/stphnwlkr/media-bridge-for-etch 765 766 To 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 768 Bundled 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 750 770 == Third-party licenses == 751 771
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)