PluginProbe
WP Attachments – Smarter File Management & Download Lists / 6.1
WP Attachments – Smarter File Management & Download Lists v6.1
6.1 6.0.2 6.0.3 trunk 2.0 3 3.0.1 3.0.2 3.0.3 3.1 3.1.1 3.1.2 3.1.3 3.1.4 3.2 3.2.1 3.2.2 3.2.3 3.2.4 3.3 3.4 3.5 3.5.1 3.5.2 3.5.3 All 53 releases
← All changes | inc/meta-box.php +121 -151 6.0.2 → 6.1 View file →
@@ -3,9 +3,10 @@
3 3 class WP_Attachments
4 4 {
5 5 private $actions = array(
6 6 'add_meta_boxes', 'admin_enqueue_scripts',
7 - 'wp_ajax_wpa_realign', 'wp_ajax_wpa_attach_media' // renamed from ij_
7 + 'wp_ajax_wpa_realign', 'wp_ajax_wpa_attach_media', // renamed from ij_
8 + 'wp_ajax_wpa_unattach_media', 'wp_ajax_wpa_delete_media'
8 9 );
9 10
10 11 private static $instance;
11 12
@@ -84,9 +85,9 @@
84 85 wp_enqueue_script(
85 86 'wp-attachments',
86 87 plugin_dir_url(__FILE__) . 'scripts/metabox.js',
87 88 array('jquery-ui-sortable'),
88 - '6.0',
89 + WPATT_VERSION,
89 90 true
90 91 );
91 92
92 93 wp_localize_script('wp-attachments', 'WP_Attachments_Vars', array(
@@ -108,8 +109,12 @@
108 109 'previewLabel' => __('Preview %s', 'wp-attachments'),
109 110 'previewFallback' => __('File Preview', 'wp-attachments'),
110 111 'previewUnavailable' => __('Preview not available for this file type.', 'wp-attachments'),
111 112 'downloadFile' => __('Download file', 'wp-attachments'),
113 + 'fileUnattached' => __('File unattached.', 'wp-attachments'),
114 + 'fileDeleted' => __('File deleted.', 'wp-attachments'),
115 + 'actionFailed' => __('The file could not be changed.', 'wp-attachments'),
116 + 'requestFailed' => __('The request failed. Please try again.', 'wp-attachments'),
112 117 'postID' => get_the_ID(), // Use get_the_ID() instead of $_GET
113 118 'ajaxurl' => admin_url('admin-ajax.php'),
114 119 'nonce' => wp_create_nonce('wpa-attachments-nonce')
115 120 ));
@@ -115,9 +120,9 @@
115 120 ));
116 121
117 122 // Inline-only stylesheet: registering a handle keeps the CSS inside the
118 123 // normal dependency pipeline instead of echoing it mid-request.
119 - wp_register_style('wpa-metabox', false, array(), '6.0');
124 + wp_register_style('wpa-metabox', false, array(), WPATT_VERSION);
120 125 wp_enqueue_style('wpa-metabox');
121 126 wp_add_inline_style('wpa-metabox', $this->getStyles());
122 127 }
123 128
@@ -190,10 +195,14 @@
190 195 padding: 10px 12px;
191 196 background: var(--wpa-surface);
192 197 border: 1px solid var(--wpa-border);
193 198 border-radius: var(--wpa-radius);
194 - transition: border-color .15s ease, box-shadow .15s ease;
199 + transition: border-color .15s ease, box-shadow .15s ease, opacity .2s ease;
195 200 }
201 +.wpa-attachments-wrapper .wpa-attachment-item.is-busy {
202 + opacity: .5;
203 + pointer-events: none;
204 +}
196 205 .wpa-attachments-wrapper .wpa-attachment-item:hover {
197 206 border-color: var(--wpa-border-strong);
198 207 box-shadow: 0 1px 2px rgba(0, 0, 0, .06);
199 208 }
@@ -617,124 +626,8 @@
617 626 );
618 627 }
619 628
620 629 /**
621 - * Reduce a MIME type to one of the icon families below.
622 - */
623 - private function getFileType($mime_type)
624 - {
625 - $mime_type = strtolower((string) $mime_type);
626 -
627 - // Checked in order: the first prefix that matches wins, so the more
628 - // specific entries (text/csv) come before the broad ones (text/).
629 - $prefixes = array(
630 - 'image/' => 'image',
631 - 'video/' => 'video',
632 - 'audio/' => 'audio',
633 - 'text/csv' => 'sheet',
634 - 'text/html' => 'code',
635 - 'text/' => 'text',
636 - );
637 - foreach ($prefixes as $prefix => $type) {
638 - if (strpos($mime_type, $prefix) === 0) {
639 - return $type;
640 - }
641 - }
642 -
643 - $needles = array(
644 - 'pdf' => 'pdf',
645 - 'wordprocessing' => 'doc',
646 - 'msword' => 'doc',
647 - 'opendocument.text' => 'doc',
648 - 'rtf' => 'doc',
649 - 'spreadsheet' => 'sheet',
650 - 'ms-excel' => 'sheet',
651 - 'presentation' => 'slides',
652 - 'ms-powerpoint' => 'slides',
653 - 'zip' => 'archive',
654 - 'compressed' => 'archive',
655 - 'tar' => 'archive',
656 - 'gzip' => 'archive',
657 - 'json' => 'code',
658 - 'xml' => 'code',
659 - 'javascript' => 'code',
660 - );
661 - foreach ($needles as $needle => $type) {
662 - if (strpos($mime_type, $needle) !== false) {
663 - return $type;
664 - }
665 - }
666 -
667 - return 'default';
668 - }
669 -
670 - /**
671 - * Inline SVG icon for a file family.
672 - *
673 - * Inline rather than a font or sprite: crisp at any pixel density, colour
674 - * comes from the tile via currentColor, and it costs no extra request.
675 - */
676 - private function getFileIconSvg($type)
677 - {
678 - $open = '<svg class="wpa-file-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">';
679 - $close = '</svg>';
680 -
681 - // Sheet of paper with a folded corner, shared by the document families.
682 - $page = '<path d="M14 2.75H7A2.25 2.25 0 0 0 4.75 5v14A2.25 2.25 0 0 0 7 21.25h10A2.25 2.25 0 0 0 19.25 19V8L14 2.75Z" fill="currentColor" fill-opacity=".13"/>'
683 - . '<path d="M14 2.75H7A2.25 2.25 0 0 0 4.75 5v14A2.25 2.25 0 0 0 7 21.25h10A2.25 2.25 0 0 0 19.25 19V8L14 2.75Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>'
684 - . '<path d="M13.75 3v4.25H18" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>';
685 -
686 - // Rounded frame shared by the media families.
687 - $frame = '<rect x="3.75" y="4.75" width="16.5" height="14.5" rx="2.25" fill="currentColor" fill-opacity=".13"/>'
688 - . '<rect x="3.75" y="4.75" width="16.5" height="14.5" rx="2.25" stroke="currentColor" stroke-width="1.5"/>';
689 -
690 - $glyphs = array(
691 - 'pdf' => $page
692 - . '<path d="M8 12.5h8M8 15.5h8M8 18.5h5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>',
693 -
694 - 'doc' => $page
695 - . '<path d="M8 12.5h8M8 15.5h8M8 18.5h5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>',
696 -
697 - 'text' => $page
698 - . '<path d="M8 12.5h8M8 15.5h8M8 18.5h5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>',
699 -
700 - 'sheet' => $page
701 - . '<rect x="7.25" y="12.25" width="9.5" height="6.5" rx="1" stroke="currentColor" stroke-width="1.5"/>'
702 - . '<path d="M12 12.25v6.5M7.25 15.5h9.5" stroke="currentColor" stroke-width="1.5"/>',
703 -
704 - 'slides' => $page
705 - . '<rect x="7.25" y="12.25" width="9.5" height="6.5" rx="1" stroke="currentColor" stroke-width="1.5"/>'
706 - . '<path d="M9.5 16.5l2-2 1.75 1.75 1.25-1.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>',
707 -
708 - 'code' => $page
709 - . '<path d="M10.25 12.75 7.75 15.5l2.5 2.75M13.75 12.75l2.5 2.75-2.5 2.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>',
710 -
711 - 'image' => $frame
712 - . '<circle cx="8.75" cy="10" r="1.5" fill="currentColor"/>'
713 - . '<path d="M4.75 17.5 9.5 12.75l3.25 3.25 2.25-1.75 4.25 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>',
714 -
715 - 'video' => $frame
716 - . '<path d="M10.25 9.25v5.5l5-2.75-5-2.75Z" fill="currentColor"/>',
717 -
718 - 'audio' => '<path d="M9.5 16.5V6.75l8.75-1.75V14.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
719 - . '<ellipse cx="7.25" cy="16.75" rx="2.75" ry="2.25" fill="currentColor"/>'
720 - . '<ellipse cx="16" cy="14.75" rx="2.75" ry="2.25" fill="currentColor"/>',
721 -
722 - 'archive' => '<path d="M4.75 7.75h14.5V19A2.25 2.25 0 0 1 17 21.25H7A2.25 2.25 0 0 1 4.75 19V7.75Z" fill="currentColor" fill-opacity=".13"/>'
723 - . '<rect x="3.75" y="3.75" width="16.5" height="4" rx="1.25" stroke="currentColor" stroke-width="1.5"/>'
724 - . '<path d="M5 7.75V19A2.25 2.25 0 0 0 7.25 21.25h9.5A2.25 2.25 0 0 0 19 19V7.75" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>'
725 - . '<path d="M10.25 11.5h3.5M10.25 14.5h3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>',
726 -
727 - 'default' => $page
728 - . '<path d="M8 13.5h8M8 16.5h5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>',
729 - );
730 -
731 - $glyph = isset($glyphs[$type]) ? $glyphs[$type] : $glyphs['default'];
732 -
733 - return $open . $glyph . $close;
734 - }
735 -
736 - /**
737 630 * Row-action icons, drawn to stay legible at 18px.
738 631 */
739 632 private function getActionIcon($name)
740 633 {
@@ -815,9 +708,9 @@
815 708 . '<svg viewBox="0 0 24 24" fill="none" focusable="false"><circle cx="12" cy="12" r="9" fill="rgba(0,0,0,.55)"/><path d="M10 8.5v7l5.5-3.5-5.5-3.5Z" fill="#fff"/></svg>'
816 709 . '</span>';
817 710 }
818 711 } else {
819 - $inner = $this->getFileIconSvg($this->getFileType($mime_type));
712 + $inner = wpatt_get_file_icon_svg(wpatt_get_file_type($mime_type));
820 713 }
821 714
822 715 $previewable = $is_image
823 716 || $is_video
@@ -862,12 +755,12 @@
862 755 $size = 0;
863 756
864 757 foreach ((array) $attachments as $attachment) {
865 758 $count++;
866 - $id = is_object($attachment) ? $attachment->ID : (int) $attachment;
867 - $path = get_attached_file($id);
868 - if ($path && file_exists($path)) {
869 - $size += filesize($path);
759 + $id = is_object($attachment) ? $attachment->ID : (int) $attachment;
760 + $bytes = wpatt_get_attachment_filesize($id);
761 + if (false !== $bytes) {
762 + $size += $bytes;
870 763 }
871 764 }
872 765
873 766 return array('count' => $count, 'size' => $size);
@@ -932,19 +825,10 @@
932 825 ));
933 826
934 827 $stats = $this->getStats($attachments->posts);
935 828
936 - // Get default ON/OFF for this post type
937 - $default_on = get_option('wpatt_enable_display_' . $post->post_type, '1');
938 - $is_off = get_post_meta($post->ID, 'wpa_off', true);
939 - // If meta not set, use default
940 - if ( $is_off == 1 ) {
941 - // Disabled by user
942 - } else if (in_array($post->post_status, array('auto-draft', 'draft', 'new'))) {
943 - $is_off = ($default_on === '1') ? '' : '1';
944 - } else {
945 - $is_off = ($default_on === '1') ? '' : '1';
946 - }
829 + $is_off = !wpatt_is_display_enabled($post);
830 + $frontend_on = wpatt_is_frontend_enabled($post->post_type);
947 831 ?>
948 832
949 833 <div class="wpa-attachments-wrapper">
950 834 <div class="wpa-attachments-stats" id="wpa-attachments-stats">
@@ -974,12 +858,16 @@
974 858 ?></p>
975 859 <div id="wpa-reorder-status" class="screen-reader-text" role="status" aria-live="polite"></div>
976 860
977 861 <div class="wpa-attachments-footer">
978 - <div class="wpa-toggle-wrapper">
979 - <input type="checkbox" id="wpa_off_n" name="wpa_off" <?php checked(!$is_off); ?> />
980 - <label for="wpa_off_n"><?php esc_html_e('Display attachments in frontend', 'wp-attachments'); ?></label>
981 - </div>
862 + <?php if ($frontend_on) : ?>
863 + <div class="wpa-toggle-wrapper">
864 + <input type="checkbox" id="wpa_off_n" name="wpa_off" <?php checked(!$is_off); ?> />
865 + <label for="wpa_off_n"><?php esc_html_e('Display attachments in frontend', 'wp-attachments'); ?></label>
866 + </div>
867 + <?php else : ?>
868 + <p class="description"><?php esc_html_e('The frontend list is disabled for this post type in the WP Attachments settings.', 'wp-attachments'); ?></p>
869 + <?php endif; ?>
982 870 <?php if (current_user_can('upload_files')) : ?>
983 871 <div class="wpa-attachments-footer-buttons">
984 872 <?php // type="button": inside the edit form a bare <button> defaults to submit. ?>
985 873 <button type="button" class="button button-primary add_media wpa_attach_file">
@@ -1111,9 +999,85 @@
1111 999 'skipped' => $skipped,
1112 1000 ));
1113 1001 }
1114 1002
1003 + // AJAX: unattach a file from the post being edited.
1004 + public function wp_ajax_wpa_unattach_media()
1005 + {
1006 + $this->handleRowAction('unattach');
1007 + }
1008 +
1009 + // AJAX: permanently delete a file of the post being edited.
1010 + public function wp_ajax_wpa_delete_media()
1011 + {
1012 + $this->handleRowAction('delete');
1013 + }
1014 +
1115 1015 /**
1016 + * Shared handler for the unattach and delete row actions.
1017 + *
1018 + * One file per request, on purpose. It must be attached to the given
1019 + * post and is checked on its own: a nonce plus the right to edit the
1020 + * post must not be enough to unattach or delete somebody else's file.
1021 + *
1022 + * @param string $action 'unattach' or 'delete'.
1023 + */
1024 + private function handleRowAction($action)
1025 + {
1026 + check_ajax_referer('wpa-attachments-nonce', 'nonce');
1027 +
1028 + $post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0;
1029 + if (!$post_id || !current_user_can('edit_post', $post_id)) {
1030 + wp_send_json_error(__('Permission denied', 'wp-attachments'), 403);
1031 + }
1032 +
1033 + $id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0;
1034 + $attachment = $id ? get_post($id) : null;
1035 +
1036 + if (!$attachment || $attachment->post_type !== 'attachment' || (int) $attachment->post_parent !== $post_id) {
1037 + wp_send_json_error(__('The file could not be changed.', 'wp-attachments'), 400);
1038 + }
1039 +
1040 + if ('delete' === $action) {
1041 + // wp_delete_attachment() also removes the files from disk.
1042 + $ok = current_user_can('delete_post', $id) && wp_delete_attachment($id, true);
1043 + } else {
1044 + $ok = current_user_can('edit_post', $id)
1045 + && !is_wp_error(wp_update_post(array('ID' => $id, 'post_parent' => 0), true));
1046 + }
1047 +
1048 + if (!$ok) {
1049 + wp_send_json_error(__('The file could not be changed.', 'wp-attachments'), 403);
1050 + }
1051 +
1052 + // Refreshed counters, and the empty state once nothing is left.
1053 + $remaining = get_posts(array(
1054 + 'post_parent' => $post_id,
1055 + 'post_type' => 'attachment',
1056 + 'post_status' => 'any',
1057 + 'posts_per_page' => -1,
1058 + 'fields' => 'ids',
1059 + ));
1060 +
1061 + ob_start();
1062 + $this->renderStats($this->getStats($remaining));
1063 + $stats = ob_get_clean();
1064 +
1065 + $empty = '';
1066 + if (empty($remaining)) {
1067 + ob_start();
1068 + $this->printEmptyState();
1069 + $empty = ob_get_clean();
1070 + }
1071 +
1072 + wp_send_json_success(array(
1073 + 'id' => $id,
1074 + 'stats' => $stats,
1075 + 'empty' => $empty,
1076 + ));
1077 + }
1078 +
1079 + /**
1116 1080 * Render one row. Single source of truth for the item markup: printMetaBox()
1117 1081 * loops over it, and the attach AJAX handler buffers it for the response.
1118 1082 */
1119 1083 private function print_single_attachment_item($attachment)
@@ -1120,17 +1084,17 @@
1120 1084 {
1121 1085 $attachment_id = $attachment->ID;
1122 1086 $attachment_url = wp_get_attachment_url($attachment_id);
1123 1087 $attachment_mime = sanitize_title($attachment->post_mime_type);
1124 - $attachment_path = get_attached_file($attachment_id);
1125 - $file_type = $this->getFileType($attachment->post_mime_type);
1088 + $file_bytes = wpatt_get_attachment_filesize($attachment_id);
1089 + $file_type = wpatt_get_file_type($attachment->post_mime_type);
1126 1090
1127 1091 $title = ('' !== trim((string) $attachment->post_title))
1128 1092 ? $attachment->post_title
1129 1093 : __('(no title)', 'wp-attachments');
1130 1094
1131 - $file_size = ($attachment_path && file_exists($attachment_path))
1132 - ? wpatt_format_bytes(filesize($attachment_path), 1)
1095 + $file_size = (false !== $file_bytes)
1096 + ? wpatt_format_bytes($file_bytes, 1)
1133 1097 : __('Not found', 'wp-attachments');
1134 1098
1135 1099 $formatted_date = $this->formatDate($attachment->post_date);
1136 1100
@@ -1241,15 +1205,21 @@
1241 1205 if (!isset($_POST['wpa_checkfieldpreventautosaveonnewcpt'])) {
1242 1206 // Don't update meta if our box wasn't submitted (e.g. auto-draft)
1243 1207 return;
1244 1208 }
1245 - if ( isset($_POST["wpa_off"]) ) {
1246 - delete_post_meta($post_id, "wpa_off");
1209 + if (wp_is_post_revision($post_id)) {
1210 + return;
1211 + }
1212 + // No toggle is printed while the frontend is off for this post type:
1213 + // keep the post's own choice instead of reading that as "hide".
1214 + if (!wpatt_is_frontend_enabled(get_post_type($post_id))) {
1215 + return;
1216 + }
1217 +
1218 + // The checkbox is named after the old meta but means "show".
1219 + if (isset($_POST['wpa_off'])) {
1220 + delete_post_meta($post_id, 'wpa_off');
1247 1221 } else {
1248 - update_post_meta($post_id, "wpa_off", isset($_POST["wpa_off"]) ? '' : '1');
1222 + update_post_meta($post_id, 'wpa_off', '1');
1249 1223 }
1250 -});
1251 -
1252 -add_action('plugins_loaded', function() {
1253 - load_plugin_textdomain('wp-attachments', false, dirname(plugin_basename(__FILE__)) . '/languages/');
1254 1224 });
1255 1225 ?>