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 | wp-attachments.php +591 -92 6.0.2 → 6.1 View file →
@@ -1,13 +1,13 @@
1 1 <?php
2 2 /*
3 -Plugin Name: WP Attachments
3 +Plugin Name: WP Attachments – Smarter File Management & Download Lists
4 4 Plugin URI: https://wordpress.org/plugins/wp-attachments
5 5 Description: Powerful solution to manage and show your WordPress media in posts and pages
6 6 Author: Marco Milesi
7 7 Author URI: https://www.marcomilesi.com
8 -Version: 6.0.2
9 -Requires at least: 4.4
8 +Version: 6.1
9 +Requires at least: 5.0
10 10 Requires PHP: 7.4
11 11 License: GPLv2 or later
12 12 License URI: http://www.gnu.org/licenses/gpl-2.0.html
13 13 Text Domain: wp-attachments
@@ -12,21 +12,115 @@
12 12 License URI: http://www.gnu.org/licenses/gpl-2.0.html
13 13 Text Domain: wp-attachments
14 14 */
15 15
16 -// Keep in sync with the Version header above.
17 -define( 'WPATT_VERSION', '6.0' );
16 +// Keep in sync with the Version header above: it versions the CSS and JS
17 +// URLs, so a stale value keeps browsers on the old files after an update.
18 +define( 'WPATT_VERSION', '6.1' );
18 19
19 20 require_once( plugin_dir_path(__FILE__) . 'inc/attach_unattach_reattach.php' );
21 +require_once( plugin_dir_path(__FILE__) . 'inc/file-types.php' );
20 22
21 23 add_action('init', function () {
22 24 load_plugin_textdomain( 'wp-attachments' );
25 +});
23 26
24 - $wpa_ict = (int) get_option('wpa_ict', 0);
25 - wp_enqueue_style('wpa-css', plugin_dir_url(__FILE__) . 'styles/' . $wpa_ict . '/wpa.css');
27 +// Frontend only: enqueueing on 'init' also loaded it in wp-admin and on the login screen.
28 +add_action('wp_enqueue_scripts', 'wpatt_enqueue_list_styles');
29 +
30 +// Settings screen: the template previews use the real list styles.
31 +add_action('admin_enqueue_scripts', function ($hook) {
32 + if ('settings_page_wpatt-option-page' === $hook) {
33 + wpatt_enqueue_list_styles();
34 + }
26 35 });
27 36
28 37 /**
38 + * Stylesheets of the attachments list: the shared one, then the icon pack.
39 + *
40 + * Also used by the settings screen, so its previews look like the site.
41 + */
42 +function wpatt_enqueue_list_styles() {
43 + $base = plugin_dir_url(__FILE__) . 'styles/';
44 +
45 + wp_enqueue_style('wpa-common', $base . 'common.css', array(), WPATT_VERSION);
46 + wp_enqueue_style('wpa-css', $base . wpatt_icon_pack() . '/wpa.css', array('wpa-common'), WPATT_VERSION);
47 +}
48 +
49 +/** Modern icon pack: inline SVG icons instead of PNG backgrounds. */
50 +define( 'WPATT_PACK_MODERN', 5 );
51 +
52 +/** Card template: always uses the Modern icons, whatever the pack. */
53 +define( 'WPATT_TEMPLATE_CARD', 4 );
54 +
55 +/**
56 + * Selected icon pack, 0-5. Modern is the default for new installs.
57 + *
58 + * @return int
59 + */
60 +function wpatt_icon_pack() {
61 + // Fallback 0, not Modern: sites updated from an older version may never
62 + // have saved this option, and must keep the pack they have always shown.
63 + // New installs get Modern from wpa_register_initial_settings().
64 + $pack = (int) get_option( 'wpa_ict', 0 );
65 +
66 + return ( $pack >= 0 && $pack <= WPATT_PACK_MODERN ) ? $pack : 0;
67 +}
68 +
69 +/**
70 + * Selected display template, 0-4.
71 + *
72 + * @return int
73 + */
74 +function wpatt_template_id() {
75 + $template = (int) get_option( 'wpa_template', 0 );
76 +
77 + return ( $template >= 0 && $template <= WPATT_TEMPLATE_CARD ) ? $template : 0;
78 +}
79 +
80 +/**
81 + * How the SVG icons are coloured: 'type' (a colour per file type), 'theme'
82 + * (the text colour of the theme) or 'custom' (one colour chosen in the settings).
83 + *
84 + * @return string
85 + */
86 +function wpatt_icons_color_mode() {
87 + $mode = get_option( 'wpatt_icons_color', 'type' );
88 +
89 + return in_array( $mode, array( 'type', 'theme', 'custom' ), true ) ? $mode : 'type';
90 +}
91 +
92 +/**
93 + * The custom icon colour, as a hex value.
94 + *
95 + * @return string
96 + */
97 +function wpatt_icons_custom_color() {
98 + $color = sanitize_hex_color( (string) get_option( 'wpatt_icons_custom_color', '#1c5f96' ) );
99 +
100 + return $color ? $color : '#1c5f96';
101 +}
102 +
103 +/**
104 + * Class and inline style that colour the SVG icons inside an element.
105 + *
106 + * The custom colour travels as a CSS variable on the wrapper, so a single
107 + * rule in common.css covers it and nothing is printed in the page head.
108 + *
109 + * @return array { @type string $class Class to append, with a leading space. @type string $style Inline style. }
110 + */
111 +function wpatt_icons_color_attrs() {
112 + switch ( wpatt_icons_color_mode() ) {
113 + case 'custom':
114 + return array( 'class' => ' wpa-icons-custom', 'style' => '--wpa-custom:' . wpatt_icons_custom_color() );
115 + case 'theme':
116 + return array( 'class' => '', 'style' => '' );
117 + default:
118 + return array( 'class' => ' wpa-icons-color', 'style' => '' );
119 + }
120 +}
121 +
122 +/**
29 123 * Handle ?download=ID hits: count the click, then redirect to the real file.
30 124 *
31 125 * Runs on 'template_redirect' because conditional tags such as is_attachment()
32 126 * are not reliable before the main query has run.
@@ -44,8 +138,12 @@
44 138 if ( ! $download_id || get_post_type($download_id) !== 'attachment' ) {
45 139 return;
46 140 }
47 141
142 + if ( ! wpa_can_download( $download_id ) ) {
143 + return;
144 + }
145 +
48 146 $excludelogged = true;
49 147 if ( get_option('wpatt_excludelogged_counter') ) {
50 148 $excludelogged = !is_user_logged_in();
51 149 }
@@ -55,35 +153,97 @@
55 153 $newcounter++;
56 154 update_post_meta($download_id, 'wpa-download', $newcounter );
57 155 }
58 156
157 + // wp_redirect(), not wp_safe_redirect(): the URL comes from the database,
158 + // not from the request, and media offloaded to a CDN or S3 lives on
159 + // another host -- which wp_safe_redirect() turned into a jump to wp-admin.
59 160 $redirect_url = wp_get_attachment_url($download_id);
60 161 if ($redirect_url) {
61 - wp_safe_redirect(esc_url_raw($redirect_url));
162 + wp_redirect(esc_url_raw($redirect_url));
62 163 exit;
63 164 }
64 165 });
65 166
66 167 /**
67 - * Handle redirect after deleting an attachment from the metabox
168 + * May the current visitor follow a ?download= link to this attachment?
169 + *
170 + * The redirect reveals the file URL, so it follows the visibility of the
171 + * content the file is attached to: an ID must not be enough to dig out the
172 + * files of private, draft or password protected posts.
173 + *
174 + * @param int $attachment_id Attachment ID.
175 + * @return bool
68 176 */
177 +function wpa_can_download( $attachment_id ) {
178 + $attachment = get_post( $attachment_id );
179 + if ( ! $attachment ) {
180 + return false;
181 + }
182 +
183 + $parent_id = (int) $attachment->post_parent;
184 + $allowed = true;
185 +
186 + if ( $parent_id ) {
187 + // WooCommerce orders, which under HPOS are not posts: customers may
188 + // follow the links shown on their own order.
189 + $order = function_exists( 'wc_get_order' ) ? wc_get_order( $parent_id ) : false;
190 +
191 + if ( $order ) {
192 + $allowed = current_user_can( 'view_order', $parent_id ) || current_user_can( 'edit_shop_orders' );
193 + } else {
194 + $parent = get_post( $parent_id );
195 +
196 + if ( ! $parent ) {
197 + $allowed = true; // Orphaned: behaves like an unattached file.
198 + } elseif ( post_password_required( $parent ) ) {
199 + $allowed = false;
200 + } else {
201 + $public = function_exists( 'is_post_publicly_viewable' )
202 + ? is_post_publicly_viewable( $parent )
203 + : ( 'publish' === get_post_status( $parent ) );
204 + $allowed = $public || current_user_can( 'read_post', $parent_id );
205 + }
206 + }
207 + }
208 +
209 + /**
210 + * Filter whether the current visitor may download an attachment through the counter link.
211 + *
212 + * @param bool $allowed Whether the download is allowed.
213 + * @param int $attachment_id Attachment ID.
214 + */
215 + return (bool) apply_filters( 'wpatt_can_download', $allowed, $attachment_id );
216 +}
217 +
218 +/**
219 + * Send the user back to the editor after deleting an attachment from the metabox.
220 + *
221 + * Core redirects to the Media Library, so the destination is swapped through
222 + * the wp_redirect filter. It must not redirect and exit from 'deleted_post':
223 + * wp_delete_attachment() removes the files from disk only after that hook,
224 + * so exiting there left the original and every thumbnail publicly reachable.
225 + */
69 226 add_action('deleted_post', function($post_id, $post) {
70 - if ($post->post_type !== 'attachment') {
227 + if ( ! $post || $post->post_type !== 'attachment' ) {
71 228 return;
72 229 }
73 230
74 - if (isset($_REQUEST['forcedelete']) && $_REQUEST['forcedelete'] === 'true') {
75 - $referer = wp_get_referer();
76 - if ($referer && strpos($referer, 'post.php') !== false) {
77 - wp_safe_redirect(remove_query_arg('message', $referer));
78 - exit;
79 - }
231 + if ( ! isset($_REQUEST['forcedelete']) || $_REQUEST['forcedelete'] !== 'true' ) {
232 + return;
80 233 }
234 +
235 + $referer = wp_get_referer();
236 + if ( ! $referer || strpos($referer, 'post.php') === false ) {
237 + return;
238 + }
239 +
240 + add_filter('wp_redirect', function() use ($referer) {
241 + return remove_query_arg('message', $referer);
242 + }, 99);
81 243 }, 10, 2);
82 244
83 245 add_action('admin_init', function() {
84 - load_plugin_textdomain( 'wp-attachments', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
85 -
86 246 require_once(plugin_dir_path(__FILE__) . 'inc/settings.php');
87 247 require_once(plugin_dir_path(__FILE__) . 'inc/meta-box.php');
88 248 require_once(plugin_dir_path(__FILE__) . 'inc/post-columns.php');
89 249 if (get_option('wpatt_counter')) { require_once(plugin_dir_path(__FILE__) . 'inc/counter.php'); }
@@ -114,8 +274,32 @@
114 274
115 275 return ( false === $formatted ) ? size_format( 0 ) : $formatted;
116 276 }
117 277
278 +/**
279 + * Size of an attachment in bytes.
280 + *
281 + * Read from the attachment metadata first (WordPress 6.0+ stores it there),
282 + * so it also works when the file is not on the local disk, as with media
283 + * offloaded to a CDN or S3. Falls back to the local file.
284 + *
285 + * @param int $attachment_id Attachment ID.
286 + * @return int|false Size in bytes, or false when it cannot be determined.
287 + */
288 +function wpatt_get_attachment_filesize( $attachment_id ) {
289 + $meta = wp_get_attachment_metadata( $attachment_id );
290 + if ( is_array( $meta ) && ! empty( $meta['filesize'] ) ) {
291 + return (int) $meta['filesize'];
292 + }
293 +
294 + $path = get_attached_file( $attachment_id );
295 + if ( $path && file_exists( $path ) ) {
296 + return (int) filesize( $path );
297 + }
298 +
299 + return false;
300 +}
301 +
118 302 add_action('woocommerce_order_details_after_customer_details', function( $order ) {
119 303 // "My Account" > "Order View". Under HPOS an order is not a post, so it has
120 304 // no ->ID and must not be passed through the the_content filter.
121 305 if ( ! is_object( $order ) || ! method_exists( $order, 'get_id' ) ) {
@@ -145,21 +329,89 @@
145 329 if ( !$post ) {
146 330 global $post;
147 331 }
148 332
149 - if ( !is_object($post) || empty($post->ID) || get_post_meta($post->ID, 'wpa_off', true) || post_password_required() || ( get_option('wpatt_option_restrictload') && !is_single() && !is_page() ) ) {
333 + if ( !is_object($post) || empty($post->ID) || post_password_required() || ( get_option('wpatt_option_restrictload') && !is_single() && !is_page() ) ) {
150 334 return $content;
151 335 }
152 336
153 - $enabled = get_option('wpatt_enable_metabox_' . $post->post_type, '1');
154 - if ( $enabled !== '1' ) {
337 + if ( ! wpatt_is_frontend_enabled( $post->post_type ) || ! wpatt_is_display_enabled( $post ) ) {
155 338 return $content;
156 339 }
157 340
341 + if ( ! wpatt_should_render( $post ) ) {
342 + return $content;
343 + }
344 +
158 345 return $content . wpatt_get_attachments_html( $post->ID );
159 346 }
160 347
161 348 /**
349 + * Is this the_content call one the list belongs to?
350 + *
351 + * the_content also runs for feeds, REST responses, automatic excerpts and,
352 + * in block themes, for every post inside Query Loop blocks such as the
353 + * "More posts" section under a single post -- each of which would get its
354 + * own copy of the list.
355 + *
356 + * @param WP_Post $post Post being rendered.
357 + * @return bool
358 + */
359 +function wpatt_should_render( $post ) {
360 + $render = true;
361 +
362 + if ( is_feed() || doing_filter( 'get_the_excerpt' ) ) {
363 + $render = false;
364 + } elseif ( function_exists( 'wp_is_serving_rest_request' ) ? wp_is_serving_rest_request() : ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
365 + $render = false;
366 + } elseif ( is_singular() && (int) get_queried_object_id() !== (int) $post->ID ) {
367 + // On a single post or page, only that content gets the list.
368 + $render = false;
369 + }
370 +
371 + /**
372 + * Filter whether the attachments list is appended to this content.
373 + *
374 + * @param bool $render Whether the list is shown.
375 + * @param WP_Post $post Post being rendered.
376 + */
377 + return (bool) apply_filters( 'wpatt_should_render', $render, $post );
378 +}
379 +
380 +/**
381 + * Is the frontend list switched on for this post type?
382 + *
383 + * Until "Enable Frontend" is first saved it follows "Enable Metabox", which
384 + * is what used to hide the list before the two were separate settings.
385 + *
386 + * @param string $post_type Post type name.
387 + * @return bool
388 + */
389 +function wpatt_is_frontend_enabled( $post_type ) {
390 + $metabox = get_option( 'wpatt_enable_metabox_' . $post_type, '1' );
391 +
392 + return get_option( 'wpatt_enable_frontend_' . $post_type, $metabox ) === '1';
393 +}
394 +
395 +/**
396 + * Has this post not been opted out of the list?
397 + *
398 + * The list shows by default; the metabox toggle is the exception that turns
399 + * it off, stored as 'wpa_off' = '1'.
400 + *
401 + * @param WP_Post|int $post Post object or ID.
402 + * @return bool
403 + */
404 +function wpatt_is_display_enabled( $post ) {
405 + $post = get_post( $post );
406 + if ( ! $post ) {
407 + return false;
408 + }
409 +
410 + return '1' !== (string) get_post_meta( $post->ID, 'wpa_off', true );
411 +}
412 +
413 +/**
162 414 * Build the attachments list for a given parent ID.
163 415 *
164 416 * Kept separate from the the_content filter so callers that are not posts --
165 417 * WooCommerce orders under HPOS, for instance -- can render the same list
@@ -216,16 +468,24 @@
216 468 . esc_html__( 'Sort by:', 'wp-attachments' ) . '</span>' . $sort_items . '</span>';
217 469 }
218 470
219 471 if ($attachments) {
220 - $content_l = '<!-- WP Attachments -->
221 - <div class="wpa-attachments-block">
222 - <div class="wpa-attachments-head">
223 - <h3 class="wpa-attachments-title">' . esc_html( get_option('wpatt_option_localization') ) . '</h3>'
224 - . $orderby_html . '
225 - </div>
226 - <ul class="post-attachments">';
472 + // The default applies only until the settings are first saved; an
473 + // empty header saved on purpose hides the heading.
474 + $heading_text = trim( (string) get_option( 'wpatt_option_localization', __( 'Attachments', 'wp-attachments' ) ) );
475 + $heading_tag = wpatt_heading_tag();
476 + $heading_html = ( '' !== $heading_text )
477 + ? '<' . $heading_tag . ' class="wpa-attachments-title">' . esc_html( $heading_text ) . '</' . $heading_tag . '>'
478 + : '';
227 479
480 + $head_html = ( '' !== $heading_html || '' !== $orderby_html )
481 + ? '<div class="wpa-attachments-head">' . $heading_html . $orderby_html . '</div>'
482 + : '';
483 +
484 + $template_id = wpatt_template_id();
485 + $template = wpatt_get_template_string( $template_id );
486 + $items = '';
487 +
228 488 foreach ($attachments as $attachment) {
229 489 $include_images = get_option('wpatt_option_includeimages');
230 490 if ($include_images !== '1' && wp_attachment_is_image( $attachment->ID )) {
231 491 continue;
@@ -234,70 +494,247 @@
234 494 if ( !apply_filters( 'wpatt_accepted_formats', sanitize_title($attachment->post_mime_type) ) ) {
235 495 continue;
236 496 }
237 497
238 - $class = "post-attachment mime-" . sanitize_title($attachment->post_mime_type);
498 + $items .= wpatt_render_list_item( $template_id, $template, wpatt_get_entry_data( $attachment ) );
499 + $toShow = 1;
500 + }
239 501
240 - $file_path = get_attached_file($attachment->ID);
241 - $wpatt_fs = (file_exists($file_path)) ? wpatt_format_bytes(filesize($file_path)) : 'ERROR';
502 + if ( $toShow ) {
503 + $content .= apply_filters( 'wpatt_list_html', wpatt_wrap_list( $items, $head_html, $template_id ) );
504 + }
505 + }
506 + return $content;
507 +}
242 508
243 - $wpatt_date = new DateTime($attachment->post_date);
509 +/**
510 + * Markup of a display template, with its %TAG% placeholders.
511 + *
512 + * @param int $template_id 0 simple, 1 with date, 2 detailed, 3 custom, 4 Modern Card.
513 + * @param array|null $card Modern Card options, from wpatt_get_card_options().
514 + * @return string
515 + */
516 +function wpatt_get_template_string( $template_id, $card = null ) {
517 + switch ( (int) $template_id ) {
518 + case 1:
519 + return '<a href="%URL%">%TITLE%</a> <small>(%SIZE%)</small> <span class="wpa-attachment-date">%DATE%</span>';
520 + case 2:
521 + return '<a href="%URL%">%TITLE%</a> <small>&bull; %SIZE% &bull; %DOWNLOADS% click</small> <span class="wpa-attachment-date">%DATE%</span><br><small>%CAPTION%</small>';
522 + case 3:
523 + // Legacy templates were stored HTML-encoded, so they still need
524 + // decoding -- but kses must run again afterwards, otherwise an
525 + // encoded <script> smuggled past the save-time wp_kses_post()
526 + // would be decoded straight into the page.
527 + return wp_kses_post( html_entity_decode( (string) get_option('wpa_template_custom') ) );
528 + case WPATT_TEMPLATE_CARD:
529 + return wpatt_get_card_template( is_array( $card ) ? $card : wpatt_get_card_options() );
530 + default:
531 + return '<a href="%URL%">%TITLE%</a> <small>(%SIZE%)</small>';
532 + }
533 +}
244 534
245 - switch ( get_option('wpa_template') ) {
246 - case 1:
247 - $wpattachments_string = '<a href="%URL%">%TITLE%</a> <small>(%SIZE%)</small> <span class="wpa-attachment-date">%DATE%</span>';
248 - break;
249 - case 2:
250 - $wpattachments_string = '<a href="%URL%">%TITLE%</a> <small>&bull; %SIZE% &bull; %DOWNLOADS% click</small> <span class="wpa-attachment-date">%DATE%</span><br><small>%CAPTION%</small>';
251 - break;
252 - case 3:
253 - // Legacy templates were stored HTML-encoded, so they still need
254 - // decoding -- but kses must run again afterwards, otherwise an
255 - // encoded <script> smuggled past the save-time wp_kses_post()
256 - // would be decoded straight into the page.
257 - $wpattachments_string = wp_kses_post( html_entity_decode( get_option('wpa_template_custom') ) );
258 - break;
259 - default:
260 - $wpattachments_string = '<a href="%URL%">%TITLE%</a> <small>(%SIZE%)</small>';
261 - }
535 +/**
536 + * Markup of the Modern Card template for the given options.
537 + *
538 + * Each detail sits in its own <span> with no whitespace around the tag, so
539 + * the CSS can hide the empty ones (:empty) and put a separator only between
540 + * the details that are actually shown.
541 + *
542 + * @param array $card Options from wpatt_get_card_options().
543 + * @return string
544 + */
545 +function wpatt_get_card_template( array $card ) {
546 + $meta = '';
547 + if ( $card['ext'] ) {
548 + $meta .= '<span>%EXT%</span>';
549 + }
550 + if ( $card['size'] ) {
551 + $meta .= '<span>%SIZE%</span>';
552 + }
553 + if ( $card['date'] ) {
554 + $meta .= '<span>%DATE%</span>';
555 + }
556 + if ( $card['downloads'] && get_option( 'wpatt_counter' ) ) {
557 + /* translators: %s: number of downloads. */
558 + $meta .= '<span>' . sprintf( esc_html__( 'Downloads: %s', 'wp-attachments' ), '%DOWNLOADS%' ) . '</span>';
559 + }
262 560
263 - $wpattachments_string = apply_filters( 'wpatt_before_entry_html', $wpattachments_string );
561 + // href first: the "open in a new tab" option matches '<a href'.
562 + return '<span class="wpa-card-icon">%ICON%</span>'
563 + . '<span class="wpa-card-body"><a href="%URL%" class="wpa-card-title">%TITLE%</a>'
564 + . ( '' !== $meta ? '<span class="wpa-card-meta">' . $meta . '</span>' : '' )
565 + . ( $card['caption'] ? '<span class="wpa-card-caption">%CAPTION%</span>' : '' )
566 + . '</span>';
567 +}
264 568
265 - if ( get_option('wpatt_option_targetblank') ) {
266 - $wpattachments_string = str_replace('<a href', '<a target="_blank" rel="noopener noreferrer" href', $wpattachments_string);
267 - }
569 +/**
570 + * Values for the template tags of one attachment, not yet escaped.
571 + *
572 + * @param WP_Post $attachment Attachment.
573 + * @return array
574 + */
575 +function wpatt_get_entry_data( $attachment ) {
576 + $bytes = wpatt_get_attachment_filesize( $attachment->ID );
268 577
269 - if ( get_option('wpatt_counter') ) {
270 - $url = add_query_arg( 'download', $attachment->ID, get_permalink() );
271 - } else {
272 - $url = wp_get_attachment_url($attachment->ID);
273 - }
578 + return array(
579 + 'url' => get_option('wpatt_counter')
580 + ? add_query_arg( 'download', $attachment->ID, get_permalink() )
581 + : wp_get_attachment_url( $attachment->ID ),
582 + 'title' => $attachment->post_title,
583 + 'size' => ( false !== $bytes ) ? wpatt_format_bytes( $bytes ) : '—',
584 + 'date' => wpatt_format_entry_date( $attachment->post_date ),
585 + 'caption' => $attachment->post_excerpt,
586 + 'description' => $attachment->post_content,
587 + 'author' => get_the_author_meta( 'display_name', $attachment->post_author ),
588 + 'downloads' => (int) wpa_get_downloads( $attachment->ID ),
589 + 'ext' => wpatt_get_file_extension( $attachment->ID ),
590 + 'mime' => (string) $attachment->post_mime_type,
591 + );
592 +}
274 593
275 - $wpattachments_string = str_replace("%URL%", esc_url( $url ), $wpattachments_string);
276 - $wpattachments_string = str_replace("%TITLE%", esc_html( $attachment->post_title ), $wpattachments_string);
277 - $wpattachments_string = str_replace("%SIZE%", esc_html( $wpatt_fs ), $wpattachments_string);
278 - $wpatt_date_format = get_option('wpatt_option_date_localization');
279 - if ( '' === trim( (string) $wpatt_date_format ) ) {
280 - $wpatt_date_format = get_option('date_format');
281 - }
282 - $wpattachments_string = str_replace("%DATE%", esc_html( date_i18n( $wpatt_date_format, strtotime($attachment->post_date) ) ), $wpattachments_string);
283 - $wpattachments_string = str_replace("%CAPTION%", esc_html( $attachment->post_excerpt ), $wpattachments_string);
284 - $wpattachments_string = str_replace("%DESCRIPTION%", esc_html( $attachment->post_content ), $wpattachments_string);
285 - $wpattachments_string = str_replace("%AUTHOR%", esc_html( get_the_author_meta( 'display_name', $attachment->post_author) ), $wpattachments_string);
286 - $wpattachments_string = str_replace("%DOWNLOADS%", intval(wpa_get_downloads($attachment->ID)), $wpattachments_string);
594 +/**
595 + * Date of an attachment in the format chosen in the settings.
596 + *
597 + * @param string $post_date Date as stored in the post.
598 + * @return string
599 + */
600 +function wpatt_format_entry_date( $post_date ) {
601 + $format = get_option('wpatt_option_date_localization');
602 + if ( '' === trim( (string) $format ) ) {
603 + $format = get_option('date_format');
604 + }
287 605
288 - $content_l .= '<li class="' . esc_attr($class) . '">' . apply_filters( 'wpatt_after_entry_html', $wpattachments_string ) . '</li>';
289 - $toShow = 1;
606 + return date_i18n( $format, strtotime( $post_date ) );
607 +}
608 +
609 +/**
610 + * One <li> of the list.
611 + *
612 + * @param int $template_id Template ID.
613 + * @param string $template Template markup.
614 + * @param array $data Values from wpatt_get_entry_data().
615 + * @return string
616 + */
617 +function wpatt_render_list_item( $template_id, $template, array $data ) {
618 + $type = wpatt_get_file_type( $data['mime'] );
619 + $icon = wpatt_get_file_icon_svg( $type, 'wpa-icon' );
620 +
621 + $html = apply_filters( 'wpatt_before_entry_html', $template );
622 +
623 + if ( get_option('wpatt_option_targetblank') ) {
624 + $html = str_replace('<a href', '<a target="_blank" rel="noopener noreferrer" href', $html);
625 + }
626 +
627 + // strtr() replaces in one pass, so a title that happens to contain a tag
628 + // such as %URL% is not expanded a second time.
629 + $html = strtr( $html, array(
630 + '%URL%' => esc_url( $data['url'] ),
631 + '%TITLE%' => esc_html( $data['title'] ),
632 + '%SIZE%' => esc_html( $data['size'] ),
633 + '%DATE%' => esc_html( $data['date'] ),
634 + '%CAPTION%' => esc_html( $data['caption'] ),
635 + '%DESCRIPTION%' => esc_html( $data['description'] ),
636 + '%AUTHOR%' => esc_html( $data['author'] ),
637 + '%DOWNLOADS%' => (int) $data['downloads'],
638 + '%EXT%' => esc_html( $data['ext'] ),
639 + '%MIME%' => esc_html( $data['mime'] ),
640 + '%ICON%' => $icon,
641 + ) );
642 +
643 + $html = apply_filters( 'wpatt_after_entry_html', $html );
644 +
645 + // Modern pack: the icon leads the entry. The Card template places it
646 + // itself through %ICON%.
647 + if ( WPATT_PACK_MODERN === wpatt_icon_pack() && WPATT_TEMPLATE_CARD !== (int) $template_id ) {
648 + $html = $icon . $html;
649 + }
650 +
651 + $class = 'post-attachment mime-' . sanitize_title( $data['mime'] ) . ' wpa-type-' . $type;
652 +
653 + return '<li class="' . esc_attr( $class ) . '">' . $html . '</li>';
654 +}
655 +
656 +/**
657 + * Wrap the items in the list block.
658 + *
659 + * @param string $items <li> elements.
660 + * @param string $head_html Heading and sort links, may be empty.
661 + * @param int $template_id Template ID.
662 + * @param array|null $card Modern Card options, from wpatt_get_card_options().
663 + * @return string
664 + */
665 +function wpatt_wrap_list( $items, $head_html, $template_id, $card = null ) {
666 + $is_card = ( WPATT_TEMPLATE_CARD === (int) $template_id );
667 + $uses_svg = ( WPATT_PACK_MODERN === wpatt_icon_pack() || $is_card );
668 +
669 + $colors = $uses_svg ? wpatt_icons_color_attrs() : array( 'class' => '', 'style' => '' );
670 + $block_class = 'wpa-attachments-block' . $colors['class'];
671 + $list_class = 'post-attachments';
672 +
673 + if ( $is_card ) {
674 + $card = is_array( $card ) ? $card : wpatt_get_card_options();
675 + $list_class .= ' wpa-cards';
676 + if ( 'rows' === $card['layout'] ) {
677 + $list_class .= ' wpa-cards--rows';
678 + } elseif ( 'normal' !== $card['width'] ) {
679 + $list_class .= ' wpa-cards--' . $card['width'];
290 680 }
291 - $content_l .= '</ul></div>';
292 - if ( $toShow ) {
293 - $content .= apply_filters( 'wpatt_list_html', $content_l );
294 - }
295 681 }
296 - return $content;
682 +
683 + return '<!-- WP Attachments --><div class="' . esc_attr( $block_class ) . '"'
684 + . ( $colors['style'] ? ' style="' . esc_attr( $colors['style'] ) . '"' : '' ) . '>' . $head_html
685 + . '<ul class="' . esc_attr( $list_class ) . '">' . $items . '</ul></div>';
297 686 }
298 687
688 +/**
689 + * List preview for the settings screen, built from sample files.
690 + *
691 + * Goes through the same functions as the real list, so it shows what the
692 + * site will show with the saved icon pack.
693 + *
694 + * @param int $template_id Template ID.
695 + * @return string
696 + */
697 +function wpatt_render_template_preview( $template_id ) {
698 + $samples = array(
699 + array(
700 + 'title' => __( 'Annual report', 'wp-attachments' ),
701 + 'mime' => 'application/pdf',
702 + 'ext' => 'PDF',
703 + 'size' => '1.2 MB',
704 + 'caption' => __( 'Approved by the board', 'wp-attachments' ),
705 + ),
706 + array(
707 + 'title' => __( 'Application form', 'wp-attachments' ),
708 + 'mime' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
709 + 'ext' => 'DOCX',
710 + 'size' => '48 KB',
711 + 'caption' => '',
712 + ),
713 + );
299 714
715 + $template = wpatt_get_template_string( $template_id );
716 + $items = '';
717 +
718 + foreach ( $samples as $i => $sample ) {
719 + $items .= wpatt_render_list_item( $template_id, $template, array(
720 + 'url' => '#',
721 + 'title' => $sample['title'],
722 + 'size' => $sample['size'],
723 + 'date' => wpatt_format_entry_date( gmdate( 'Y-m-d H:i:s', time() - $i * DAY_IN_SECONDS ) ),
724 + 'caption' => $sample['caption'],
725 + 'description' => '',
726 + 'author' => wp_get_current_user()->display_name,
727 + 'downloads' => 12 - $i * 7,
728 + 'ext' => $sample['ext'],
729 + 'mime' => $sample['mime'],
730 + ) );
731 + }
732 +
733 + return wpatt_wrap_list( $items, '', $template_id );
734 +}
735 +
736 +
300 737 /* Register Settings */
301 738
302 739 function wpa_get_downloads($ID) {
303 740 if (get_post_meta($ID, "wpa-download", true)) {
@@ -389,31 +826,93 @@
389 826
390 827 return true;
391 828 }
392 829
393 -add_action('admin_init', function() {
394 - if (get_option('wpatt_option_localization') == '') {
395 - $value = __('Attachments','wp-attachments');
396 - update_option('wpatt_option_localization', $value);
397 - }
398 -});
830 +add_action('admin_init', 'wpa_register_initial_settings', 5);
831 +// Also on activation: WP-CLI and automated installers activate without ever
832 +// loading an admin page, which left a new site on the legacy defaults.
833 +register_activation_hook(__FILE__, 'wpa_register_initial_settings');
399 834
400 835 add_action('admin_menu', function() {
401 836 add_options_page('WP Attachments - Settings', 'WP Attachments', 'manage_options', 'wpatt-option-page', 'wpatt_plugin_options');
402 837 });
403 838
839 +/**
840 + * Seed the options on first run only.
841 + *
842 + * add_option() leaves existing values alone, so an empty list header or
843 + * date format saved on purpose is kept instead of being refilled on the
844 + * next admin page load.
845 + */
404 846 function wpa_register_initial_settings() {
405 - if ( !get_option('wpatt_option_localization') ) {
406 - update_option('wpatt_option_localization', __('Attachments','wp-attachments'));
847 + // Every earlier version stored these two on its first admin page load,
848 + // so their absence means a brand new install. Runs at priority 5, before
849 + // the admin_init callback that writes wpa_version_number.
850 + $fresh_install = ( false === get_option('wpatt_option_localization') && false === get_option('wpa_version_number') );
851 +
852 + add_option('wpatt_option_localization', __('Attachments','wp-attachments'));
853 + // Empty: follow the WordPress date format (Settings > General).
854 + add_option('wpatt_option_date_localization', '');
855 + add_option('wpatt_option_heading_tag', 'h3');
856 + add_option('wpatt_icons_color', 'type');
857 + add_option('wpatt_card', wpatt_card_defaults());
858 +
859 + // New look for new installs only: updated sites keep what they show today.
860 + add_option('wpa_ict', $fresh_install ? (string) WPATT_PACK_MODERN : '0');
861 + add_option('wpa_template', $fresh_install ? (string) WPATT_TEMPLATE_CARD : '0');
862 +}
863 +
864 +/**
865 + * Default options of the Modern Card template.
866 + *
867 + * @return array
868 + */
869 +function wpatt_card_defaults() {
870 + return array(
871 + 'ext' => 1,
872 + 'size' => 1,
873 + 'date' => 1,
874 + 'downloads' => 0,
875 + 'caption' => 0,
876 + 'layout' => 'grid', // grid | rows
877 + 'width' => 'normal', // compact | normal | wide
878 + );
879 +}
880 +
881 +/**
882 + * Modern Card options: the saved ones, optionally overridden.
883 + *
884 + * The overrides are what a block or shortcode will pass for a single list,
885 + * so one page can show a wide grid and another one card per row.
886 + *
887 + * @param array $overrides Options for this list only.
888 + * @return array
889 + */
890 +function wpatt_get_card_options( array $overrides = array() ) {
891 + $saved = get_option( 'wpatt_card', array() );
892 + $opts = array_merge( wpatt_card_defaults(), is_array( $saved ) ? $saved : array(), $overrides );
893 +
894 + foreach ( array( 'ext', 'size', 'date', 'downloads', 'caption' ) as $flag ) {
895 + $opts[ $flag ] = empty( $opts[ $flag ] ) ? 0 : 1;
407 896 }
408 - if ( !get_option('wpatt_option_date_localization') ) {
409 - update_option('wpatt_option_date_localization', 'd.m.Y');
897 + if ( ! in_array( $opts['layout'], array( 'grid', 'rows' ), true ) ) {
898 + $opts['layout'] = 'grid';
410 899 }
411 - if ( !get_option('wpa_ict') ) {
412 - update_option('wpa_ict', '0');
900 + if ( ! in_array( $opts['width'], array( 'compact', 'normal', 'wide' ), true ) ) {
901 + $opts['width'] = 'normal';
413 902 }
414 - if ( !get_option('wpa_template') ) {
415 - update_option('wpa_template', '0');
416 - }
903 +
904 + return $opts;
905 +}
906 +
907 +/**
908 + * Heading level of the list title, as chosen in the settings.
909 + *
910 + * @return string One of h1-h6.
911 + */
912 +function wpatt_heading_tag() {
913 + $tag = get_option('wpatt_option_heading_tag', 'h3');
914 +
915 + return in_array($tag, array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'), true) ? $tag : 'h3';
417 916 }
418 917
419 918 ?>