Changeset 3353021
- Timestamp:
- 08/30/2025 08:22:13 AM (4 months ago)
- Location:
- media-library-downloader/trunk
- Files:
-
- 4 edited
-
assets/js/admin.js (modified) (4 diffs)
-
includes/class-main.php (modified) (4 diffs)
-
media-library-downloader.php (modified) (5 diffs)
-
readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
media-library-downloader/trunk/assets/js/admin.js
r3281245 r3353021 1 1 document.addEventListener('DOMContentLoaded', function () { 2 let body = document.body; 3 if (!body.classList.contains('upload-php')) { return; } 2 const body = document.body; 3 if (!body.classList.contains('upload-php')) { 4 return; 5 } 4 6 5 7 /** 6 * Get current view8 * Download Manager Class 7 9 */ 8 let viewList = document.querySelector('.table-view-list'); 9 if (viewList !== null) { 10 let bulkActionTop = document.querySelector('#bulk-action-selector-top'); 11 let bulkActionBottom = document.querySelector('#bulk-action-selector-bottom'); 12 13 /** 14 * Add option on Top 15 */ 16 const option1 = document.createElement("option"); 17 option1.value = "mld-download-files" 18 option1.innerText = mld_i18n.download_files; 19 bulkActionTop.appendChild(option1); 20 21 /** 22 * Add option on Bottom 23 */ 24 const option2 = document.createElement("option"); 25 option2.value = "mld-download-files" 26 option2.innerText = mld_i18n.download_files; 27 bulkActionBottom.appendChild(option2); 28 29 // Ajax Call 30 let doAction = document.querySelector('#doaction'); 31 doAction.addEventListener('click', function (e) { 32 if ((bulkActionTop.value || bulkActionBottom.value) !== 'mld-download-files') { 10 class MLDDownloadManager { 11 constructor() { 12 this.isDownloading = false; 13 this.init(); 14 } 15 16 init() { 17 const viewList = document.querySelector('.table-view-list'); 18 if (viewList !== null) { 19 this.initListView(); 20 } else { 21 this.initGridView(); 22 } 23 } 24 25 /** 26 * Initialize List View 27 */ 28 initListView() { 29 this.addBulkActions(); 30 this.handleBulkDownload(); 31 this.addIndividualDownloadButtons(); 32 } 33 34 /** 35 * Add bulk action options 36 */ 37 addBulkActions() { 38 const bulkActionTop = document.querySelector('#bulk-action-selector-top'); 39 const bulkActionBottom = document.querySelector('#bulk-action-selector-bottom'); 40 41 if (!bulkActionTop || !bulkActionBottom) return; 42 43 // Add option to top selector 44 const option1 = document.createElement("option"); 45 option1.value = "mld-download-files"; 46 option1.innerText = mld_i18n.download_files; 47 bulkActionTop.appendChild(option1); 48 49 // Add option to bottom selector 50 const option2 = document.createElement("option"); 51 option2.value = "mld-download-files"; 52 option2.innerText = mld_i18n.download_files; 53 bulkActionBottom.appendChild(option2); 54 } 55 56 /** 57 * Handle bulk download action 58 */ 59 handleBulkDownload() { 60 const doAction = document.querySelector('#doaction'); 61 const doAction2 = document.querySelector('#doaction2'); 62 63 if (doAction) { 64 doAction.addEventListener('click', (e) => this.processBulkAction(e, 'top')); 65 } 66 if (doAction2) { 67 doAction2.addEventListener('click', (e) => this.processBulkAction(e, 'bottom')); 68 } 69 } 70 71 /** 72 * Process bulk action 73 */ 74 processBulkAction(e, position) { 75 const selector = position === 'top' ? 76 document.querySelector('#bulk-action-selector-top') : 77 document.querySelector('#bulk-action-selector-bottom'); 78 79 if (!selector || selector.value !== 'mld-download-files') { 33 80 return; 34 81 } … … 36 83 e.preventDefault(); 37 84 38 let checkedFiles = document.querySelectorAll('#the-list input:checked');85 const checkedFiles = document.querySelectorAll('#the-list input:checked'); 39 86 if (!checkedFiles || checkedFiles.length === 0) { 40 alert(mld_i18n.no_files_selected);87 this.showMessage(mld_i18n.no_files_selected, 'error'); 41 88 return; 42 89 } 43 90 44 let selection = new Array(); 45 for (let index = 0; index < checkedFiles.length; index++) { 46 const element = checkedFiles[index]; 91 const selection = []; 92 for (const element of checkedFiles) { 47 93 if (element && element.value) { 48 94 selection.push(element.value); … … 51 97 52 98 if (selection.length === 0) { 53 alert(mld_i18n.no_files_selected);99 this.showMessage(mld_i18n.no_files_selected, 'error'); 54 100 return; 55 101 } 102 103 this.downloadFiles(selection); 104 } 105 106 /** 107 * Add individual download buttons to list view 108 */ 109 addIndividualDownloadButtons() { 110 const rows = document.querySelectorAll('#the-list tr'); 111 112 rows.forEach(row => { 113 const titleCell = row.querySelector('.title'); 114 if (!titleCell) return; 115 116 const attachmentId = this.getAttachmentId(row); 117 if (!attachmentId) return; 118 119 // Create download button 120 const downloadBtn = document.createElement('button'); 121 downloadBtn.className = 'button button-small mld-single-download'; 122 downloadBtn.type = 'button'; 123 downloadBtn.innerHTML = '📥 ' + mld_i18n.download_single; 124 downloadBtn.dataset.attachmentId = attachmentId; 125 downloadBtn.style.marginLeft = '10px'; 126 downloadBtn.setAttribute('aria-label', mld_i18n.download_single + ' ' + (titleCell.querySelector('.title')?.textContent || 'file')); 127 downloadBtn.setAttribute('title', mld_i18n.download_single); 128 129 // Add click handler 130 downloadBtn.addEventListener('click', (e) => { 131 e.preventDefault(); 132 this.downloadFiles([attachmentId]); 133 }); 134 135 // Add keyboard support 136 downloadBtn.addEventListener('keydown', (e) => { 137 if (e.key === 'Enter' || e.key === ' ') { 138 e.preventDefault(); 139 this.downloadFiles([attachmentId]); 140 } 141 }); 142 143 // Insert button 144 const rowActions = titleCell.querySelector('.row-actions'); 145 if (rowActions) { 146 const downloadAction = document.createElement('span'); 147 downloadAction.className = 'mld-download'; 148 downloadAction.appendChild(downloadBtn); 149 rowActions.appendChild(downloadAction); 150 } 151 }); 152 } 153 154 /** 155 * Get attachment ID from row 156 */ 157 getAttachmentId(row) { 158 const checkbox = row.querySelector('input[type="checkbox"]'); 159 return checkbox ? checkbox.value : null; 160 } 161 162 /** 163 * Initialize Grid View 164 */ 165 initGridView() { 166 this.watchForGridChanges(); 167 this.addGridDownloadButtons(); 168 } 169 170 /** 171 * Watch for grid view changes 172 */ 173 watchForGridChanges() { 174 const observer = new MutationObserver(() => { 175 this.addGridDownloadButtons(); 176 }); 177 178 const attachmentsWrapper = document.querySelector('.attachments-wrapper'); 179 if (attachmentsWrapper) { 180 observer.observe(attachmentsWrapper, { 181 childList: true, 182 subtree: true 183 }); 184 } 185 186 // Also watch for select mode toggle 187 setTimeout(() => { 188 const bulkSelect = document.querySelector('.media-toolbar .select-mode-toggle-button'); 189 if (bulkSelect) { 190 bulkSelect.addEventListener('click', () => { 191 setTimeout(() => this.addBulkDownloadButton(), 100); 192 }); 193 } 194 }, 200); 195 } 196 197 /** 198 * Add bulk download button for grid view 199 */ 200 addBulkDownloadButton() { 201 // Remove existing button 202 const existingBtn = document.querySelector('#mld-download'); 203 if (existingBtn) { 204 existingBtn.remove(); 205 } 206 207 const toolbar = document.querySelector('.media-toolbar-secondary'); 208 const deleteBtn = document.querySelector('.delete-selected-button'); 209 210 if (!toolbar || !deleteBtn) return; 211 212 const downloadButton = document.createElement('button'); 213 downloadButton.id = 'mld-download'; 214 downloadButton.type = 'button'; 215 downloadButton.className = 'button media-button button-primary button-large'; 216 downloadButton.innerHTML = mld_i18n.download_files; 217 downloadButton.setAttribute('aria-label', mld_i18n.download_files); 218 downloadButton.setAttribute('title', mld_i18n.download_files); 219 220 downloadButton.addEventListener('click', () => { 221 const selectedAttachments = document.querySelectorAll('.attachments-wrapper .attachments .attachment[aria-checked="true"]'); 222 223 if (selectedAttachments.length === 0) { 224 this.showMessage(mld_i18n.no_files_selected, 'error'); 225 return; 226 } 227 228 const filesToDownload = []; 229 selectedAttachments.forEach(element => { 230 const dataID = element.dataset.id; 231 if (dataID) { 232 filesToDownload.push(dataID); 233 } 234 }); 235 236 if (filesToDownload.length > 0) { 237 this.downloadFiles(filesToDownload); 238 } 239 }); 240 241 toolbar.insertBefore(downloadButton, deleteBtn); 242 } 243 244 /** 245 * Add individual download buttons to grid view 246 */ 247 addGridDownloadButtons() { 248 const attachments = document.querySelectorAll('.attachment:not(.mld-processed)'); 249 250 attachments.forEach(attachment => { 251 attachment.classList.add('mld-processed'); 252 253 const attachmentId = attachment.dataset.id; 254 if (!attachmentId) return; 255 256 // Create download overlay 257 const downloadOverlay = document.createElement('div'); 258 downloadOverlay.className = 'mld-download-overlay'; 259 downloadOverlay.style.cssText = ` 260 position: absolute; 261 top: 5px; 262 right: 5px; 263 background: rgba(0,0,0,0.7); 264 border-radius: 3px; 265 padding: 2px; 266 opacity: 0; 267 transition: opacity 0.2s; 268 z-index: 10; 269 `; 270 271 const downloadBtn = document.createElement('button'); 272 downloadBtn.className = 'button button-small mld-grid-download'; 273 downloadBtn.type = 'button'; 274 downloadBtn.innerHTML = '📥'; 275 downloadBtn.title = mld_i18n.download_single; 276 downloadBtn.style.cssText = ` 277 background: #0073aa; 278 color: white; 279 border: none; 280 padding: 4px 6px; 281 font-size: 12px; 282 cursor: pointer; 283 `; 284 285 downloadBtn.addEventListener('click', (e) => { 286 e.preventDefault(); 287 e.stopPropagation(); 288 this.downloadFiles([attachmentId]); 289 }); 290 291 downloadOverlay.appendChild(downloadBtn); 292 attachment.style.position = 'relative'; 293 attachment.appendChild(downloadOverlay); 294 295 // Show/hide on hover 296 attachment.addEventListener('mouseenter', () => { 297 downloadOverlay.style.opacity = '1'; 298 }); 299 attachment.addEventListener('mouseleave', () => { 300 downloadOverlay.style.opacity = '0'; 301 }); 302 }); 303 } 304 305 /** 306 * Download files via AJAX 307 */ 308 downloadFiles(attachmentIds) { 309 if (this.isDownloading) { 310 this.showMessage(mld_i18n.preparing_download, 'info'); 311 return; 312 } 313 314 this.isDownloading = true; 315 this.showProgress(true); 56 316 57 317 jQuery.post({ … … 59 319 data: { 60 320 action: "download_files", 61 ids: selection, 321 ids: attachmentIds, 322 nonce: admin.nonce 62 323 }, 63 success: function (response) { 64 if (response.success) { 65 window.location = response.data; 66 } else { 67 alert(response.data); 68 } 324 success: (response) => { 325 this.handleDownloadResponse(response); 69 326 }, 70 error: function() { 71 alert(mld_i18n.download_error); 327 error: (xhr, status, error) => { 328 this.handleDownloadError(xhr, status, error); 329 }, 330 complete: () => { 331 this.isDownloading = false; 332 this.showProgress(false); 72 333 } 73 334 }); 74 }) 75 } else { 76 77 setTimeout(() => { 78 let bulkSelect = document.querySelector('.media-toolbar .select-mode-toggle-button'); 79 80 if (bulkSelect !== null) { 81 bulkSelect.setAttribute('aria-selected', 'false'); 82 bulkSelect.addEventListener('click', function (e) { 83 let existingDownloadButton = document.querySelector('#mld-download'); 84 85 // Remove old button 86 if (e.target.classList.contains('large-button')){ 87 existingDownloadButton.remove(); 88 } 89 if (existingDownloadButton === null) { 90 let downloadButton = document.createElement('button'); 91 downloadButton.id = 'mld-download' 92 downloadButton.type = 'button'; 93 downloadButton.classList = 'button media-button button-primary button-large delete-selected-button'; 94 downloadButton.innerText = 'Télécharger'; 95 document.querySelector('.media-toolbar-secondary').insertBefore(downloadButton, document.querySelector('.delete-selected-button')) 96 } 97 }) 98 } 99 }, 200); 100 101 // Ajax Call 102 window.addEventListener('click', function(e){ 103 let clickedElementID = e.target.id; 104 if (clickedElementID !== 'mld-download'){ 105 return; 106 } 107 let filesToDownload = new Array(); 108 let imagesSelected = document.querySelectorAll('.attachments-wrapper .attachments .attachment[aria-checked="true"]'); 109 if (imagesSelected.length > 0){ 110 for (let index = 0; index < imagesSelected.length; index++) { 111 const element = imagesSelected[index]; 112 let dataID = element.dataset.id; 113 filesToDownload.push(dataID); 114 } 115 jQuery.post({ 116 url: admin.ajax_url, 117 data: { 118 action: "download_files", 119 ids: filesToDownload, 120 }, 121 success: function (response) { 122 window.location = response.data; 123 }, 124 }); 125 } 126 }) 335 } 336 337 /** 338 * Handle successful download response 339 */ 340 handleDownloadResponse(response) { 341 if (response.success && response.data) { 342 const data = response.data; 343 344 if (data.single) { 345 // Direct download for single files 346 window.open(data.url, '_blank'); 347 this.showMessage(`Downloaded: ${data.filename}`, 'success'); 348 } else { 349 // ZIP download for multiple files 350 window.location = data.url; 351 this.showMessage(`Prepared ZIP with ${data.file_count} files`, 'success'); 352 } 353 } else { 354 this.showMessage(response.data || mld_i18n.download_error, 'error'); 355 } 356 } 357 358 /** 359 * Handle download error 360 */ 361 handleDownloadError(xhr, status, error) { 362 let errorMessage = mld_i18n.download_error; 363 364 if (xhr.responseJSON && xhr.responseJSON.data) { 365 errorMessage = xhr.responseJSON.data; 366 } else if (error) { 367 errorMessage = error; 368 } 369 370 this.showMessage(errorMessage, 'error'); 371 } 372 373 /** 374 * Show progress indicator 375 */ 376 showProgress(show) { 377 let progressEl = document.querySelector('#mld-progress'); 378 379 if (show) { 380 if (!progressEl) { 381 progressEl = document.createElement('div'); 382 progressEl.id = 'mld-progress'; 383 progressEl.style.cssText = ` 384 position: fixed; 385 top: 32px; 386 right: 20px; 387 background: #0073aa; 388 color: white; 389 padding: 10px 15px; 390 border-radius: 4px; 391 z-index: 100000; 392 font-size: 14px; 393 `; 394 progressEl.innerHTML = '📥 ' + mld_i18n.preparing_download; 395 document.body.appendChild(progressEl); 396 } 397 } else { 398 if (progressEl) { 399 progressEl.remove(); 400 } 401 } 402 } 403 404 /** 405 * Show message to user 406 */ 407 showMessage(message, type = 'info') { 408 // Remove existing message 409 const existingMessage = document.querySelector('#mld-message'); 410 if (existingMessage) { 411 existingMessage.remove(); 412 } 413 414 const messageEl = document.createElement('div'); 415 messageEl.id = 'mld-message'; 416 417 let bgColor = '#0073aa'; 418 if (type === 'error') bgColor = '#dc3232'; 419 if (type === 'success') bgColor = '#46b450'; 420 421 messageEl.style.cssText = ` 422 position: fixed; 423 top: 32px; 424 right: 20px; 425 background: ${bgColor}; 426 color: white; 427 padding: 10px 15px; 428 border-radius: 4px; 429 z-index: 100000; 430 font-size: 14px; 431 max-width: 300px; 432 `; 433 messageEl.textContent = message; 434 document.body.appendChild(messageEl); 435 436 // Auto remove after 5 seconds 437 setTimeout(() => { 438 if (messageEl && messageEl.parentNode) { 439 messageEl.remove(); 440 } 441 }, 5000); 442 } 127 443 } 128 }) 444 445 // Initialize the download manager 446 new MLDDownloadManager(); 447 }); -
media-library-downloader/trunk/includes/class-main.php
r3281245 r3353021 10 10 add_action( 'admin_enqueue_scripts', array( $this, 'mld_enqueue_back' ) ); 11 11 add_action( 'wp_ajax_download_files', array( $this, 'mld_download_files' ) ); 12 add_action( 'init', array( $this, 'schedule_cleanup' ) ); 12 13 } 13 14 … … 17 18 public function mld_empty_temp_folder() { 18 19 $current_screen_obj = get_current_screen(); 19 if ( !$current_screen_obj && $current_screen_obj->base !== 'upload' ) : 20 return; 21 endif; 20 if ( ! $current_screen_obj || $current_screen_obj->base !== 'upload' ) { 21 return; 22 } 23 24 if ( ! is_dir( MLD_TEMP_PATH ) ) { 25 return; 26 } 27 22 28 $files = glob( MLD_TEMP_PATH . '/*' ); 23 foreach ( $files as $file ) : 24 if ( is_file( $file ) ) : 29 if ( ! $files ) { 30 return; 31 } 32 33 foreach ( $files as $file ) { 34 // Security check: ensure file is within temp directory 35 if ( strpos( realpath( $file ), realpath( MLD_TEMP_PATH ) ) !== 0 ) { 36 continue; 37 } 38 39 if ( is_file( $file ) ) { 25 40 unlink( $file ); 26 endif; 27 if ( is_dir( $file ) ) : 28 rmdir( $file ); 29 endif; 30 endforeach; 41 } elseif ( is_dir( $file ) ) { 42 $this->mld_remove_directory( $file ); 43 } 44 } 45 } 46 47 /** 48 * Recursively remove directory 49 */ 50 private function mld_remove_directory( $dir ) { 51 if ( ! is_dir( $dir ) ) { 52 return false; 53 } 54 55 $files = array_diff( scandir( $dir ), array( '.', '..' ) ); 56 foreach ( $files as $file ) { 57 $path = $dir . '/' . $file; 58 if ( is_dir( $path ) ) { 59 $this->mld_remove_directory( $path ); 60 } else { 61 unlink( $path ); 62 } 63 } 64 return rmdir( $dir ); 31 65 } 32 66 … … 35 69 */ 36 70 public function mld_enqueue_back() { 37 wp_enqueue_script( 'mld-admin-script', MLD_ASSETS_JS . 'admin.js', array( 'jquery' ), 1.0, true ); 38 wp_localize_script( 'mld-admin-script', 'admin', array( 'ajax_url' => admin_url( 'admin-ajax.php' ) ) ); 71 $current_screen = get_current_screen(); 72 if ( ! $current_screen || $current_screen->base !== 'upload' ) { 73 return; 74 } 75 76 wp_enqueue_script( 'mld-admin-script', MLD_ASSETS_JS . 'admin.js', array( 'jquery' ), '1.3.4', true ); 77 wp_localize_script( 'mld-admin-script', 'admin', array( 78 'ajax_url' => admin_url( 'admin-ajax.php' ), 79 'nonce' => wp_create_nonce( 'mld_download_nonce' ) 80 ) ); 39 81 wp_localize_script( 'mld-admin-script', 'mld_i18n', array( 40 'download_files' => __('Download selected files', 'media-library-downloader'), 41 'no_files_selected' => __('No files selected', 'media-library-downloader'), 42 'download_error' => __('An error occurred while downloading files', 'media-library-downloader') 82 'download_files' => __( 'Download selected files', 'media-library-downloader' ), 83 'download_single' => __( 'Download', 'media-library-downloader' ), 84 'no_files_selected' => __( 'No files selected', 'media-library-downloader' ), 85 'download_error' => __( 'An error occurred while downloading files', 'media-library-downloader' ), 86 'preparing_download' => __( 'Preparing download...', 'media-library-downloader' ), 87 'invalid_file' => __( 'Invalid file selected', 'media-library-downloader' ), 88 'unauthorized' => __( 'Unauthorized access', 'media-library-downloader' ) 43 89 )); 44 90 } … … 48 94 */ 49 95 public function mld_download_files() { 50 51 // Prevent non authorized user to make action 52 if ( !current_user_can('upload_files') ) : 53 wp_send_json_error('Unauthorized access', 403); 54 wp_die(); 55 endif; 56 57 // Check $_POST data 58 if ( isset( $_POST['ids'] ) && !empty( $_POST['ids'] ) ) : 59 $attachment_ids = filter_input( INPUT_POST, 'ids', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ); 60 endif; 61 62 /** 63 * Create temp folder 64 */ 65 $timestamp = gmdate( 'U' ); 66 $folder_name = 'media-library-downloader-' . $timestamp; 67 $folder_path = MLD_TEMP_PATH . $folder_name; 68 $root = MLD_TEMP_URL; 69 $create_folder = mkdir( $folder_path ); 70 71 if ( $create_folder && $attachment_ids ) : 72 $zip = new ZipArchive(); 73 if ( $zip->open( $folder_path . '.zip', ZipArchive::CREATE ) ) : 74 foreach ( $attachment_ids as $attachment_id ) : 75 $attachment_name = basename( get_attached_file( $attachment_id ) ); 76 $attachment_url = get_attached_file($attachment_id ); 77 if ( ini_get( 'allow_url_fopen' ) ) { 78 $file_content = file_get_contents( $attachment_url ); //phpcs:disable 79 } else { 80 $ch = curl_init(); 81 curl_setopt( $ch, CURLOPT_URL, $attachment_url ); 82 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); 83 $file_content = curl_exec( $ch ); 84 curl_close( $ch ); 85 } 86 $zip->addFromString( $attachment_name, $file_content ); 87 endforeach; 88 $zip->close(); 89 header( 'Content-disposition: attachment; filename=' . $folder_name . '.zip' ); 90 header( 'Content-type: application/zip' ); 91 wp_send_json_success( $root . $folder_name . '.zip', null, 0 ); 92 wp_die(); 93 endif; 94 endif; 96 // Verify nonce for security 97 if ( ! wp_verify_nonce( $_POST['nonce'] ?? '', 'mld_download_nonce' ) ) { 98 wp_send_json_error( __( 'Security check failed', 'media-library-downloader' ), 403 ); 99 } 100 101 // Check user permissions 102 if ( ! current_user_can( 'upload_files' ) ) { 103 wp_send_json_error( __( 'Unauthorized access', 'media-library-downloader' ), 403 ); 104 } 105 106 // Validate and sanitize input 107 $attachment_ids = $this->mld_validate_attachment_ids(); 108 if ( empty( $attachment_ids ) ) { 109 wp_send_json_error( __( 'No valid files selected', 'media-library-downloader' ), 400 ); 110 } 111 112 // Check if user can access these attachments 113 $valid_ids = $this->mld_filter_accessible_attachments( $attachment_ids ); 114 if ( empty( $valid_ids ) ) { 115 wp_send_json_error( __( 'No accessible files found', 'media-library-downloader' ), 403 ); 116 } 117 118 // Handle single file download 119 if ( count( $valid_ids ) === 1 ) { 120 $this->mld_download_single_file( $valid_ids[0] ); 121 return; 122 } 123 124 // Handle multiple files download 125 $this->mld_download_multiple_files( $valid_ids ); 126 } 127 128 /** 129 * Validate and sanitize attachment IDs 130 */ 131 private function mld_validate_attachment_ids() { 132 if ( ! isset( $_POST['ids'] ) || ! is_array( $_POST['ids'] ) ) { 133 return array(); 134 } 135 136 $attachment_ids = array(); 137 foreach ( $_POST['ids'] as $id ) { 138 $clean_id = absint( $id ); 139 if ( $clean_id > 0 && get_post_type( $clean_id ) === 'attachment' ) { 140 $attachment_ids[] = $clean_id; 141 } 142 } 143 144 return array_unique( $attachment_ids ); 145 } 146 147 /** 148 * Filter attachments that user can access 149 */ 150 private function mld_filter_accessible_attachments( $attachment_ids ) { 151 $valid_ids = array(); 152 153 foreach ( $attachment_ids as $attachment_id ) { 154 // Check if attachment exists and user can edit it 155 if ( current_user_can( 'edit_post', $attachment_id ) ) { 156 $file_path = get_attached_file( $attachment_id ); 157 if ( $file_path && file_exists( $file_path ) ) { 158 $valid_ids[] = $attachment_id; 159 } 160 } 161 } 162 163 return $valid_ids; 164 } 165 166 /** 167 * Download single file 168 */ 169 private function mld_download_single_file( $attachment_id ) { 170 $file_path = get_attached_file( $attachment_id ); 171 $file_name = basename( $file_path ); 172 173 if ( ! $file_path || ! file_exists( $file_path ) ) { 174 wp_send_json_error( __( 'File not found', 'media-library-downloader' ), 404 ); 175 } 176 177 // Log the download 178 $this->log_download( array( $attachment_id ), 'single' ); 179 180 // For single files, return the direct URL 181 $file_url = wp_get_attachment_url( $attachment_id ); 182 wp_send_json_success( array( 183 'url' => $file_url, 184 'filename' => $file_name, 185 'single' => true 186 ) ); 187 } 188 189 /** 190 * Download multiple files as ZIP 191 */ 192 private function mld_download_multiple_files( $attachment_ids ) { 193 if ( ! class_exists( 'ZipArchive' ) ) { 194 wp_send_json_error( __( 'ZIP functionality not available', 'media-library-downloader' ), 500 ); 195 } 196 197 // Create temp directory if it doesn't exist 198 if ( ! is_dir( MLD_TEMP_PATH ) ) { 199 if ( ! wp_mkdir_p( MLD_TEMP_PATH ) ) { 200 wp_send_json_error( __( 'Cannot create temporary directory', 'media-library-downloader' ), 500 ); 201 } 202 } 203 204 $timestamp = time(); 205 $folder_name = $this->generate_zip_filename( $timestamp ); 206 $zip_path = MLD_TEMP_PATH . $folder_name . '.zip'; 207 $zip_url = MLD_TEMP_URL . $folder_name . '.zip'; 208 209 $zip = new ZipArchive(); 210 if ( $zip->open( $zip_path, ZipArchive::CREATE ) !== TRUE ) { 211 wp_send_json_error( __( 'Cannot create ZIP file', 'media-library-downloader' ), 500 ); 212 } 213 214 $file_count = 0; 215 $total_size = 0; 216 $max_size = $this->mld_get_max_download_size(); 217 218 foreach ( $attachment_ids as $attachment_id ) { 219 $file_path = get_attached_file( $attachment_id ); 220 $file_name = basename( $file_path ); 221 222 if ( ! $file_path || ! file_exists( $file_path ) ) { 223 continue; 224 } 225 226 $file_size = filesize( $file_path ); 227 if ( $total_size + $file_size > $max_size ) { 228 break; // Stop if we exceed size limit 229 } 230 231 // Handle duplicate filenames 232 $unique_name = $this->mld_get_unique_filename( $zip, $file_name ); 233 234 if ( $zip->addFile( $file_path, $unique_name ) ) { 235 $file_count++; 236 $total_size += $file_size; 237 } 238 } 239 240 $zip->close(); 241 242 if ( $file_count === 0 ) { 243 unlink( $zip_path ); 244 wp_send_json_error( __( 'No files could be added to ZIP', 'media-library-downloader' ), 500 ); 245 } 246 247 // Log the download 248 $this->log_download( $attachment_ids, 'zip' ); 249 250 wp_send_json_success( array( 251 'url' => $zip_url, 252 'filename' => $folder_name . '.zip', 253 'file_count' => $file_count, 254 'single' => false 255 ) ); 256 } 257 258 /** 259 * Get maximum download size in bytes 260 */ 261 private function mld_get_max_download_size() { 262 $settings = get_option( 'mld_settings', array() ); 263 $max_size_mb = $settings['max_download_size'] ?? 100; 264 $max_size = $max_size_mb * 1024 * 1024; // Convert to bytes 265 266 $max_size = apply_filters( 'mld_max_download_size', $max_size ); 267 return min( $max_size, wp_max_upload_size() * 2 ); // Don't exceed 2x upload limit 268 } 269 270 /** 271 * Get unique filename for ZIP to handle duplicates 272 */ 273 private function mld_get_unique_filename( $zip, $filename ) { 274 $name = pathinfo( $filename, PATHINFO_FILENAME ); 275 $ext = pathinfo( $filename, PATHINFO_EXTENSION ); 276 $counter = 1; 277 $unique_name = $filename; 278 279 while ( $zip->locateName( $unique_name ) !== false ) { 280 $unique_name = $name . '_' . $counter . ( $ext ? '.' . $ext : '' ); 281 $counter++; 282 } 283 284 return $unique_name; 285 } 286 287 /** 288 * Generate ZIP filename based on pattern 289 */ 290 private function generate_zip_filename( $timestamp ) { 291 $settings = get_option( 'mld_settings', array() ); 292 $pattern = $settings['zip_filename_pattern'] ?? 'media-library-download-{timestamp}'; 293 294 $current_user = wp_get_current_user(); 295 $replacements = array( 296 '{timestamp}' => $timestamp, 297 '{date}' => date( 'Y-m-d', $timestamp ), 298 '{user}' => $current_user->user_login, 299 '{userid}' => $current_user->ID, 300 ); 301 302 $filename = str_replace( array_keys( $replacements ), array_values( $replacements ), $pattern ); 303 return sanitize_file_name( $filename ); 304 } 305 306 /** 307 * Log download activity 308 */ 309 private function log_download( $attachment_ids, $type ) { 310 $settings = get_option( 'mld_settings', array() ); 311 if ( empty( $settings['enable_logging'] ) ) { 312 return; 313 } 314 315 $current_user = wp_get_current_user(); 316 $log_entry = array( 317 'timestamp' => time(), 318 'user' => $current_user->user_login, 319 'user_id' => $current_user->ID, 320 'file_count' => count( $attachment_ids ), 321 'attachment_ids' => $attachment_ids, 322 'type' => $type, 323 'ip' => $this->get_user_ip(), 324 ); 325 326 $logs = get_option( 'mld_download_logs', array() ); 327 $logs[] = $log_entry; 328 329 // Keep only last 1000 entries 330 if ( count( $logs ) > 1000 ) { 331 $logs = array_slice( $logs, -1000 ); 332 } 333 334 update_option( 'mld_download_logs', $logs ); 335 336 // Hook for developers 337 do_action( 'mld_download_logged', $log_entry ); 338 } 339 340 /** 341 * Get user IP address 342 */ 343 private function get_user_ip() { 344 if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) { 345 return sanitize_text_field( wp_unslash( $_SERVER['HTTP_CLIENT_IP'] ) ); 346 } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { 347 return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ); 348 } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) { 349 return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); 350 } 351 return 'unknown'; 352 } 353 354 /** 355 * Schedule automatic cleanup 356 */ 357 public function schedule_cleanup() { 358 if ( ! wp_next_scheduled( 'mld_cleanup_temp_files' ) ) { 359 $settings = get_option( 'mld_settings', array() ); 360 $interval = $settings['cleanup_interval'] ?? 24; 361 362 wp_schedule_event( time(), 'hourly', 'mld_cleanup_temp_files' ); 363 } 364 365 add_action( 'mld_cleanup_temp_files', array( $this, 'automatic_cleanup' ) ); 366 } 367 368 /** 369 * Automatic cleanup via cron 370 */ 371 public function automatic_cleanup() { 372 $settings = get_option( 'mld_settings', array() ); 373 $cleanup_interval = $settings['cleanup_interval'] ?? 24; 374 $cutoff_time = time() - ( $cleanup_interval * HOUR_IN_SECONDS ); 375 376 if ( ! is_dir( MLD_TEMP_PATH ) ) { 377 return; 378 } 379 380 $files = glob( MLD_TEMP_PATH . '*.zip' ); 381 if ( ! $files ) { 382 return; 383 } 384 385 foreach ( $files as $file ) { 386 if ( is_file( $file ) && filemtime( $file ) < $cutoff_time ) { 387 unlink( $file ); 388 } 389 } 390 391 // Log cleanup if logging is enabled 392 if ( ! empty( $settings['enable_logging'] ) ) { 393 $log_entry = array( 394 'timestamp' => time(), 395 'user' => 'system', 396 'user_id' => 0, 397 'file_count' => 0, 398 'attachment_ids' => array(), 399 'type' => 'cleanup', 400 'ip' => 'system', 401 ); 402 403 $logs = get_option( 'mld_download_logs', array() ); 404 $logs[] = $log_entry; 405 update_option( 'mld_download_logs', $logs ); 406 } 95 407 } 96 408 } -
media-library-downloader/trunk/media-library-downloader.php
r3281245 r3353021 4 4 * Plugin URI: https://wordpress.org/plugins/media-library-downloader/ 5 5 * Description: Download multiple media library files in one click ! 6 * Version: 1. 3.36 * Version: 1.4.0 7 7 * Tags: library, media, files, download, downloader, WordPress 8 8 * Requires at least: 5.0 or higher 9 9 * Requires PHP: 5.6 10 * Tested up to: 6. 6.111 * Stable tag: 1. 3.312 * Author: M . Code10 * Tested up to: 6.8.2 11 * Stable tag: 1.4.0 12 * Author: Michael Revellin-Clerc 13 13 * License: GPL v2 or later 14 14 * License URI: https://www.gnu.org/licenses/gpl-2.0.html 15 * Contributors: M . Code15 * Contributors: Michael Revellin-Clerc 16 16 * Donate link: https://ko-fi.com/devloper 17 17 */ … … 34 34 */ 35 35 public function __construct() { 36 $this->define_constants(); 37 $this->setup_actions(); 38 $this->include_files(); 39 } 40 41 /** 42 * Define plugin constants 43 */ 44 private function define_constants() { 45 define( 'MLD_VERSION', '1.4.0' ); 36 46 define( 'MLD_PATH', plugin_dir_path( __FILE__ ) ); 47 define( 'MLD_URL', plugin_dir_url( __FILE__ ) ); 37 48 define( 'MLD_BASENAME', plugin_basename( __FILE__ ) ); 38 49 define( 'MLD_TEMP_PATH', plugin_dir_path( __FILE__ ) . 'temp/' ); 39 50 define( 'MLD_TEMP_URL', plugin_dir_url( __FILE__ ) . 'temp/' ); 40 define( 'MLD_ASSETS_JS', plugin_dir_url(__FILE__ ) . '/assets/js/' ); 41 define( 'MLD_INCLUDES', plugin_dir_path(__FILE__ ) . 'includes/' ); 42 $this->setup_actions(); 43 $this->include_files(); 51 define( 'MLD_ASSETS_JS', plugin_dir_url( __FILE__ ) . 'assets/js/' ); 52 define( 'MLD_INCLUDES', plugin_dir_path( __FILE__ ) . 'includes/' ); 44 53 } 45 54 … … 49 58 public function setup_actions() { 50 59 register_activation_hook( __FILE__, array( $this, 'mld_check_requirements' ) ); 60 add_action( 'plugins_loaded', array( $this, 'load_textdomain' ) ); 61 add_action( 'init', array( $this, 'mld_init' ) ); 62 } 63 64 /** 65 * Load plugin textdomain 66 */ 67 public function load_textdomain() { 68 load_plugin_textdomain( 'media-library-downloader', false, dirname( MLD_BASENAME ) . '/languages' ); 69 } 70 71 /** 72 * Initialize plugin 73 */ 74 public function mld_init() { 75 // Create temp directory if it doesn't exist 76 if ( ! is_dir( MLD_TEMP_PATH ) ) { 77 wp_mkdir_p( MLD_TEMP_PATH ); 78 } 51 79 } 52 80 … … 58 86 array( 59 87 'type' => 'module', 60 'link' => 'https://www.php.net/manual/ fr/zip.installation.php',88 'link' => 'https://www.php.net/manual/en/zip.installation.php', 61 89 'name' => 'zip', 90 'required' => true, 62 91 ), 63 92 array( 64 'type' => 'module', 65 'link' => 'https://www.php.net/manual/ fr/curl.installation.php',93 'type' => 'module', 94 'link' => 'https://www.php.net/manual/en/curl.installation.php', 66 95 'name' => 'curl', 67 ), 68 array( 69 'type' => 'value', 70 'name' => 'allow_url_fopen', 96 'required' => false, // Not strictly required, fallback exists 71 97 ), 72 98 ); 73 99 74 if ( $requirements ) : 75 $loaded_php_extensions = get_loaded_extensions(); 76 if ( $loaded_php_extensions ) : 77 foreach ( $requirements as $requirement ) : 78 $requirement_type = $requirement['type']; 79 $requirement_url = $requirement['link']; 80 $requirement_name = $requirement['name']; 81 switch ( $requirement_type ) : 82 case 'module': 83 if ( !in_array( $requirement_name, $loaded_php_extensions, true ) ) : 84 echo '<div class="notice notice-error is-dismissible"><p>'; 85 printf( "Le module PHP <a href=' . $requirement_url . '><strong>$requirement_name</strong></a> n'est pas installé. <br> L'extension ne fonctionnera pas correctement. <br> Veuillez l'installer et réactiver le plugin." ); 86 echo '</p><button type="button" class="notice-dismiss"><span class="screen-reader-text">Ignorer cette notification.</span></button></div>'; 87 endif; 88 break; 89 case 'value': 90 if ( !ini_get( $requirement_name ) ) : 91 echo '<div class="notice notice-error is-dismissible"><p>'; 92 printf( '<strong>' . $requirement_name . '</strong> is not enable in the php.ini file, activate it and retry.' ); 93 echo '</p><button type="button" class="notice-dismiss"><span class="screen-reader-text">Ignorer cette notification.</span></button></div>'; 94 endif; 95 break; 96 endswitch; 97 endforeach; 98 endif; 99 endif; 100 $errors = array(); 101 $loaded_php_extensions = get_loaded_extensions(); 102 103 foreach ( $requirements as $requirement ) { 104 $requirement_type = $requirement['type']; 105 $requirement_url = $requirement['link']; 106 $requirement_name = $requirement['name']; 107 $is_required = $requirement['required']; 108 109 switch ( $requirement_type ) { 110 case 'module': 111 if ( ! in_array( $requirement_name, $loaded_php_extensions, true ) ) { 112 if ( $is_required ) { 113 $errors[] = sprintf( 114 /* translators: %1$s: requirement URL, %2$s: requirement name */ 115 __( 'The PHP module <a href="%1$s"><strong>%2$s</strong></a> is not installed. The plugin will not work correctly. Please install it and reactivate the plugin.', 'media-library-downloader' ), 116 esc_url( $requirement_url ), 117 esc_html( $requirement_name ) 118 ); 119 } 120 } 121 break; 122 123 case 'value': 124 if ( ! ini_get( $requirement_name ) ) { 125 $errors[] = sprintf( 126 /* translators: %s: PHP setting name */ 127 __( '<strong>%s</strong> is not enabled in the php.ini file. Please enable it and try again.', 'media-library-downloader' ), 128 esc_html( $requirement_name ) 129 ); 130 } 131 break; 132 } 133 } 134 135 // Display errors if any 136 if ( ! empty( $errors ) ) { 137 add_action( 'admin_notices', function() use ( $errors ) { 138 foreach ( $errors as $error ) { 139 echo '<div class="notice notice-error is-dismissible"><p>' . wp_kses_post( $error ) . '</p></div>'; 140 } 141 }); 142 } 143 144 return empty( $errors ); 100 145 } 101 146 … … 105 150 public function include_files() { 106 151 require MLD_INCLUDES . 'class-main.php'; 152 153 // Include admin class only in admin area 154 if ( is_admin() ) { 155 require MLD_INCLUDES . 'class-admin.php'; 156 } 107 157 } 108 158 } -
media-library-downloader/trunk/readme.txt
r3281245 r3353021 2 2 Contributors: devloper00 3 3 Donate link: https://ko-fi.com/devloper 4 Tags: library, media, files, download, downloader , WordPress4 Tags: library, media, files, download, downloader 5 5 Requires at least: 5.0 or higher 6 Tested up to: 6. 6.16 Tested up to: 6.8.2 7 7 Requires PHP: 5.6 8 Stable tag: 1. 3.38 Stable tag: 1.4.0 9 9 License: GPL v2 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html 11 11 12 Download multiples files / media in one click, from the WordPress media library 12 Professional media download solution with bulk operations, smart management, and enterprise-grade security for WordPress 13 13 14 14 == Description == 15 15 16 Natively WordPress doesn't offer the possibility to download files directly from the WordPress media library. With that simple extension you can download any files you need! 16 Transform your WordPress media library into a powerful download center! While WordPress doesn't provide native file download capabilities, Media Library Downloader bridges this gap with enterprise-grade functionality and professional user experience. 17 17 18 = Main features: = 18 Whether you need to download a single image or backup hundreds of media files, this plugin makes it effortless with intuitive one-click downloads, smart bulk operations, and advanced management features. 19 19 20 * Download single / multiple files 21 * Compatible with List / Grid view 22 * AJAX Method (No reload) 20 = Core Features: = 21 22 * **Single & Bulk Downloads** - Download individual files instantly or create ZIP archives from multiple selections 23 * **Universal Compatibility** - Works seamlessly with both List and Grid view layouts 24 * **Smart Download Management** - Automatic file organization with customizable naming patterns 25 * **Progress Indicators** - Real-time feedback during download preparation 26 * **AJAX Technology** - Lightning-fast downloads with no page refreshes 27 28 = Advanced Features: = 29 30 * **Admin Settings Dashboard** - Complete control panel for configuration and monitoring 31 * **Download Activity Logging** - Track usage patterns and generate statistics 32 * **Automatic Cleanup** - Scheduled maintenance to keep your server optimized 33 * **Security & Permissions** - Enterprise-grade access control and CSRF protection 34 * **Accessibility Compliant** - Full keyboard navigation and screen reader support 35 * **Developer Friendly** - Extensive hooks and filters for customization 36 37 = Perfect For: = 38 39 * **Content Creators** - Quickly download media for offline editing 40 * **Site Administrators** - Bulk backup and migration of media files 41 * **Agencies** - Client asset delivery and portfolio management 42 * **Developers** - Media file management during development 43 * **Anyone** - Who needs efficient file access from WordPress media library 44 45 Experience the difference of professional-grade media management with intuitive design and powerful functionality! 23 46 24 47 == Installation == … … 27 50 2. Activate the plugin through the 'Plugins' menu in WordPress 28 51 52 == How to Use == 53 54 Once the plugin is installed and activated, you can start downloading files from your media library immediately. 55 56 = Downloading Single Files = 57 58 1. Go to your WordPress admin area 59 2. Navigate to **Media > Library** 60 3. You can use either List view or Grid view 61 4. Locate the file you want to download 62 5. Click the **Download** button that appears next to each media file 63 6. The file will be downloaded to your computer 64 65 = Downloading Multiple Files = 66 67 1. Go to **Media > Library** 68 2. Switch to List view for easier bulk selection 69 3. Use the checkboxes to select multiple files you want to download 70 4. Click the **Bulk Download** button 71 5. All selected files will be packaged and downloaded as a ZIP file 72 73 = View Options = 74 75 The plugin works seamlessly with both WordPress media library view options: 76 77 * **List View**: Shows files in a table format with download buttons in each row 78 * **Grid View**: Displays files as thumbnails with download options accessible via hover or click 79 80 = Plugin Settings = 81 82 1. Go to **Settings > Media Downloader** in your WordPress admin 83 2. Configure maximum download size limits (default: 100MB) 84 3. Set automatic cleanup intervals for temporary files (default: 24 hours) 85 4. Enable download logging to track activity (optional) 86 5. Customize ZIP filename patterns with placeholders like {timestamp}, {date}, {user} 87 6. Use manual cleanup to remove temporary files immediately 88 89 = Important Notes = 90 91 * Downloads are processed using AJAX, so there's no page reload 92 * Large files or multiple file downloads may take a few moments to process 93 * Ensure your browser allows downloads from your WordPress site 94 * The plugin respects WordPress user permissions - only users with appropriate media access can download files 95 * Temporary ZIP files are automatically cleaned up based on your settings 96 * Download activity can be logged and viewed in the admin dashboard 97 29 98 == Changelog == 99 100 = 1.4.0 = 101 * **MAJOR UPDATE**: Complete security and feature overhaul 102 * Added CSRF protection with nonces for all AJAX requests 103 * Improved input validation and sanitization 104 * Added file access permission checks 105 * Enhanced security for temporary file handling 106 * **NEW**: Single file download functionality 107 * **NEW**: Download progress indicators and better user feedback 108 * **NEW**: Individual download buttons in both List and Grid views 109 * **NEW**: Admin settings page in Settings > Media Downloader 110 * **NEW**: Configurable maximum download size limits 111 * **NEW**: Automatic cleanup scheduling with configurable intervals 112 * **NEW**: Optional download activity logging and statistics 113 * **NEW**: Custom ZIP filename patterns with placeholders 114 * **NEW**: Enhanced accessibility with ARIA labels and keyboard navigation 115 * **NEW**: Developer hooks and filters for extensibility 116 * Added manual cleanup option in admin settings 117 * Added download statistics dashboard 118 * Fixed critical logic bug in temp folder cleanup 119 * Optimized memory usage for large file downloads 120 * Added file size limits and duplicate filename handling 121 * Improved error handling with detailed messages 122 * Better internationalization support 123 * Modern JavaScript with ES6 class structure 124 * Added comprehensive code documentation 125 * Improved WordPress cron integration for automatic maintenance 126 * Enhanced user IP tracking for security logs 127 * Better error handling in admin interface 128 * Performance optimizations and WordPress coding standards compliance 30 129 31 130 = 1.3.3 =
Note: See TracChangeset
for help on using the changeset viewer.