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
wp-attachments / inc / meta-box.php

meta-box.php in WP Attachments – Smarter File Management & Download Lists 6.1, at inc/meta-box.php

1,225 lines 44.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 ?>