= 0 && $pack <= WPATT_PACK_MODERN ) ? $pack : 0; } /** * Selected display template, 0-4. * * @return int */ function wpatt_template_id() { $template = (int) get_option( 'wpa_template', 0 ); return ( $template >= 0 && $template <= WPATT_TEMPLATE_CARD ) ? $template : 0; } /** * How the SVG icons are coloured: 'type' (a colour per file type), 'theme' * (the text colour of the theme) or 'custom' (one colour chosen in the settings). * * @return string */ function wpatt_icons_color_mode() { $mode = get_option( 'wpatt_icons_color', 'type' ); return in_array( $mode, array( 'type', 'theme', 'custom' ), true ) ? $mode : 'type'; } /** * The custom icon colour, as a hex value. * * @return string */ function wpatt_icons_custom_color() { $color = sanitize_hex_color( (string) get_option( 'wpatt_icons_custom_color', '#1c5f96' ) ); return $color ? $color : '#1c5f96'; } /** * Class and inline style that colour the SVG icons inside an element. * * The custom colour travels as a CSS variable on the wrapper, so a single * rule in common.css covers it and nothing is printed in the page head. * * @return array { @type string $class Class to append, with a leading space. @type string $style Inline style. } */ function wpatt_icons_color_attrs() { switch ( wpatt_icons_color_mode() ) { case 'custom': return array( 'class' => ' wpa-icons-custom', 'style' => '--wpa-custom:' . wpatt_icons_custom_color() ); case 'theme': return array( 'class' => '', 'style' => '' ); default: return array( 'class' => ' wpa-icons-color', 'style' => '' ); } } /** * Handle ?download=ID hits: count the click, then redirect to the real file. * * Runs on 'template_redirect' because conditional tags such as is_attachment() * are not reliable before the main query has run. */ add_action('template_redirect', function () { if ( ! get_option('wpatt_counter') || ! isset($_GET['download']) ) { return; } if ( is_attachment() ) { return; } $download_id = absint( wp_unslash($_GET['download']) ); if ( ! $download_id || get_post_type($download_id) !== 'attachment' ) { return; } if ( ! wpa_can_download( $download_id ) ) { return; } $excludelogged = true; if ( get_option('wpatt_excludelogged_counter') ) { $excludelogged = !is_user_logged_in(); } if ( $excludelogged && wpa_is_countable_request() && wpa_is_valid_download($download_id) ) { $newcounter = intval(get_post_meta($download_id, "wpa-download", true)); $newcounter++; update_post_meta($download_id, 'wpa-download', $newcounter ); } // wp_redirect(), not wp_safe_redirect(): the URL comes from the database, // not from the request, and media offloaded to a CDN or S3 lives on // another host -- which wp_safe_redirect() turned into a jump to wp-admin. $redirect_url = wp_get_attachment_url($download_id); if ($redirect_url) { wp_redirect(esc_url_raw($redirect_url)); exit; } }); /** * May the current visitor follow a ?download= link to this attachment? * * The redirect reveals the file URL, so it follows the visibility of the * content the file is attached to: an ID must not be enough to dig out the * files of private, draft or password protected posts. * * @param int $attachment_id Attachment ID. * @return bool */ function wpa_can_download( $attachment_id ) { $attachment = get_post( $attachment_id ); if ( ! $attachment ) { return false; } $parent_id = (int) $attachment->post_parent; $allowed = true; if ( $parent_id ) { // WooCommerce orders, which under HPOS are not posts: customers may // follow the links shown on their own order. $order = function_exists( 'wc_get_order' ) ? wc_get_order( $parent_id ) : false; if ( $order ) { $allowed = current_user_can( 'view_order', $parent_id ) || current_user_can( 'edit_shop_orders' ); } else { $parent = get_post( $parent_id ); if ( ! $parent ) { $allowed = true; // Orphaned: behaves like an unattached file. } elseif ( post_password_required( $parent ) ) { $allowed = false; } else { $public = function_exists( 'is_post_publicly_viewable' ) ? is_post_publicly_viewable( $parent ) : ( 'publish' === get_post_status( $parent ) ); $allowed = $public || current_user_can( 'read_post', $parent_id ); } } } /** * Filter whether the current visitor may download an attachment through the counter link. * * @param bool $allowed Whether the download is allowed. * @param int $attachment_id Attachment ID. */ return (bool) apply_filters( 'wpatt_can_download', $allowed, $attachment_id ); } /** * Send the user back to the editor after deleting an attachment from the metabox. * * Core redirects to the Media Library, so the destination is swapped through * the wp_redirect filter. It must not redirect and exit from 'deleted_post': * wp_delete_attachment() removes the files from disk only after that hook, * so exiting there left the original and every thumbnail publicly reachable. */ add_action('deleted_post', function($post_id, $post) { if ( ! $post || $post->post_type !== 'attachment' ) { return; } if ( ! isset($_REQUEST['forcedelete']) || $_REQUEST['forcedelete'] !== 'true' ) { return; } $referer = wp_get_referer(); if ( ! $referer || strpos($referer, 'post.php') === false ) { return; } add_filter('wp_redirect', function() use ($referer) { return remove_query_arg('message', $referer); }, 99); }, 10, 2); add_action('admin_init', function() { require_once(plugin_dir_path(__FILE__) . 'inc/settings.php'); require_once(plugin_dir_path(__FILE__) . 'inc/meta-box.php'); require_once(plugin_dir_path(__FILE__) . 'inc/post-columns.php'); if (get_option('wpatt_counter')) { require_once(plugin_dir_path(__FILE__) . 'inc/counter.php'); } // get_plugin_data() re-read and parsed this file on every admin request. update_option( 'wpa_version_number', WPATT_VERSION ); } ); /** * Format a byte count for display. * * Thin wrapper around core size_format(); kept as a function for * back-compat with themes that may already call it. * * @param int $a_bytes Size in bytes. * @param int $decimals Decimal places to show. * @return string */ function wpatt_format_bytes($a_bytes, $decimals = 0) { $a_bytes = (int) $a_bytes; // size_format() would render this as '0.0 B' when decimals are requested. if ( $a_bytes <= 0 ) { return size_format( 0 ); } $formatted = size_format( $a_bytes, $decimals ); return ( false === $formatted ) ? size_format( 0 ) : $formatted; } /** * Size of an attachment in bytes. * * Read from the attachment metadata first (WordPress 6.0+ stores it there), * so it also works when the file is not on the local disk, as with media * offloaded to a CDN or S3. Falls back to the local file. * * @param int $attachment_id Attachment ID. * @return int|false Size in bytes, or false when it cannot be determined. */ function wpatt_get_attachment_filesize( $attachment_id ) { $meta = wp_get_attachment_metadata( $attachment_id ); if ( is_array( $meta ) && ! empty( $meta['filesize'] ) ) { return (int) $meta['filesize']; } $path = get_attached_file( $attachment_id ); if ( $path && file_exists( $path ) ) { return (int) filesize( $path ); } return false; } add_action('woocommerce_order_details_after_customer_details', function( $order ) { // "My Account" > "Order View". Under HPOS an order is not a post, so it has // no ->ID and must not be passed through the the_content filter. if ( ! is_object( $order ) || ! method_exists( $order, 'get_id' ) ) { return; } if ( ! is_wc_endpoint_url( 'view-order' ) ) { return; } echo wpatt_get_attachments_html( $order->get_id() ); }, 10, 1 ); /** * Tell WooCommerce this plugin is safe with High-Performance Order Storage. * Without it WooCommerce lists the plugin as incompatible. */ add_action('before_woocommerce_init', function() { if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) { \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); } }); add_filter('the_content', 'wpatt_content_filter'); function wpatt_content_filter( $content, $post = null ) { if ( !$post ) { global $post; } if ( !is_object($post) || empty($post->ID) || post_password_required() || ( get_option('wpatt_option_restrictload') && !is_single() && !is_page() ) ) { return $content; } if ( ! wpatt_is_frontend_enabled( $post->post_type ) || ! wpatt_is_display_enabled( $post ) ) { return $content; } if ( ! wpatt_should_render( $post ) ) { return $content; } return $content . wpatt_get_attachments_html( $post->ID ); } /** * Is this the_content call one the list belongs to? * * the_content also runs for feeds, REST responses, automatic excerpts and, * in block themes, for every post inside Query Loop blocks such as the * "More posts" section under a single post -- each of which would get its * own copy of the list. * * @param WP_Post $post Post being rendered. * @return bool */ function wpatt_should_render( $post ) { $render = true; if ( is_feed() || doing_filter( 'get_the_excerpt' ) ) { $render = false; } elseif ( function_exists( 'wp_is_serving_rest_request' ) ? wp_is_serving_rest_request() : ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { $render = false; } elseif ( is_singular() && (int) get_queried_object_id() !== (int) $post->ID ) { // On a single post or page, only that content gets the list. $render = false; } /** * Filter whether the attachments list is appended to this content. * * @param bool $render Whether the list is shown. * @param WP_Post $post Post being rendered. */ return (bool) apply_filters( 'wpatt_should_render', $render, $post ); } /** * Is the frontend list switched on for this post type? * * Until "Enable Frontend" is first saved it follows "Enable Metabox", which * is what used to hide the list before the two were separate settings. * * @param string $post_type Post type name. * @return bool */ function wpatt_is_frontend_enabled( $post_type ) { $metabox = get_option( 'wpatt_enable_metabox_' . $post_type, '1' ); return get_option( 'wpatt_enable_frontend_' . $post_type, $metabox ) === '1'; } /** * Has this post not been opted out of the list? * * The list shows by default; the metabox toggle is the exception that turns * it off, stored as 'wpa_off' = '1'. * * @param WP_Post|int $post Post object or ID. * @return bool */ function wpatt_is_display_enabled( $post ) { $post = get_post( $post ); if ( ! $post ) { return false; } return '1' !== (string) get_post_meta( $post->ID, 'wpa_off', true ); } /** * Build the attachments list for a given parent ID. * * Kept separate from the the_content filter so callers that are not posts -- * WooCommerce orders under HPOS, for instance -- can render the same list * without faking a WP_Post object. * * @param int $parent_id Parent object ID. * @return string HTML, or an empty string when there is nothing to show. */ function wpatt_get_attachments_html( $parent_id ) { $parent_id = absint( $parent_id ); if ( ! $parent_id ) { return ''; } $content = ''; $orderby = sanitize_text_field(get_query_var('orderby')); $order = 'ASC'; if ($orderby === 'date') { $order = 'DESC'; } elseif ($orderby !== 'title') { $orderby = 'menu_order'; } $attachments = get_posts(array( 'post_type' => 'attachment', 'orderby' => $orderby, 'order' => $order, 'posts_per_page' => -1, 'post_status' => 'any', 'post_parent' => $parent_id )); $toShow = 0; $orderby_html = ''; if ( get_option('wpatt_show_orderby') != 0 && count($attachments) > 1 ) { $sort_links = array( 'menu_order' => array( esc_html__( 'Default', 'wp-attachments' ), remove_query_arg( 'orderby' ) ), 'date' => array( esc_html__( 'Date', 'wp-attachments' ), add_query_arg( 'orderby', 'date' ) ), 'title' => array( esc_html__( 'Name', 'wp-attachments' ), add_query_arg( 'orderby', 'title' ) ), ); $sort_items = ''; foreach ( $sort_links as $sort_key => $sort_link ) { $is_current = ( $orderby === $sort_key ); $sort_items .= '' . $sort_link[0] . ''; } $orderby_html = '' . esc_html__( 'Sort by:', 'wp-attachments' ) . '' . $sort_items . ''; } if ($attachments) { // The default applies only until the settings are first saved; an // empty header saved on purpose hides the heading. $heading_text = trim( (string) get_option( 'wpatt_option_localization', __( 'Attachments', 'wp-attachments' ) ) ); $heading_tag = wpatt_heading_tag(); $heading_html = ( '' !== $heading_text ) ? '<' . $heading_tag . ' class="wpa-attachments-title">' . esc_html( $heading_text ) . '' : ''; $head_html = ( '' !== $heading_html || '' !== $orderby_html ) ? '
' . $heading_html . $orderby_html . '
' : ''; $template_id = wpatt_template_id(); $template = wpatt_get_template_string( $template_id ); $items = ''; foreach ($attachments as $attachment) { $include_images = get_option('wpatt_option_includeimages'); if ($include_images !== '1' && wp_attachment_is_image( $attachment->ID )) { continue; } if ( !apply_filters( 'wpatt_accepted_formats', sanitize_title($attachment->post_mime_type) ) ) { continue; } $items .= wpatt_render_list_item( $template_id, $template, wpatt_get_entry_data( $attachment ) ); $toShow = 1; } if ( $toShow ) { $content .= apply_filters( 'wpatt_list_html', wpatt_wrap_list( $items, $head_html, $template_id ) ); } } return $content; } /** * Markup of a display template, with its %TAG% placeholders. * * @param int $template_id 0 simple, 1 with date, 2 detailed, 3 custom, 4 Modern Card. * @param array|null $card Modern Card options, from wpatt_get_card_options(). * @return string */ function wpatt_get_template_string( $template_id, $card = null ) { switch ( (int) $template_id ) { case 1: return '%TITLE% (%SIZE%) %DATE%'; case 2: return '%TITLE% • %SIZE% • %DOWNLOADS% click %DATE%
%CAPTION%'; case 3: // Legacy templates were stored HTML-encoded, so they still need // decoding -- but kses must run again afterwards, otherwise an // encoded