| 1 |
<?php |
| 2 |
|
| 3 |
class WP_Attachments |
| 4 |
{ |
| 5 |
private $actions = array( |
| 6 |
'add_meta_boxes', 'admin_enqueue_scripts', |
| 7 |
'wp_ajax_wpa_realign', 'wp_ajax_wpa_attach_media', // renamed from ij_ |
| 8 |
'wp_ajax_wpa_unattach_media', 'wp_ajax_wpa_delete_media' |
| 9 |
); |
| 10 |
|
| 11 |
private static $instance; |
| 12 |
|
| 13 |
private function __construct() |
| 14 |
{ |
| 15 |
foreach ($this->actions as $action) |
| 16 |
add_action($action, array($this, $action)); |
| 17 |
} |
| 18 |
|
| 19 |
static public function getInstance() |
| 20 |
{ |
| 21 |
if (!isset(self::$instance)) |
| 22 |
self::$instance = new WP_Attachments(); |
| 23 |
|
| 24 |
return self::$instance; |
| 25 |
} |
| 26 |
|
| 27 |
public function __clone() |
| 28 |
{ |
| 29 |
throw new Exception("Clone is disallowed."); |
| 30 |
} |
| 31 |
|
| 32 |
public function add_meta_boxes() |
| 33 |
{ |
| 34 |
// Get all post types that support attachments |
| 35 |
$post_types = get_post_types(array('public' => true), 'names'); |
| 36 |
foreach ($post_types as $post_type) { |
| 37 |
if ($post_type === 'attachment') continue; |
| 38 |
// Check if enabled for this post type |
| 39 |
if (get_option('wpatt_enable_metabox_' . $post_type, '1') !== '1') continue; |
| 40 |
add_meta_box( |
| 41 |
'wpa-attachments', |
| 42 |
__('Media Attachments', 'wp-attachments'), |
| 43 |
array($this, 'printMetaBox'), |
| 44 |
$post_type, |
| 45 |
'normal', |
| 46 |
'high', |
| 47 |
array( |
| 48 |
'__back_compat_meta_box' => false, |
| 49 |
'__block_editor_compatible_meta_box' => true |
| 50 |
) |
| 51 |
); |
| 52 |
} |
| 53 |
} |
| 54 |
|
| 55 |
/** |
| 56 |
* Is the current admin screen one that actually shows the metabox? |
| 57 |
* |
| 58 |
* get_current_screen()->base covers post.php and post-new.php for every |
| 59 |
* post type, and works in both the classic and the block editor. |
| 60 |
*/ |
| 61 |
private function isMetaBoxScreen() |
| 62 |
{ |
| 63 |
if (!function_exists('get_current_screen')) { |
| 64 |
return false; |
| 65 |
} |
| 66 |
|
| 67 |
$screen = get_current_screen(); |
| 68 |
if (!$screen || $screen->base !== 'post' || $screen->post_type === 'attachment') { |
| 69 |
return false; |
| 70 |
} |
| 71 |
|
| 72 |
return get_option('wpatt_enable_metabox_' . $screen->post_type, '1') === '1'; |
| 73 |
} |
| 74 |
|
| 75 |
public function admin_enqueue_scripts() |
| 76 |
{ |
| 77 |
// Without this guard the media library, the metabox script and all of |
| 78 |
// the CSS loaded on every single admin screen. |
| 79 |
if (!$this->isMetaBoxScreen()) { |
| 80 |
return; |
| 81 |
} |
| 82 |
|
| 83 |
wp_enqueue_media(); |
| 84 |
|
| 85 |
wp_enqueue_script( |
| 86 |
'wp-attachments', |
| 87 |
plugin_dir_url(__FILE__) . 'scripts/metabox.js', |
| 88 |
array('jquery-ui-sortable'), |
| 89 |
WPATT_VERSION, |
| 90 |
true |
| 91 |
); |
| 92 |
|
| 93 |
wp_localize_script('wp-attachments', 'WP_Attachments_Vars', array( |
| 94 |
'editMedia' => __('Edit Media', 'wp-attachments'), |
| 95 |
'youSure' => __('Are you sure you want to do this?', 'wp-attachments'), |
| 96 |
'confirmDelete' => __('Are you sure you want to delete this file permanently?', 'wp-attachments'), |
| 97 |
'mediaFrameTitle' => __('Add Media Attachments', 'wp-attachments'), |
| 98 |
'mediaFrameButton' => __('Attach to Post', 'wp-attachments'), |
| 99 |
'saveFirst' => __('Please save this content before adding attachments.', 'wp-attachments'), |
| 100 |
/* translators: 1: file name, 2: title of the content it is currently attached to, 3: that content's ID. */ |
| 101 |
'reattachWarning' => __('The file "%1$s" is already attached to "%2$s" (ID: %3$s).' . "\n" |
| 102 |
. 'Attaching it here will detach it from its current location.' . "\n\n" |
| 103 |
. 'Do you want to proceed?', 'wp-attachments'), |
| 104 |
/* translators: 1: new position, 2: total number of files. */ |
| 105 |
'movedTo' => __('Moved to position %1$s of %2$s', 'wp-attachments'), |
| 106 |
'doneEditing' => __('Done', 'wp-attachments'), |
| 107 |
'noTitle' => __('(no title)', 'wp-attachments'), |
| 108 |
/* translators: %s: attachment title. */ |
| 109 |
'previewLabel' => __('Preview %s', 'wp-attachments'), |
| 110 |
'previewFallback' => __('File Preview', 'wp-attachments'), |
| 111 |
'previewUnavailable' => __('Preview not available for this file type.', 'wp-attachments'), |
| 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'), |
| 117 |
'postID' => get_the_ID(), // Use get_the_ID() instead of $_GET |
| 118 |
'ajaxurl' => admin_url('admin-ajax.php'), |
| 119 |
'nonce' => wp_create_nonce('wpa-attachments-nonce') |
| 120 |
)); |
| 121 |
|
| 122 |
// Inline-only stylesheet: registering a handle keeps the CSS inside the |
| 123 |
// normal dependency pipeline instead of echoing it mid-request. |
| 124 |
wp_register_style('wpa-metabox', false, array(), WPATT_VERSION); |
| 125 |
wp_enqueue_style('wpa-metabox'); |
| 126 |
wp_add_inline_style('wpa-metabox', $this->getStyles()); |
| 127 |
} |
| 128 |
|
| 129 |
/** |
| 130 |
* Metabox stylesheet. |
| 131 |
* |
| 132 |
* Everything is scoped under .wpa-attachments-wrapper so nothing leaks into |
| 133 |
* the rest of wp-admin, and the accent colour follows the admin colour |
| 134 |
* scheme wherever the block editor exposes its custom properties. |
| 135 |
*/ |
| 136 |
private function getStyles() |
| 137 |
{ |
| 138 |
return <<<'CSS' |
| 139 |
.wpa-attachments-wrapper { |
| 140 |
--wpa-accent: var(--wp-admin-theme-color, #2271b1); |
| 141 |
--wpa-accent-dark: var(--wp-admin-theme-color-darker-10, #135e96); |
| 142 |
--wpa-text: #1d2327; |
| 143 |
--wpa-muted: #646970; |
| 144 |
--wpa-border: #dcdcde; |
| 145 |
--wpa-border-strong: #c3c4c7; |
| 146 |
--wpa-surface: #fff; |
| 147 |
--wpa-surface-alt: #f6f7f7; |
| 148 |
--wpa-danger: #d63638; |
| 149 |
--wpa-radius: 4px; |
| 150 |
font-size: 13px; |
| 151 |
line-height: 1.5; |
| 152 |
color: var(--wpa-text); |
| 153 |
} |
| 154 |
|
| 155 |
/* ---------- stats bar ---------- */ |
| 156 |
.wpa-attachments-wrapper .wpa-attachments-stats { |
| 157 |
display: flex; |
| 158 |
flex-wrap: wrap; |
| 159 |
align-items: center; |
| 160 |
gap: 4px 18px; |
| 161 |
margin: 0 0 12px; |
| 162 |
padding: 0 0 12px; |
| 163 |
border-bottom: 1px solid var(--wpa-border); |
| 164 |
color: var(--wpa-muted); |
| 165 |
font-size: 12px; |
| 166 |
} |
| 167 |
.wpa-attachments-wrapper .wpa-attachments-stat { |
| 168 |
display: inline-flex; |
| 169 |
align-items: center; |
| 170 |
gap: 5px; |
| 171 |
} |
| 172 |
.wpa-attachments-wrapper .wpa-stat-icon { |
| 173 |
display: block; |
| 174 |
width: 15px; |
| 175 |
height: 15px; |
| 176 |
color: var(--wpa-border-strong); |
| 177 |
} |
| 178 |
.wpa-attachments-wrapper .wpa-attachments-stat strong { |
| 179 |
font-weight: 600; |
| 180 |
color: var(--wpa-text); |
| 181 |
} |
| 182 |
|
| 183 |
/* ---------- list ---------- */ |
| 184 |
.wpa-attachments-wrapper .wpa-attachment-list { |
| 185 |
display: flex; |
| 186 |
flex-direction: column; |
| 187 |
gap: 8px; |
| 188 |
margin: 0; |
| 189 |
} |
| 190 |
.wpa-attachments-wrapper .wpa-attachment-item { |
| 191 |
display: grid; |
| 192 |
grid-template-columns: auto auto minmax(0, 1fr) auto; |
| 193 |
align-items: center; |
| 194 |
gap: 12px; |
| 195 |
padding: 10px 12px; |
| 196 |
background: var(--wpa-surface); |
| 197 |
border: 1px solid var(--wpa-border); |
| 198 |
border-radius: var(--wpa-radius); |
| 199 |
transition: border-color .15s ease, box-shadow .15s ease, opacity .2s ease; |
| 200 |
} |
| 201 |
.wpa-attachments-wrapper .wpa-attachment-item.is-busy { |
| 202 |
opacity: .5; |
| 203 |
pointer-events: none; |
| 204 |
} |
| 205 |
.wpa-attachments-wrapper .wpa-attachment-item:hover { |
| 206 |
border-color: var(--wpa-border-strong); |
| 207 |
box-shadow: 0 1px 2px rgba(0, 0, 0, .06); |
| 208 |
} |
| 209 |
.wpa-attachments-wrapper .wpa-attachment-item:focus-within { |
| 210 |
border-color: var(--wpa-accent); |
| 211 |
box-shadow: 0 0 0 1px var(--wpa-accent); |
| 212 |
} |
| 213 |
.wpa-attachments-wrapper .wpa-attachment-item.ui-sortable-helper { |
| 214 |
border-color: var(--wpa-accent); |
| 215 |
box-shadow: 0 6px 18px rgba(0, 0, 0, .16); |
| 216 |
} |
| 217 |
.wpa-attachments-wrapper .wpa-attachment-item.ui-sortable-placeholder { |
| 218 |
visibility: visible !important; |
| 219 |
background: var(--wpa-surface-alt); |
| 220 |
border: 1px dashed var(--wpa-accent); |
| 221 |
box-shadow: none; |
| 222 |
} |
| 223 |
|
| 224 |
/* ---------- drag handle ---------- */ |
| 225 |
.wpa-attachments-wrapper .wpa-attachment-move { |
| 226 |
display: flex; |
| 227 |
flex-direction: column; |
| 228 |
align-items: center; |
| 229 |
gap: 1px; |
| 230 |
} |
| 231 |
.wpa-attachments-wrapper .wpa-move, |
| 232 |
.wpa-attachments-wrapper .wpa-attachment-drag-handle { |
| 233 |
display: flex; |
| 234 |
align-items: center; |
| 235 |
justify-content: center; |
| 236 |
width: 22px; |
| 237 |
margin: 0; |
| 238 |
padding: 0; |
| 239 |
border: 0; |
| 240 |
border-radius: 3px; |
| 241 |
background: none; |
| 242 |
/* #c3c4c7 on white is roughly 1.9:1 -- effectively invisible. */ |
| 243 |
color: #787c82; |
| 244 |
transition: color .15s ease, background .15s ease, box-shadow .15s ease; |
| 245 |
} |
| 246 |
.wpa-attachments-wrapper .wpa-move { |
| 247 |
height: 16px; |
| 248 |
cursor: pointer; |
| 249 |
} |
| 250 |
.wpa-attachments-wrapper .wpa-attachment-drag-handle { |
| 251 |
height: 20px; |
| 252 |
cursor: grab; |
| 253 |
} |
| 254 |
.wpa-attachments-wrapper .wpa-attachment-drag-handle:active { cursor: grabbing; } |
| 255 |
.wpa-attachments-wrapper .wpa-chevron { display: block; width: 13px; height: 13px; } |
| 256 |
.wpa-attachments-wrapper .wpa-move:hover:not(:disabled), |
| 257 |
.wpa-attachments-wrapper .wpa-attachment-drag-handle:hover { |
| 258 |
color: var(--wpa-accent); |
| 259 |
background: #f0f0f1; |
| 260 |
} |
| 261 |
.wpa-attachments-wrapper .wpa-move:disabled { |
| 262 |
opacity: .35; |
| 263 |
cursor: default; |
| 264 |
} |
| 265 |
.wpa-attachments-wrapper .wpa-move:focus-visible, |
| 266 |
.wpa-attachments-wrapper .wpa-attachment-drag-handle:focus-visible { |
| 267 |
outline: none; |
| 268 |
color: var(--wpa-accent); |
| 269 |
box-shadow: 0 0 0 2px var(--wpa-accent); |
| 270 |
} |
| 271 |
.wpa-attachments-wrapper .wpa-grip { |
| 272 |
display: block; |
| 273 |
width: 10px; |
| 274 |
height: 16px; |
| 275 |
fill: currentColor; |
| 276 |
} |
| 277 |
|
| 278 |
|
| 279 |
/* ---------- preview thumb ---------- */ |
| 280 |
.wpa-attachments-wrapper .wpa-attachment-preview { |
| 281 |
position: relative; |
| 282 |
width: 44px; |
| 283 |
height: 44px; |
| 284 |
border: 1px solid var(--wpa-border); |
| 285 |
border-radius: 3px; |
| 286 |
background: var(--wpa-surface-alt); |
| 287 |
overflow: hidden; |
| 288 |
flex-shrink: 0; |
| 289 |
} |
| 290 |
.wpa-attachments-wrapper .wpa-preview-trigger, |
| 291 |
.wpa-attachments-wrapper .wpa-preview-static { |
| 292 |
display: flex; |
| 293 |
align-items: center; |
| 294 |
justify-content: center; |
| 295 |
width: 100%; |
| 296 |
height: 100%; |
| 297 |
margin: 0; |
| 298 |
padding: 0; |
| 299 |
border: 0; |
| 300 |
background: none; |
| 301 |
color: inherit; /* takes the file-type colour from the tile */ |
| 302 |
} |
| 303 |
.wpa-attachments-wrapper .wpa-preview-trigger { cursor: pointer; } |
| 304 |
.wpa-attachments-wrapper .wpa-preview-trigger:hover { background: rgba(0, 0, 0, .05); } |
| 305 |
.wpa-attachments-wrapper .wpa-preview-trigger:focus-visible { |
| 306 |
outline: 2px solid var(--wpa-accent); |
| 307 |
outline-offset: -2px; |
| 308 |
} |
| 309 |
.wpa-attachments-wrapper .wpa-preview-trigger img, |
| 310 |
.wpa-attachments-wrapper .wpa-preview-static img { |
| 311 |
width: 100%; |
| 312 |
height: 100%; |
| 313 |
object-fit: cover; |
| 314 |
display: block; |
| 315 |
} |
| 316 |
.wpa-attachments-wrapper .wpa-file-icon { |
| 317 |
display: block; |
| 318 |
width: 26px; |
| 319 |
height: 26px; |
| 320 |
} |
| 321 |
|
| 322 |
/* |
| 323 |
* File-type colours. The tile carries both the tint and the colour; the SVG |
| 324 |
* picks the colour up through currentColor. |
| 325 |
*/ |
| 326 |
.wpa-attachments-wrapper .wpa-attachment-preview { color: var(--wpa-muted); } |
| 327 |
.wpa-attachments-wrapper .wpa-file--pdf { color: #c9282a; background: #fdf1f1; border-color: #f4d4d4; } |
| 328 |
.wpa-attachments-wrapper .wpa-file--doc { color: #1c5f96; background: #eef4fa; border-color: #cfe0ee; } |
| 329 |
.wpa-attachments-wrapper .wpa-file--sheet { color: #0a7040; background: #edf7f1; border-color: #cbe7d8; } |
| 330 |
.wpa-attachments-wrapper .wpa-file--slides { color: #a35a00; background: #fdf4e9; border-color: #f2ddc2; } |
| 331 |
.wpa-attachments-wrapper .wpa-file--archive { color: #7a6100; background: #fbf6e3; border-color: #ebdfb4; } |
| 332 |
.wpa-attachments-wrapper .wpa-file--audio { color: #6d47a1; background: #f4effb; border-color: #ded1f0; } |
| 333 |
.wpa-attachments-wrapper .wpa-file--video { color: #2f45c5; background: #eef0fd; border-color: #d2d8f6; } |
| 334 |
.wpa-attachments-wrapper .wpa-file--image { color: #0a6f6a; background: #ecf6f5; border-color: #c8e4e2; } |
| 335 |
.wpa-attachments-wrapper .wpa-file--code { color: #3c434a; background: #f0f1f2; border-color: #dcdcde; } |
| 336 |
.wpa-attachments-wrapper .wpa-file--text { color: #4b5157; background: #f2f3f4; border-color: #dcdcde; } |
| 337 |
.wpa-attachments-wrapper .wpa-preview-badge { |
| 338 |
position: absolute; |
| 339 |
top: 50%; |
| 340 |
left: 50%; |
| 341 |
transform: translate(-50%, -50%); |
| 342 |
width: 22px; |
| 343 |
height: 22px; |
| 344 |
pointer-events: none; |
| 345 |
} |
| 346 |
.wpa-attachments-wrapper .wpa-preview-badge svg { display: block; width: 22px; height: 22px; } |
| 347 |
|
| 348 |
/* ---------- file info ---------- */ |
| 349 |
.wpa-attachments-wrapper .wpa-attachment-info { min-width: 0; } |
| 350 |
.wpa-attachments-wrapper .wpa-attachment-title { |
| 351 |
font-weight: 600; |
| 352 |
margin-bottom: 2px; |
| 353 |
} |
| 354 |
.wpa-attachments-wrapper .wpa-attachment-title a { |
| 355 |
display: block; |
| 356 |
color: var(--wpa-text); |
| 357 |
text-decoration: none; |
| 358 |
overflow: hidden; |
| 359 |
text-overflow: ellipsis; |
| 360 |
white-space: nowrap; |
| 361 |
} |
| 362 |
.wpa-attachments-wrapper .wpa-attachment-title a:hover, |
| 363 |
.wpa-attachments-wrapper .wpa-attachment-title a:focus { color: var(--wpa-accent); } |
| 364 |
.wpa-attachments-wrapper .wpa-attachment-meta { |
| 365 |
display: flex; |
| 366 |
flex-wrap: wrap; |
| 367 |
align-items: center; |
| 368 |
gap: 3px 8px; |
| 369 |
font-size: 12px; |
| 370 |
color: var(--wpa-muted); |
| 371 |
} |
| 372 |
.wpa-attachments-wrapper .wpa-attachment-type { |
| 373 |
font-size: 10px; |
| 374 |
font-weight: 600; |
| 375 |
letter-spacing: .03em; |
| 376 |
padding: 1px 5px; |
| 377 |
border: 1px solid var(--wpa-border); |
| 378 |
border-radius: 2px; |
| 379 |
background: var(--wpa-surface-alt); |
| 380 |
} |
| 381 |
|
| 382 |
/* ---------- row actions ---------- */ |
| 383 |
.wpa-attachments-wrapper .wpa-attachment-actions { |
| 384 |
display: flex; |
| 385 |
gap: 4px; |
| 386 |
} |
| 387 |
.wpa-attachments-wrapper .wpa-attachment-actions .button { |
| 388 |
display: inline-flex; |
| 389 |
align-items: center; |
| 390 |
justify-content: center; |
| 391 |
width: 30px; |
| 392 |
height: 30px; |
| 393 |
min-height: 30px; |
| 394 |
padding: 0; |
| 395 |
color: #7c8185; |
| 396 |
background: transparent; |
| 397 |
border-color: transparent; |
| 398 |
box-shadow: none; |
| 399 |
transition: background .15s ease, color .15s ease, border-color .15s ease; |
| 400 |
} |
| 401 |
.wpa-attachments-wrapper .wpa-action-icon { display: block; width: 18px; height: 18px; } |
| 402 |
.wpa-attachments-wrapper .wpa-attachment-actions .button:hover { |
| 403 |
color: var(--wpa-accent-dark); |
| 404 |
background: #f0f0f1; |
| 405 |
border-color: #dcdcde; |
| 406 |
} |
| 407 |
.wpa-attachments-wrapper .wpa-attachment-actions .button:focus-visible { |
| 408 |
color: var(--wpa-accent); |
| 409 |
border-color: var(--wpa-accent); |
| 410 |
box-shadow: 0 0 0 1px var(--wpa-accent); |
| 411 |
} |
| 412 |
.wpa-attachments-wrapper .wpa-attachment-actions .wpa-delete-action:hover { |
| 413 |
color: var(--wpa-danger); |
| 414 |
background: #fcf0f0; |
| 415 |
border-color: #f1c9c9; |
| 416 |
} |
| 417 |
|
| 418 |
/* ---------- empty state ---------- */ |
| 419 |
.wpa-attachments-wrapper .wpa-no-attachments { |
| 420 |
text-align: center; |
| 421 |
padding: 32px 20px; |
| 422 |
border: 1px dashed var(--wpa-border-strong); |
| 423 |
border-radius: var(--wpa-radius); |
| 424 |
background: var(--wpa-surface-alt); |
| 425 |
color: var(--wpa-muted); |
| 426 |
} |
| 427 |
.wpa-attachments-wrapper .wpa-no-attachments .dashicons { |
| 428 |
font-size: 36px; |
| 429 |
width: 36px; |
| 430 |
height: 36px; |
| 431 |
color: var(--wpa-border-strong); |
| 432 |
} |
| 433 |
.wpa-attachments-wrapper .wpa-no-attachments h4 { |
| 434 |
margin: 10px 0 4px; |
| 435 |
font-size: 13px; |
| 436 |
font-weight: 600; |
| 437 |
color: var(--wpa-text); |
| 438 |
} |
| 439 |
.wpa-attachments-wrapper .wpa-no-attachments p { margin: 0; font-size: 12px; } |
| 440 |
|
| 441 |
/* ---------- footer ---------- */ |
| 442 |
.wpa-attachments-wrapper .wpa-attachments-footer { |
| 443 |
display: flex; |
| 444 |
justify-content: space-between; |
| 445 |
align-items: center; |
| 446 |
flex-wrap: wrap; |
| 447 |
gap: 12px; |
| 448 |
margin-top: 14px; |
| 449 |
padding-top: 14px; |
| 450 |
border-top: 1px solid var(--wpa-border); |
| 451 |
} |
| 452 |
.wpa-attachments-wrapper .wpa-toggle-wrapper { |
| 453 |
display: inline-flex; |
| 454 |
align-items: center; |
| 455 |
gap: 8px; |
| 456 |
} |
| 457 |
.wpa-attachments-wrapper .wpa-toggle-wrapper label { |
| 458 |
margin: 0; |
| 459 |
cursor: pointer; |
| 460 |
color: var(--wpa-text); |
| 461 |
} |
| 462 |
.wpa-attachments-wrapper .wpa-toggle-wrapper input[type="checkbox"] { |
| 463 |
appearance: none; |
| 464 |
-webkit-appearance: none; |
| 465 |
position: relative; |
| 466 |
flex-shrink: 0; |
| 467 |
width: 36px; |
| 468 |
height: 20px; |
| 469 |
min-width: 36px; |
| 470 |
margin: 0; |
| 471 |
padding: 0; |
| 472 |
border: 0; |
| 473 |
border-radius: 10px; |
| 474 |
background: var(--wpa-border-strong); |
| 475 |
box-shadow: none; |
| 476 |
cursor: pointer; |
| 477 |
transition: background .15s ease; |
| 478 |
} |
| 479 |
.wpa-attachments-wrapper .wpa-toggle-wrapper input[type="checkbox"]::before, |
| 480 |
.wpa-attachments-wrapper .wpa-toggle-wrapper input[type="checkbox"]:checked::before { |
| 481 |
content: ""; |
| 482 |
display: block; |
| 483 |
position: absolute; |
| 484 |
top: 2px; |
| 485 |
left: 2px; |
| 486 |
width: 16px; |
| 487 |
height: 16px; |
| 488 |
margin: 0; |
| 489 |
border-radius: 50%; |
| 490 |
background: #fff; |
| 491 |
box-shadow: 0 1px 2px rgba(0, 0, 0, .25); |
| 492 |
transition: transform .15s ease; |
| 493 |
} |
| 494 |
.wpa-attachments-wrapper .wpa-toggle-wrapper input[type="checkbox"]:checked { background: var(--wpa-accent); } |
| 495 |
.wpa-attachments-wrapper .wpa-toggle-wrapper input[type="checkbox"]:checked::before { transform: translateX(16px); } |
| 496 |
.wpa-attachments-wrapper .wpa-toggle-wrapper input[type="checkbox"]:focus-visible { |
| 497 |
outline: 2px solid var(--wpa-accent); |
| 498 |
outline-offset: 2px; |
| 499 |
} |
| 500 |
|
| 501 |
/* ---------- preview modal ---------- */ |
| 502 |
.wpa-preview-modal { |
| 503 |
display: none; |
| 504 |
position: fixed; |
| 505 |
top: 0; |
| 506 |
right: 0; |
| 507 |
bottom: 0; |
| 508 |
left: 0; |
| 509 |
z-index: 100050; |
| 510 |
padding: 24px 16px; |
| 511 |
background: rgba(0, 0, 0, .75); |
| 512 |
overflow: auto; |
| 513 |
} |
| 514 |
.wpa-preview-modal.is-open { display: block; } |
| 515 |
body.wpa-modal-open { overflow: hidden; } |
| 516 |
.wpa-preview-modal .wpa-preview-content { |
| 517 |
position: relative; |
| 518 |
width: 100%; |
| 519 |
max-width: 880px; |
| 520 |
margin: 0 auto; |
| 521 |
padding: 18px 22px 22px; |
| 522 |
background: #fff; |
| 523 |
border-radius: 4px; |
| 524 |
box-shadow: 0 10px 40px rgba(0, 0, 0, .3); |
| 525 |
} |
| 526 |
.wpa-preview-modal .wpa-preview-heading { |
| 527 |
margin: 0 40px 14px 0; |
| 528 |
padding: 0; |
| 529 |
font-size: 15px; |
| 530 |
line-height: 1.4; |
| 531 |
color: #1d2327; |
| 532 |
word-break: break-word; |
| 533 |
} |
| 534 |
.wpa-preview-modal .wpa-preview-close { |
| 535 |
position: absolute; |
| 536 |
top: 12px; |
| 537 |
right: 12px; |
| 538 |
display: inline-flex; |
| 539 |
align-items: center; |
| 540 |
justify-content: center; |
| 541 |
width: 32px; |
| 542 |
height: 32px; |
| 543 |
padding: 0; |
| 544 |
border: 0; |
| 545 |
border-radius: 3px; |
| 546 |
background: none; |
| 547 |
color: #646970; |
| 548 |
cursor: pointer; |
| 549 |
} |
| 550 |
.wpa-preview-modal .wpa-preview-close:hover { background: #f0f0f1; color: #1d2327; } |
| 551 |
.wpa-preview-modal .wpa-preview-close:focus-visible { |
| 552 |
outline: 2px solid var(--wp-admin-theme-color, #2271b1); |
| 553 |
outline-offset: 1px; |
| 554 |
} |
| 555 |
.wpa-preview-modal .wpa-preview-file { text-align: center; } |
| 556 |
.wpa-preview-modal .wpa-preview-file img, |
| 557 |
.wpa-preview-modal .wpa-preview-file video { |
| 558 |
max-width: 100%; |
| 559 |
max-height: 70vh; |
| 560 |
border-radius: 2px; |
| 561 |
} |
| 562 |
.wpa-preview-modal .wpa-preview-file audio { width: 100%; } |
| 563 |
.wpa-preview-modal .wpa-preview-file iframe { |
| 564 |
width: 100%; |
| 565 |
height: 70vh; |
| 566 |
border: 0; |
| 567 |
border-radius: 2px; |
| 568 |
} |
| 569 |
.wpa-preview-modal .wpa-preview-file pre { |
| 570 |
text-align: left; |
| 571 |
max-height: 60vh; |
| 572 |
overflow: auto; |
| 573 |
padding: 14px; |
| 574 |
border-radius: 3px; |
| 575 |
background: #f6f7f7; |
| 576 |
font-size: 12px; |
| 577 |
} |
| 578 |
|
| 579 |
/* ---------- narrow screens ---------- */ |
| 580 |
@media screen and (max-width: 782px) { |
| 581 |
.wpa-attachments-wrapper .wpa-attachment-item { |
| 582 |
grid-template-columns: auto auto minmax(0, 1fr); |
| 583 |
row-gap: 10px; |
| 584 |
} |
| 585 |
.wpa-attachments-wrapper .wpa-attachment-actions { |
| 586 |
grid-column: 1 / -1; |
| 587 |
justify-content: flex-end; |
| 588 |
} |
| 589 |
.wpa-attachments-wrapper .wpa-attachment-actions .button { |
| 590 |
width: 34px; |
| 591 |
height: 34px; |
| 592 |
min-height: 34px; |
| 593 |
} |
| 594 |
.wpa-attachments-wrapper .wpa-attachments-footer { flex-direction: column; align-items: stretch; } |
| 595 |
} |
| 596 |
|
| 597 |
@media (prefers-reduced-motion: reduce) { |
| 598 |
.wpa-attachments-wrapper *, |
| 599 |
.wpa-preview-modal * { |
| 600 |
transition: none !important; |
| 601 |
animation: none !important; |
| 602 |
} |
| 603 |
} |
| 604 |
CSS; |
| 605 |
} |
| 606 |
|
| 607 |
/** |
| 608 |
* Build a nonce-protected unattach URL for a given attachment. |
| 609 |
*/ |
| 610 |
private function getUnattachUrl($attachment_id) |
| 611 |
{ |
| 612 |
$attachment_id = absint($attachment_id); |
| 613 |
|
| 614 |
// Nonce added via add_query_arg rather than wp_nonce_url(): the latter |
| 615 |
// returns an already esc_html'd URL, which the template would then |
| 616 |
// escape a second time. |
| 617 |
return add_query_arg( |
| 618 |
array( |
| 619 |
'page' => 'unattach', |
| 620 |
'noheader' => 'true', |
| 621 |
'id' => $attachment_id, |
| 622 |
'referer' => remove_query_arg('message'), |
| 623 |
'_wpnonce' => wp_create_nonce('wpa_unattach_' . $attachment_id), |
| 624 |
), |
| 625 |
admin_url('tools.php') |
| 626 |
); |
| 627 |
} |
| 628 |
|
| 629 |
/** |
| 630 |
* Row-action icons, drawn to stay legible at 18px. |
| 631 |
*/ |
| 632 |
private function getActionIcon($name) |
| 633 |
{ |
| 634 |
$paths = array( |
| 635 |
'view' => '<path d="M2.5 12S6 5.75 12 5.75 21.5 12 21.5 12 18 18.25 12 18.25 2.5 12 2.5 12Z"/>' |
| 636 |
. '<circle cx="12" cy="12" r="2.75"/>', |
| 637 |
|
| 638 |
'edit' => '<path d="M4 20h4L19.5 8.5a2.12 2.12 0 0 0-3-3L5 17v3Z"/>' |
| 639 |
. '<path d="M14.5 6.5 18.5 10.5"/>', |
| 640 |
|
| 641 |
// Two chain halves pulling apart: a clean "detach" at small sizes. |
| 642 |
'unlink' => '<path d="M10.5 13.5 8 16a3.9 3.9 0 0 1-5.5-5.5L5 8"/>' |
| 643 |
. '<path d="M13.5 10.5 16 8a3.9 3.9 0 0 1 5.5 5.5L19 16"/>', |
| 644 |
|
| 645 |
'trash' => '<path d="M4 6.75h16"/>' |
| 646 |
. '<path d="M9.5 6.75V5.5A1.5 1.5 0 0 1 11 4h2a1.5 1.5 0 0 1 1.5 1.5v1.25"/>' |
| 647 |
. '<path d="M6.75 6.75 7.5 19a2 2 0 0 0 2 1.9h5a2 2 0 0 0 2-1.9l.75-12.25"/>' |
| 648 |
. '<path d="M10.25 10.5v6M13.75 10.5v6"/>', |
| 649 |
); |
| 650 |
|
| 651 |
$d = isset($paths[$name]) ? $paths[$name] : ''; |
| 652 |
|
| 653 |
return '<svg class="wpa-action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"' |
| 654 |
. ' stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"' |
| 655 |
. ' aria-hidden="true" focusable="false">' . $d . '</svg>'; |
| 656 |
} |
| 657 |
|
| 658 |
/** |
| 659 |
* Chevron used by the move up / move down buttons. |
| 660 |
*/ |
| 661 |
private function getChevronSvg($direction) |
| 662 |
{ |
| 663 |
$d = ('up' === $direction) ? 'm6 15 6-6 6 6' : 'm6 9 6 6 6-6'; |
| 664 |
|
| 665 |
return '<svg class="wpa-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor"' |
| 666 |
. ' stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"' |
| 667 |
. ' aria-hidden="true" focusable="false"><path d="' . $d . '"/></svg>'; |
| 668 |
} |
| 669 |
|
| 670 |
/** |
| 671 |
* Six-dot grip. Reads as "drag me" far better than a hamburger glyph. |
| 672 |
*/ |
| 673 |
private function getDragHandleSvg() |
| 674 |
{ |
| 675 |
return '<svg class="wpa-grip" viewBox="0 0 10 16" aria-hidden="true" focusable="false">' |
| 676 |
. '<circle cx="2.5" cy="3" r="1.35"/><circle cx="7.5" cy="3" r="1.35"/>' |
| 677 |
. '<circle cx="2.5" cy="8" r="1.35"/><circle cx="7.5" cy="8" r="1.35"/>' |
| 678 |
. '<circle cx="2.5" cy="13" r="1.35"/><circle cx="7.5" cy="13" r="1.35"/>' |
| 679 |
. '</svg>'; |
| 680 |
} |
| 681 |
|
| 682 |
/** |
| 683 |
* Thumbnail (or file-type icon) for one attachment. |
| 684 |
* |
| 685 |
* Previewable types render as a real <button> so the preview is reachable |
| 686 |
* by keyboard and needs no inline onclick handler; anything else renders as |
| 687 |
* a plain decorative icon. |
| 688 |
*/ |
| 689 |
private function getAttachmentPreview($attachment) |
| 690 |
{ |
| 691 |
$attachment_id = $attachment->ID; |
| 692 |
$mime_type = (string) $attachment->post_mime_type; |
| 693 |
$attachment_url = wp_get_attachment_url($attachment_id); |
| 694 |
$title = $attachment->post_title; |
| 695 |
|
| 696 |
$is_image = strpos($mime_type, 'image/') === 0; |
| 697 |
$is_video = strpos($mime_type, 'video/') === 0; |
| 698 |
|
| 699 |
$thumb = ($is_image || $is_video) |
| 700 |
? wp_get_attachment_image_src($attachment_id, 'thumbnail') |
| 701 |
: false; |
| 702 |
|
| 703 |
if ($thumb) { |
| 704 |
// Decorative: the button below carries the accessible name. |
| 705 |
$inner = '<img src="' . esc_url($thumb[0]) . '" alt="" loading="lazy" />'; |
| 706 |
if ($is_video) { |
| 707 |
$inner .= '<span class="wpa-preview-badge" aria-hidden="true">' |
| 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>' |
| 709 |
. '</span>'; |
| 710 |
} |
| 711 |
} else { |
| 712 |
$inner = wpatt_get_file_icon_svg(wpatt_get_file_type($mime_type)); |
| 713 |
} |
| 714 |
|
| 715 |
$previewable = $is_image |
| 716 |
|| $is_video |
| 717 |
|| strpos($mime_type, 'audio/') === 0 |
| 718 |
|| strpos($mime_type, 'text/') === 0 |
| 719 |
|| $mime_type === 'application/pdf'; |
| 720 |
|
| 721 |
if (!$previewable || !$attachment_url) { |
| 722 |
return '<span class="wpa-preview-static">' . $inner . '</span>'; |
| 723 |
} |
| 724 |
|
| 725 |
return sprintf( |
| 726 |
'<button type="button" class="wpa-preview-trigger" data-url="%1$s" data-mime="%2$s" data-title="%3$s"><span class="screen-reader-text">%4$s</span>%5$s</button>', |
| 727 |
esc_url($attachment_url), |
| 728 |
esc_attr($mime_type), |
| 729 |
esc_attr($title), |
| 730 |
/* translators: %s: attachment title. */ |
| 731 |
esc_html(sprintf(__('Preview %s', 'wp-attachments'), $title)), |
| 732 |
$inner |
| 733 |
); |
| 734 |
} |
| 735 |
|
| 736 |
private function formatDate($date) |
| 737 |
{ |
| 738 |
$post_date = strtotime($date); |
| 739 |
$time_diff = time() - $post_date; |
| 740 |
if ($time_diff < DAY_IN_SECONDS) { |
| 741 |
return human_time_diff($post_date) . ' ' . __('ago'); |
| 742 |
} |
| 743 |
return date_i18n('M j, Y', $post_date); |
| 744 |
} |
| 745 |
|
| 746 |
/** |
| 747 |
* Count and total byte size for a set of attachments. |
| 748 |
* |
| 749 |
* @param array $attachments WP_Post objects or attachment IDs. |
| 750 |
* @return array With 'count' and 'size' keys. |
| 751 |
*/ |
| 752 |
private function getStats($attachments) |
| 753 |
{ |
| 754 |
$count = 0; |
| 755 |
$size = 0; |
| 756 |
|
| 757 |
foreach ((array) $attachments as $attachment) { |
| 758 |
$count++; |
| 759 |
$id = is_object($attachment) ? $attachment->ID : (int) $attachment; |
| 760 |
$bytes = wpatt_get_attachment_filesize($id); |
| 761 |
if (false !== $bytes) { |
| 762 |
$size += $bytes; |
| 763 |
} |
| 764 |
} |
| 765 |
|
| 766 |
return array('count' => $count, 'size' => $size); |
| 767 |
} |
| 768 |
|
| 769 |
/** |
| 770 |
* Output the contents of the stats bar. Also reused by the attach AJAX |
| 771 |
* response so the counters stay correct without a page reload. |
| 772 |
*/ |
| 773 |
private function renderStats($stats) |
| 774 |
{ |
| 775 |
printf( |
| 776 |
'<span class="wpa-attachments-stat">' |
| 777 |
. '<svg class="wpa-stat-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" aria-hidden="true" focusable="false">' |
| 778 |
. '<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z"/><path d="M13.75 3.25V8h4.75"/>' |
| 779 |
. '</svg>%s</span>', |
| 780 |
sprintf( |
| 781 |
/* translators: %s: number of attached files. */ |
| 782 |
esc_html(_n('%s file', '%s files', $stats['count'], 'wp-attachments')), |
| 783 |
'<strong>' . esc_html(number_format_i18n($stats['count'])) . '</strong>' |
| 784 |
) |
| 785 |
); |
| 786 |
|
| 787 |
printf( |
| 788 |
'<span class="wpa-attachments-stat">' |
| 789 |
. '<svg class="wpa-stat-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" aria-hidden="true" focusable="false">' |
| 790 |
. '<path d="m12 3 9 4.5-9 4.5-9-4.5L12 3Z"/><path d="m3 12 9 4.5 9-4.5"/><path d="m3 16.5 9 4.5 9-4.5"/>' |
| 791 |
. '</svg>%s</span>', |
| 792 |
sprintf( |
| 793 |
/* translators: %s: combined size of the attached files. */ |
| 794 |
esc_html__('%s total', 'wp-attachments'), |
| 795 |
'<strong>' . esc_html(wpatt_format_bytes($stats['size'], 1)) . '</strong>' |
| 796 |
) |
| 797 |
); |
| 798 |
} |
| 799 |
|
| 800 |
/** |
| 801 |
* Placeholder shown inside the list container when nothing is attached. |
| 802 |
*/ |
| 803 |
private function printEmptyState() |
| 804 |
{ |
| 805 |
?> |
| 806 |
<div class="wpa-no-attachments"> |
| 807 |
<span class="dashicons dashicons-admin-media" aria-hidden="true"></span> |
| 808 |
<h4><?php esc_html_e('No media attached yet', 'wp-attachments'); ?></h4> |
| 809 |
<?php if (current_user_can('upload_files')) : ?> |
| 810 |
<p><?php esc_html_e('Use the Add Media button below to attach files to this content.', 'wp-attachments'); ?></p> |
| 811 |
<?php endif; ?> |
| 812 |
</div> |
| 813 |
<?php |
| 814 |
} |
| 815 |
|
| 816 |
public function printMetaBox($post) |
| 817 |
{ |
| 818 |
$attachments = new WP_Query(array( |
| 819 |
'post_parent' => $post->ID, |
| 820 |
'post_type' => 'attachment', |
| 821 |
'post_status' => 'any', |
| 822 |
'orderby' => 'menu_order', |
| 823 |
'order' => 'ASC', |
| 824 |
'posts_per_page' => -1 |
| 825 |
)); |
| 826 |
|
| 827 |
$stats = $this->getStats($attachments->posts); |
| 828 |
|
| 829 |
$is_off = !wpatt_is_display_enabled($post); |
| 830 |
$frontend_on = wpatt_is_frontend_enabled($post->post_type); |
| 831 |
?> |
| 832 |
|
| 833 |
<div class="wpa-attachments-wrapper"> |
| 834 |
<div class="wpa-attachments-stats" id="wpa-attachments-stats"> |
| 835 |
<?php $this->renderStats($stats); ?> |
| 836 |
</div> |
| 837 |
|
| 838 |
<?php |
| 839 |
/* |
| 840 |
* The list container is rendered unconditionally. It used to exist |
| 841 |
* only when the post already had attachments, so the first file |
| 842 |
* attached through the media modal was appended to an empty jQuery |
| 843 |
* set and silently vanished until the page was reloaded. |
| 844 |
*/ |
| 845 |
?> |
| 846 |
<div class="wpa-attachment-list" id="wpa-attachment-list"> |
| 847 |
<?php if (!empty($attachments->posts)) : ?> |
| 848 |
<?php foreach ($attachments->posts as $attachment) : ?> |
| 849 |
<?php $this->print_single_attachment_item($attachment); ?> |
| 850 |
<?php endforeach; ?> |
| 851 |
<?php else : ?> |
| 852 |
<?php $this->printEmptyState(); ?> |
| 853 |
<?php endif; ?> |
| 854 |
</div> |
| 855 |
|
| 856 |
<p id="wpa-reorder-help" class="screen-reader-text"><?php |
| 857 |
esc_html_e('Press the up or down arrow key to move this file in the list.', 'wp-attachments'); |
| 858 |
?></p> |
| 859 |
<div id="wpa-reorder-status" class="screen-reader-text" role="status" aria-live="polite"></div> |
| 860 |
|
| 861 |
<div class="wpa-attachments-footer"> |
| 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; ?> |
| 870 |
<?php if (current_user_can('upload_files')) : ?> |
| 871 |
<div class="wpa-attachments-footer-buttons"> |
| 872 |
<?php // type="button": inside the edit form a bare <button> defaults to submit. ?> |
| 873 |
<button type="button" class="button button-primary add_media wpa_attach_file"> |
| 874 |
<?php esc_html_e('Add Media', 'wp-attachments'); ?> |
| 875 |
</button> |
| 876 |
</div> |
| 877 |
<?php endif; ?> |
| 878 |
</div> |
| 879 |
</div> |
| 880 |
|
| 881 |
<!-- File Preview Modal --> |
| 882 |
<div id="wpa-preview-modal" class="wpa-preview-modal" role="dialog" aria-modal="true" aria-labelledby="wpa-preview-title" aria-hidden="true"> |
| 883 |
<div class="wpa-preview-content"> |
| 884 |
<button type="button" class="wpa-preview-close" aria-label="<?php esc_attr_e('Close preview', 'wp-attachments'); ?>"> |
| 885 |
<span class="dashicons dashicons-no-alt" aria-hidden="true"></span> |
| 886 |
</button> |
| 887 |
<h2 id="wpa-preview-title" class="wpa-preview-heading"></h2> |
| 888 |
<div id="wpa-preview-file" class="wpa-preview-file"></div> |
| 889 |
</div> |
| 890 |
</div> |
| 891 |
|
| 892 |
<input type="hidden" name="wpa_checkfieldpreventautosaveonnewcpt" value="1" /> |
| 893 |
|
| 894 |
<?php |
| 895 |
} |
| 896 |
|
| 897 |
// AJAX: Attach media to post |
| 898 |
public function wp_ajax_wpa_attach_media() // renamed from ij_attach_media |
| 899 |
{ |
| 900 |
check_ajax_referer('wpa-attachments-nonce', 'nonce'); |
| 901 |
if (!current_user_can('upload_files')) { |
| 902 |
wp_send_json_error('Permission denied'); |
| 903 |
} |
| 904 |
$attachment_id = intval($_POST['attachment_id']); |
| 905 |
$post_id = intval($_POST['post_id']); |
| 906 |
if (!$attachment_id || !$post_id) { |
| 907 |
wp_send_json_error('Missing data'); |
| 908 |
} |
| 909 |
|
| 910 |
// Object-level authorization: verify user can edit both the target post and the attachment |
| 911 |
if (!current_user_can('edit_post', $post_id)) { |
| 912 |
wp_send_json_error('Permission denied: cannot edit target post'); |
| 913 |
} |
| 914 |
if (!current_user_can('edit_post', $attachment_id)) { |
| 915 |
wp_send_json_error('Permission denied: cannot edit attachment'); |
| 916 |
} |
| 917 |
|
| 918 |
wp_update_post(array( |
| 919 |
'ID' => $attachment_id, |
| 920 |
'post_parent' => $post_id |
| 921 |
)); |
| 922 |
|
| 923 |
// Get the attachment object |
| 924 |
$attachment = get_post($attachment_id); |
| 925 |
if (!$attachment) { |
| 926 |
wp_send_json_error('Attachment not found'); |
| 927 |
} |
| 928 |
|
| 929 |
// Generate the HTML for the new attachment item |
| 930 |
ob_start(); |
| 931 |
$this->print_single_attachment_item($attachment); |
| 932 |
$html = ob_get_clean(); |
| 933 |
|
| 934 |
// Refreshed counters, so the stats bar stays truthful without a reload. |
| 935 |
$sibling_ids = get_posts(array( |
| 936 |
'post_parent' => $post_id, |
| 937 |
'post_type' => 'attachment', |
| 938 |
'post_status' => 'any', |
| 939 |
'posts_per_page' => -1, |
| 940 |
'fields' => 'ids', |
| 941 |
)); |
| 942 |
|
| 943 |
ob_start(); |
| 944 |
$this->renderStats($this->getStats($sibling_ids)); |
| 945 |
$stats = ob_get_clean(); |
| 946 |
|
| 947 |
wp_send_json_success(array( |
| 948 |
'html' => $html, |
| 949 |
'stats' => $stats, |
| 950 |
)); |
| 951 |
} |
| 952 |
|
| 953 |
// AJAX: Re-align attachments |
| 954 |
public function wp_ajax_wpa_realign() // renamed from ij_realign |
| 955 |
{ |
| 956 |
check_ajax_referer('wpa-attachments-nonce', 'nonce'); |
| 957 |
|
| 958 |
if (!current_user_can('edit_posts')) { |
| 959 |
wp_send_json_error('Permission denied', 403); |
| 960 |
} |
| 961 |
|
| 962 |
$alignment = isset($_REQUEST['alignment']) ? wp_unslash($_REQUEST['alignment']) : array(); |
| 963 |
if (!is_array($alignment)) { |
| 964 |
$alignment = explode(',', $alignment); |
| 965 |
} |
| 966 |
$alignment = array_values(array_filter(array_map('absint', $alignment))); |
| 967 |
|
| 968 |
$updated = 0; |
| 969 |
$skipped = 0; |
| 970 |
|
| 971 |
foreach ($alignment as $i => $attachment_id) { |
| 972 |
$attachment = get_post($attachment_id); |
| 973 |
|
| 974 |
if (!$attachment || $attachment->post_type !== 'attachment') { |
| 975 |
continue; |
| 976 |
} |
| 977 |
|
| 978 |
// Object-level authorization: never trust a blanket 'edit_posts'. |
| 979 |
if (!current_user_can('edit_post', $attachment_id)) { |
| 980 |
$skipped++; |
| 981 |
continue; |
| 982 |
} |
| 983 |
|
| 984 |
if ((int) $attachment->menu_order === $i) { |
| 985 |
continue; // Already in place, skip the write. |
| 986 |
} |
| 987 |
|
| 988 |
// Update only menu_order: passing the whole post object re-runs |
| 989 |
// kses on post_content for users without 'unfiltered_html'. |
| 990 |
wp_update_post(array( |
| 991 |
'ID' => $attachment_id, |
| 992 |
'menu_order' => $i, |
| 993 |
)); |
| 994 |
$updated++; |
| 995 |
} |
| 996 |
|
| 997 |
wp_send_json_success(array( |
| 998 |
'updated' => $updated, |
| 999 |
'skipped' => $skipped, |
| 1000 |
)); |
| 1001 |
} |
| 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 |
|
| 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 |
/** |
| 1080 |
* Render one row. Single source of truth for the item markup: printMetaBox() |
| 1081 |
* loops over it, and the attach AJAX handler buffers it for the response. |
| 1082 |
*/ |
| 1083 |
private function print_single_attachment_item($attachment) |
| 1084 |
{ |
| 1085 |
$attachment_id = $attachment->ID; |
| 1086 |
$attachment_url = wp_get_attachment_url($attachment_id); |
| 1087 |
$attachment_mime = sanitize_title($attachment->post_mime_type); |
| 1088 |
$file_bytes = wpatt_get_attachment_filesize($attachment_id); |
| 1089 |
$file_type = wpatt_get_file_type($attachment->post_mime_type); |
| 1090 |
|
| 1091 |
$title = ('' !== trim((string) $attachment->post_title)) |
| 1092 |
? $attachment->post_title |
| 1093 |
: __('(no title)', 'wp-attachments'); |
| 1094 |
|
| 1095 |
$file_size = (false !== $file_bytes) |
| 1096 |
? wpatt_format_bytes($file_bytes, 1) |
| 1097 |
: __('Not found', 'wp-attachments'); |
| 1098 |
|
| 1099 |
$formatted_date = $this->formatDate($attachment->post_date); |
| 1100 |
|
| 1101 |
// parse_url() first: a query string would otherwise end up in the extension. |
| 1102 |
$url_path = $attachment_url ? parse_url($attachment_url, PHP_URL_PATH) : ''; |
| 1103 |
$file_extension = $url_path ? strtoupper(pathinfo($url_path, PATHINFO_EXTENSION)) : ''; |
| 1104 |
?> |
| 1105 |
<div class="wpa-attachment-item mime-<?php echo esc_attr($attachment_mime); ?>" |
| 1106 |
data-mimetype="<?php echo esc_attr($attachment->post_mime_type); ?>" |
| 1107 |
data-attachmentid="<?php echo esc_attr($attachment_id); ?>" |
| 1108 |
data-url="<?php echo esc_url($attachment_url); ?>" |
| 1109 |
data-title="<?php echo esc_attr($title); ?>"> |
| 1110 |
|
| 1111 |
<div class="wpa-attachment-move"> |
| 1112 |
<button type="button" class="wpa-move wpa-move-up" |
| 1113 |
title="<?php esc_attr_e('Move up', 'wp-attachments'); ?>"> |
| 1114 |
<?php echo $this->getChevronSvg('up'); ?> |
| 1115 |
<span class="screen-reader-text"><?php |
| 1116 |
/* translators: %s: attachment title. */ |
| 1117 |
echo esc_html( sprintf( __( 'Move %s up', 'wp-attachments' ), $title ) ); |
| 1118 |
?></span> |
| 1119 |
</button> |
| 1120 |
|
| 1121 |
<button type="button" |
| 1122 |
class="wpa-attachment-drag-handle" |
| 1123 |
aria-describedby="wpa-reorder-help" |
| 1124 |
title="<?php esc_attr_e('Drag to reorder', 'wp-attachments'); ?>"> |
| 1125 |
<span class="screen-reader-text"><?php |
| 1126 |
/* translators: %s: attachment title. */ |
| 1127 |
echo esc_html( sprintf( __( 'Reorder %s', 'wp-attachments' ), $title ) ); |
| 1128 |
?></span> |
| 1129 |
<?php echo $this->getDragHandleSvg(); ?> |
| 1130 |
</button> |
| 1131 |
|
| 1132 |
<button type="button" class="wpa-move wpa-move-down" |
| 1133 |
title="<?php esc_attr_e('Move down', 'wp-attachments'); ?>"> |
| 1134 |
<?php echo $this->getChevronSvg('down'); ?> |
| 1135 |
<span class="screen-reader-text"><?php |
| 1136 |
/* translators: %s: attachment title. */ |
| 1137 |
echo esc_html( sprintf( __( 'Move %s down', 'wp-attachments' ), $title ) ); |
| 1138 |
?></span> |
| 1139 |
</button> |
| 1140 |
</div> |
| 1141 |
|
| 1142 |
<div class="wpa-attachment-preview wpa-file--<?php echo esc_attr($file_type); ?>"> |
| 1143 |
<?php echo $this->getAttachmentPreview($attachment); ?> |
| 1144 |
</div> |
| 1145 |
|
| 1146 |
<div class="wpa-attachment-info"> |
| 1147 |
<div class="wpa-attachment-title"> |
| 1148 |
<a href="<?php echo esc_url($attachment_url); ?>" |
| 1149 |
target="_blank" |
| 1150 |
rel="noopener noreferrer" |
| 1151 |
title="<?php echo esc_attr($title); ?>"><?php echo esc_html($title); ?></a> |
| 1152 |
</div> |
| 1153 |
<div class="wpa-attachment-meta"> |
| 1154 |
<?php if ($file_extension) : ?> |
| 1155 |
<span class="wpa-attachment-type"><?php echo esc_html($file_extension); ?></span> |
| 1156 |
<?php endif; ?> |
| 1157 |
<span class="wpa-attachment-size"><?php echo esc_html($file_size); ?></span> |
| 1158 |
<span class="wpa-attachment-date"><?php echo esc_html($formatted_date); ?></span> |
| 1159 |
</div> |
| 1160 |
</div> |
| 1161 |
|
| 1162 |
<div class="wpa-attachment-actions"> |
| 1163 |
<a href="<?php echo esc_url($attachment_url); ?>" |
| 1164 |
class="button button-secondary" |
| 1165 |
title="<?php esc_attr_e('View', 'wp-attachments'); ?>" |
| 1166 |
target="_blank" |
| 1167 |
rel="noopener noreferrer"> |
| 1168 |
<?php echo $this->getActionIcon('view'); ?> |
| 1169 |
<span class="screen-reader-text"><?php esc_html_e('View', 'wp-attachments'); ?></span> |
| 1170 |
</a> |
| 1171 |
<a href="<?php echo esc_url(admin_url('post.php?post=' . $attachment_id . '&action=edit')); ?>" |
| 1172 |
class="button button-secondary wpa-edit-attachment" |
| 1173 |
title="<?php esc_attr_e('Edit', 'wp-attachments'); ?>" |
| 1174 |
target="_blank"> |
| 1175 |
<?php echo $this->getActionIcon('edit'); ?> |
| 1176 |
<span class="screen-reader-text"><?php esc_html_e('Edit', 'wp-attachments'); ?></span> |
| 1177 |
</a> |
| 1178 |
<a href="<?php echo esc_url($this->getUnattachUrl($attachment_id)); ?>" |
| 1179 |
class="button button-secondary wpa-unattach-action" |
| 1180 |
title="<?php esc_attr_e('Unattach', 'wp-attachments'); ?>"> |
| 1181 |
<?php echo $this->getActionIcon('unlink'); ?> |
| 1182 |
<span class="screen-reader-text"><?php esc_html_e('Unattach', 'wp-attachments'); ?></span> |
| 1183 |
</a> |
| 1184 |
<a href="<?php echo esc_url(add_query_arg('forcedelete', 'true', get_delete_post_link($attachment_id, '', true))); ?>" |
| 1185 |
class="button button-secondary wpa-delete-action" |
| 1186 |
title="<?php esc_attr_e('Delete permanently', 'wp-attachments'); ?>"> |
| 1187 |
<?php echo $this->getActionIcon('trash'); ?> |
| 1188 |
<span class="screen-reader-text"><?php esc_html_e('Delete permanently', 'wp-attachments'); ?></span> |
| 1189 |
</a> |
| 1190 |
</div> |
| 1191 |
</div> |
| 1192 |
<?php |
| 1193 |
} |
| 1194 |
} |
| 1195 |
|
| 1196 |
$WP_Attachments = WP_Attachments::getInstance(); |
| 1197 |
|
| 1198 |
add_action('save_post', function($post_id) { |
| 1199 |
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { |
| 1200 |
return; |
| 1201 |
} |
| 1202 |
if (empty($post_id) || !current_user_can('edit_post', $post_id)) { |
| 1203 |
return; |
| 1204 |
} |
| 1205 |
if (!isset($_POST['wpa_checkfieldpreventautosaveonnewcpt'])) { |
| 1206 |
// Don't update meta if our box wasn't submitted (e.g. auto-draft) |
| 1207 |
return; |
| 1208 |
} |
| 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'); |
| 1221 |
} else { |
| 1222 |
update_post_meta($post_id, 'wpa_off', '1'); |
| 1223 |
} |
| 1224 |
}); |
| 1225 |
?> |