| @@ -1,247 +1,918 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /* |
| 3 | -Plugin Name: WP Attachments | |
| 4 | -Plugin URI: http://marcomilesi.ml | |
| 5 | -Description: Automatically shows your attachments under every post and page content. Simple. Automatic. Easy. As it has to be! | |
| 3 | +Plugin Name: WP Attachments – Smarter File Management & Download Lists | |
| 4 | +Plugin URI: https://wordpress.org/plugins/wp-attachments | |
| 5 | +Description: Powerful solution to manage and show your WordPress media in posts and pages | |
| 6 | 6 | Author: Marco Milesi |
| 7 | -Version: 3.0.3 | |
| 8 | -Author URI: http://marcomilesi.ml | |
| 7 | +Author URI: https://www.marcomilesi.com | |
| 8 | +Version: 6.1 | |
| 9 | +Requires at least: 5.0 | |
| 10 | +Requires PHP: 7.4 | |
| 11 | +License: GPLv2 or later | |
| 12 | +License URI: http://www.gnu.org/licenses/gpl-2.0.html | |
| 13 | +Text Domain: wp-attachments | |
| 9 | 14 | */ |
| 10 | 15 | |
| 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' ); | |
| 11 | 19 | |
| 20 | +require_once( plugin_dir_path(__FILE__) . 'inc/attach_unattach_reattach.php' ); | |
| 21 | +require_once( plugin_dir_path(__FILE__) . 'inc/file-types.php' ); | |
| 12 | 22 | |
| 13 | -function wpatt_format_bytes($a_bytes) | |
| 14 | - { | |
| 15 | - | |
| 16 | - if ($a_bytes < 1024) | |
| 17 | - { | |
| 18 | - | |
| 19 | - return $a_bytes . ' B'; | |
| 20 | - | |
| 23 | +add_action('init', function () { | |
| 24 | + load_plugin_textdomain( 'wp-attachments' ); | |
| 25 | +}); | |
| 26 | + | |
| 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 | + } | |
| 35 | +}); | |
| 36 | + | |
| 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 | +/** | |
| 123 | + * Handle ?download=ID hits: count the click, then redirect to the real file. | |
| 124 | + * | |
| 125 | + * Runs on 'template_redirect' because conditional tags such as is_attachment() | |
| 126 | + * are not reliable before the main query has run. | |
| 127 | + */ | |
| 128 | +add_action('template_redirect', function () { | |
| 129 | + if ( ! get_option('wpatt_counter') || ! isset($_GET['download']) ) { | |
| 130 | + return; | |
| 131 | + } | |
| 132 | + | |
| 133 | + if ( is_attachment() ) { | |
| 134 | + return; | |
| 135 | + } | |
| 136 | + | |
| 137 | + $download_id = absint( wp_unslash($_GET['download']) ); | |
| 138 | + if ( ! $download_id || get_post_type($download_id) !== 'attachment' ) { | |
| 139 | + return; | |
| 140 | + } | |
| 141 | + | |
| 142 | + if ( ! wpa_can_download( $download_id ) ) { | |
| 143 | + return; | |
| 144 | + } | |
| 145 | + | |
| 146 | + $excludelogged = true; | |
| 147 | + if ( get_option('wpatt_excludelogged_counter') ) { | |
| 148 | + $excludelogged = !is_user_logged_in(); | |
| 149 | + } | |
| 150 | + | |
| 151 | + if ( $excludelogged && wpa_is_countable_request() && wpa_is_valid_download($download_id) ) { | |
| 152 | + $newcounter = intval(get_post_meta($download_id, "wpa-download", true)); | |
| 153 | + $newcounter++; | |
| 154 | + update_post_meta($download_id, 'wpa-download', $newcounter ); | |
| 155 | + } | |
| 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. | |
| 160 | + $redirect_url = wp_get_attachment_url($download_id); | |
| 161 | + if ($redirect_url) { | |
| 162 | + wp_redirect(esc_url_raw($redirect_url)); | |
| 163 | + exit; | |
| 164 | + } | |
| 165 | +}); | |
| 166 | + | |
| 167 | +/** | |
| 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 | |
| 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 | + } | |
| 21 | 206 | } |
| 22 | - elseif ($a_bytes < 1048576) | |
| 23 | - { | |
| 24 | - | |
| 25 | - return round($a_bytes / 1024, 2) . ' KB'; | |
| 26 | - | |
| 27 | - } | |
| 28 | - elseif ($a_bytes < 1073741824) | |
| 29 | - { | |
| 30 | - | |
| 31 | - return round($a_bytes / 1048576, 2) . ' MB'; | |
| 32 | - | |
| 33 | - } | |
| 34 | - elseif ($a_bytes < 1099511627776) | |
| 35 | - { | |
| 36 | - | |
| 37 | - return round($a_bytes / 1073741824, 2) . ' GB'; | |
| 38 | - | |
| 39 | - } | |
| 40 | - else | |
| 41 | - { | |
| 42 | - | |
| 43 | - return round($a_bytes / 1208925819614629174706176, 2) . ' ERROR'; | |
| 44 | - | |
| 45 | - } | |
| 46 | - | |
| 47 | 207 | } |
| 48 | 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 | +} | |
| 49 | 217 | |
| 50 | -add_filter('the_content', 'wpatt_job_cpt_template_filter'); | |
| 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 | + */ | |
| 226 | +add_action('deleted_post', function($post_id, $post) { | |
| 227 | + if ( ! $post || $post->post_type !== 'attachment' ) { | |
| 228 | + return; | |
| 229 | + } | |
| 51 | 230 | |
| 52 | -function wpatt_job_cpt_template_filter($content) | |
| 53 | - { | |
| 54 | - global $post; | |
| 55 | - | |
| 231 | + if ( ! isset($_REQUEST['forcedelete']) || $_REQUEST['forcedelete'] !== 'true' ) { | |
| 232 | + return; | |
| 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); | |
| 243 | +}, 10, 2); | |
| 244 | + | |
| 245 | +add_action('admin_init', function() { | |
| 246 | + require_once(plugin_dir_path(__FILE__) . 'inc/settings.php'); | |
| 247 | + require_once(plugin_dir_path(__FILE__) . 'inc/meta-box.php'); | |
| 248 | + require_once(plugin_dir_path(__FILE__) . 'inc/post-columns.php'); | |
| 249 | + if (get_option('wpatt_counter')) { require_once(plugin_dir_path(__FILE__) . 'inc/counter.php'); } | |
| 250 | + | |
| 251 | + // get_plugin_data() re-read and parsed this file on every admin request. | |
| 252 | + update_option( 'wpa_version_number', WPATT_VERSION ); | |
| 253 | +} ); | |
| 254 | + | |
| 255 | +/** | |
| 256 | + * Format a byte count for display. | |
| 257 | + * | |
| 258 | + * Thin wrapper around core size_format(); kept as a function for | |
| 259 | + * back-compat with themes that may already call it. | |
| 260 | + * | |
| 261 | + * @param int $a_bytes Size in bytes. | |
| 262 | + * @param int $decimals Decimal places to show. | |
| 263 | + * @return string | |
| 264 | + */ | |
| 265 | +function wpatt_format_bytes($a_bytes, $decimals = 0) { | |
| 266 | + $a_bytes = (int) $a_bytes; | |
| 267 | + | |
| 268 | + // size_format() would render this as '0.0 B' when decimals are requested. | |
| 269 | + if ( $a_bytes <= 0 ) { | |
| 270 | + return size_format( 0 ); | |
| 271 | + } | |
| 272 | + | |
| 273 | + $formatted = size_format( $a_bytes, $decimals ); | |
| 274 | + | |
| 275 | + return ( false === $formatted ) ? size_format( 0 ) : $formatted; | |
| 276 | +} | |
| 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 | + | |
| 302 | +add_action('woocommerce_order_details_after_customer_details', function( $order ) { | |
| 303 | + // "My Account" > "Order View". Under HPOS an order is not a post, so it has | |
| 304 | + // no ->ID and must not be passed through the the_content filter. | |
| 305 | + if ( ! is_object( $order ) || ! method_exists( $order, 'get_id' ) ) { | |
| 306 | + return; | |
| 307 | + } | |
| 308 | + if ( ! is_wc_endpoint_url( 'view-order' ) ) { | |
| 309 | + return; | |
| 310 | + } | |
| 311 | + | |
| 312 | + echo wpatt_get_attachments_html( $order->get_id() ); | |
| 313 | +}, 10, 1 ); | |
| 314 | + | |
| 315 | +/** | |
| 316 | + * Tell WooCommerce this plugin is safe with High-Performance Order Storage. | |
| 317 | + * Without it WooCommerce lists the plugin as incompatible. | |
| 318 | + */ | |
| 319 | +add_action('before_woocommerce_init', function() { | |
| 320 | + if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) { | |
| 321 | + \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); | |
| 322 | + } | |
| 323 | +}); | |
| 324 | + | |
| 325 | + | |
| 326 | +add_filter('the_content', 'wpatt_content_filter'); | |
| 327 | + | |
| 328 | +function wpatt_content_filter( $content, $post = null ) { | |
| 329 | + if ( !$post ) { | |
| 330 | + global $post; | |
| 331 | + } | |
| 332 | + | |
| 333 | + if ( !is_object($post) || empty($post->ID) || post_password_required() || ( get_option('wpatt_option_restrictload') && !is_single() && !is_page() ) ) { | |
| 334 | + return $content; | |
| 335 | + } | |
| 336 | + | |
| 337 | + if ( ! wpatt_is_frontend_enabled( $post->post_type ) || ! wpatt_is_display_enabled( $post ) ) { | |
| 338 | + return $content; | |
| 339 | + } | |
| 340 | + | |
| 341 | + if ( ! wpatt_should_render( $post ) ) { | |
| 342 | + return $content; | |
| 343 | + } | |
| 344 | + | |
| 345 | + return $content . wpatt_get_attachments_html( $post->ID ); | |
| 346 | +} | |
| 347 | + | |
| 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 | +/** | |
| 414 | + * Build the attachments list for a given parent ID. | |
| 415 | + * | |
| 416 | + * Kept separate from the the_content filter so callers that are not posts -- | |
| 417 | + * WooCommerce orders under HPOS, for instance -- can render the same list | |
| 418 | + * without faking a WP_Post object. | |
| 419 | + * | |
| 420 | + * @param int $parent_id Parent object ID. | |
| 421 | + * @return string HTML, or an empty string when there is nothing to show. | |
| 422 | + */ | |
| 423 | +function wpatt_get_attachments_html( $parent_id ) { | |
| 424 | + $parent_id = absint( $parent_id ); | |
| 425 | + if ( ! $parent_id ) { | |
| 426 | + return ''; | |
| 427 | + } | |
| 428 | + | |
| 429 | + $content = ''; | |
| 430 | + | |
| 431 | + $orderby = sanitize_text_field(get_query_var('orderby')); | |
| 432 | + $order = 'ASC'; | |
| 433 | + if ($orderby === 'date') { | |
| 434 | + $order = 'DESC'; | |
| 435 | + } elseif ($orderby !== 'title') { | |
| 436 | + $orderby = 'menu_order'; | |
| 437 | + } | |
| 438 | + | |
| 56 | 439 | $attachments = get_posts(array( |
| 57 | - 'post_type' => 'attachment', | |
| 58 | - 'orderby' => 'menu_order', | |
| 59 | - 'posts_per_page' => 100, | |
| 60 | - 'post_parent' => $post->ID | |
| 440 | + 'post_type' => 'attachment', | |
| 441 | + 'orderby' => $orderby, | |
| 442 | + 'order' => $order, | |
| 443 | + 'posts_per_page' => -1, | |
| 444 | + 'post_status' => 'any', | |
| 445 | + 'post_parent' => $parent_id | |
| 61 | 446 | )); |
| 62 | - | |
| 63 | - if ($attachments) | |
| 64 | - { | |
| 65 | - | |
| 66 | - $content .= '<div style="width:100%;float:left;margin:10px 0 10px 0;"><h3>' . get_option('wpatt_option_localization') . '</h3> | |
| 67 | - <style> | |
| 68 | - ul.post-attachments{list-style:none;margin:0;} | |
| 69 | - li.post-attachment{background:url(' . plugin_dir_url(__FILE__) . 'icons/document.png) 0 4px no-repeat;padding-left:24px} .post-attachment.mime-imagejpeg,.post-attachment.mime-imagepng{background-image:url(' . plugin_dir_url(__FILE__) . 'icons/document-image.png)} | |
| 70 | - .post-attachment.mime-applicationzip{background-image:url(' . plugin_dir_url(__FILE__) . 'icons/document-zipper.png)} | |
| 71 | - .post-attachment.mime-applicationpdf{background-image:url(' . plugin_dir_url(__FILE__) . 'icons/document-pdf.png)} | |
| 72 | - .post-attachment.mime-applicationvnd-ms-excel{background-image:url(' . plugin_dir_url(__FILE__) . 'icons/document-excel.png)} | |
| 73 | - .post-attachment.mime-applicationvnd-openxmlformats-officedocument-spreadsheetml-sheet{background-image:url(' . plugin_dir_url(__FILE__) . 'icons/document-excel.png)} | |
| 74 | - .post-attachment.mime-applicationmsword{background-image:url(' . plugin_dir_url(__FILE__) . 'icons/document-word.png)} | |
| 75 | - .post-attachment.mime-applicationvnd-openxmlformats-officedocument-wordprocessingml-document{background-image:url(' . plugin_dir_url(__FILE__) . 'icons/document-word.png)} | |
| 76 | - </style><ul class="post-attachments">'; | |
| 77 | - | |
| 78 | - foreach ($attachments as $attachment) | |
| 79 | - { | |
| 80 | - | |
| 81 | - | |
| 82 | - $class = "post-attachment mime-" . sanitize_title($attachment->post_mime_type); | |
| 83 | - | |
| 84 | - $content .= '<li class="' . $class . '"><a href="' . wp_get_attachment_url($attachment->ID) . '">' . $attachment->post_title . '</a> (' . wpatt_format_bytes(filesize(get_attached_file($attachment->ID))); | |
| 85 | - | |
| 86 | - $wpatt_option_showdate_get = get_option('wpatt_option_showdate'); | |
| 87 | - | |
| 88 | - if ($wpatt_option_showdate_get == '1') | |
| 89 | - { | |
| 90 | - | |
| 91 | - $wpatt_date = new DateTime($attachment->post_date); | |
| 92 | - | |
| 93 | - $content .= '<div style="float:right;">' . $wpatt_date->format('d.m.Y') . '</div>'; | |
| 94 | - | |
| 95 | - } | |
| 96 | - | |
| 97 | - $content .= ')</li>'; | |
| 98 | - | |
| 447 | + | |
| 448 | + $toShow = 0; | |
| 449 | + | |
| 450 | + $orderby_html = ''; | |
| 451 | + if ( get_option('wpatt_show_orderby') != 0 && count($attachments) > 1 ) { | |
| 452 | + $sort_links = array( | |
| 453 | + 'menu_order' => array( esc_html__( 'Default', 'wp-attachments' ), remove_query_arg( 'orderby' ) ), | |
| 454 | + 'date' => array( esc_html__( 'Date', 'wp-attachments' ), add_query_arg( 'orderby', 'date' ) ), | |
| 455 | + 'title' => array( esc_html__( 'Name', 'wp-attachments' ), add_query_arg( 'orderby', 'title' ) ), | |
| 456 | + ); | |
| 457 | + | |
| 458 | + $sort_items = ''; | |
| 459 | + foreach ( $sort_links as $sort_key => $sort_link ) { | |
| 460 | + $is_current = ( $orderby === $sort_key ); | |
| 461 | + $sort_items .= '<a class="wpa-orderby-link' . ( $is_current ? ' is-current' : '' ) . '"' | |
| 462 | + . ' href="' . esc_url( $sort_link[1] ) . '"' | |
| 463 | + . ( $is_current ? ' aria-current="true"' : '' ) . '>' | |
| 464 | + . $sort_link[0] . '</a>'; | |
| 465 | + } | |
| 466 | + | |
| 467 | + $orderby_html = '<span class="wpa-orderby"><span class="wpa-orderby-label">' | |
| 468 | + . esc_html__( 'Sort by:', 'wp-attachments' ) . '</span>' . $sort_items . '</span>'; | |
| 469 | + } | |
| 470 | + | |
| 471 | + if ($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 | + : ''; | |
| 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 | + | |
| 488 | + foreach ($attachments as $attachment) { | |
| 489 | + $include_images = get_option('wpatt_option_includeimages'); | |
| 490 | + if ($include_images !== '1' && wp_attachment_is_image( $attachment->ID )) { | |
| 491 | + continue; | |
| 99 | 492 | } |
| 100 | - $content .= '</ul></div>'; | |
| 101 | - | |
| 493 | + | |
| 494 | + if ( !apply_filters( 'wpatt_accepted_formats', sanitize_title($attachment->post_mime_type) ) ) { | |
| 495 | + continue; | |
| 496 | + } | |
| 497 | + | |
| 498 | + $items .= wpatt_render_list_item( $template_id, $template, wpatt_get_entry_data( $attachment ) ); | |
| 499 | + $toShow = 1; | |
| 102 | 500 | } |
| 501 | + | |
| 502 | + if ( $toShow ) { | |
| 503 | + $content .= apply_filters( 'wpatt_list_html', wpatt_wrap_list( $items, $head_html, $template_id ) ); | |
| 504 | + } | |
| 505 | + } | |
| 103 | 506 | return $content; |
| 104 | - | |
| 507 | +} | |
| 508 | + | |
| 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>• %SIZE% • %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>'; | |
| 105 | 532 | } |
| 533 | +} | |
| 106 | 534 | |
| 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 | + } | |
| 107 | 560 | |
| 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 | +} | |
| 108 | 568 | |
| 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 ); | |
| 577 | + | |
| 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 | +} | |
| 593 | + | |
| 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 | + } | |
| 605 | + | |
| 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']; | |
| 680 | + } | |
| 681 | + } | |
| 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>'; | |
| 686 | +} | |
| 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 | + ); | |
| 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 | + | |
| 109 | 737 | /* Register Settings */ |
| 110 | 738 | |
| 111 | -add_action('admin_init', 'wpatt_reg_settings'); | |
| 112 | - | |
| 113 | -function wpatt_reg_settings() | |
| 114 | - { | |
| 115 | - | |
| 116 | - register_setting('wpatt_options_group', 'wpatt_option_showdate', 'intval'); | |
| 117 | - | |
| 118 | - register_setting('wpatt_options_group', 'wpatt_option_localization'); | |
| 119 | - | |
| 120 | - /* Preopulate 'Attachments' */ | |
| 121 | - | |
| 122 | - $wpatt_option_showdate_get = get_option('wpatt_option_showdate'); | |
| 123 | - | |
| 124 | - if (get_option('wpatt_option_localization') == '') | |
| 125 | - { | |
| 126 | - | |
| 127 | - update_option('wpatt_option_localization', 'Attachments'); | |
| 128 | - | |
| 739 | +function wpa_get_downloads($ID) { | |
| 740 | + if (get_post_meta($ID, "wpa-download", true)) { | |
| 741 | + return get_post_meta($ID, "wpa-download", true); | |
| 742 | + } else { return 0; } | |
| 743 | +} | |
| 744 | +/** | |
| 745 | + * Should this request be allowed to move a download counter at all? | |
| 746 | + * | |
| 747 | + * Filters out browser speculative loads and obvious automation, which would | |
| 748 | + * otherwise inflate the numbers without anybody having clicked anything. | |
| 749 | + * | |
| 750 | + * @return bool | |
| 751 | + */ | |
| 752 | +function wpa_is_countable_request() { | |
| 753 | + // Chrome and Firefox announce prefetch / prerender / preview loads. | |
| 754 | + $speculative_headers = array( 'HTTP_SEC_PURPOSE', 'HTTP_PURPOSE', 'HTTP_X_PURPOSE', 'HTTP_X_MOZ' ); | |
| 755 | + foreach ( $speculative_headers as $header ) { | |
| 756 | + if ( ! empty( $_SERVER[ $header ] ) | |
| 757 | + && preg_match( '/prefetch|prerender|preview/i', (string) $_SERVER[ $header ] ) ) { | |
| 758 | + return false; | |
| 129 | 759 | } |
| 130 | - | |
| 131 | 760 | } |
| 132 | 761 | |
| 762 | + $agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? (string) $_SERVER['HTTP_USER_AGENT'] : ''; | |
| 133 | 763 | |
| 764 | + // No user agent at all is almost always a script. | |
| 765 | + $countable = ( '' !== $agent ); | |
| 134 | 766 | |
| 135 | -/* Here starts the code for the option panel */ | |
| 767 | + if ( $countable ) { | |
| 768 | + $bots = '/bot|crawl|spider|slurp|curl|wget|python-requests|okhttp|headless' | |
| 769 | + . '|facebookexternalhit|whatsapp|telegram|monitor|uptime|pingdom|lighthouse|preview/i'; | |
| 770 | + $countable = ! preg_match( $bots, $agent ); | |
| 771 | + } | |
| 136 | 772 | |
| 773 | + /** | |
| 774 | + * Filter whether the current request may increment a download counter. | |
| 775 | + * | |
| 776 | + * @param bool $countable Whether the request looks like a real visitor. | |
| 777 | + */ | |
| 778 | + return (bool) apply_filters( 'wpatt_count_download_request', $countable ); | |
| 779 | +} | |
| 137 | 780 | |
| 781 | +/** | |
| 782 | + * Has this visitor already been counted for this file recently? | |
| 783 | + * | |
| 784 | + * The throttle key is a salted hash held in a transient, so nothing | |
| 785 | + * identifying is written anywhere and the record expires by itself. The old | |
| 786 | + * implementation stored a plain text IP address in post meta, kept only one | |
| 787 | + * address per attachment -- which meant two visitors alternating cancelled | |
| 788 | + * each other's throttle -- and never expired. | |
| 789 | + * | |
| 790 | + * @param int $ID Attachment ID. | |
| 791 | + * @return bool True when the hit should be counted. | |
| 792 | + */ | |
| 793 | +function wpa_is_valid_download( $ID ) { | |
| 794 | + $ID = absint( $ID ); | |
| 795 | + if ( ! $ID ) { | |
| 796 | + return false; | |
| 797 | + } | |
| 138 | 798 | |
| 139 | -add_action('admin_menu', 'wpatt_plugin_menu'); | |
| 799 | + // REMOTE_ADDR only: HTTP_CLIENT_IP and X_FORWARDED_FOR are attacker | |
| 800 | + // controlled, so trusting them made the throttle trivial to bypass. | |
| 801 | + $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? (string) $_SERVER['REMOTE_ADDR'] : ''; | |
| 140 | 802 | |
| 803 | + // No usable address: count the hit rather than silently drop it. | |
| 804 | + if ( '' === $ip ) { | |
| 805 | + return true; | |
| 806 | + } | |
| 141 | 807 | |
| 808 | + /** | |
| 809 | + * Filter how long the same visitor is ignored for the same file. | |
| 810 | + * | |
| 811 | + * @param int $seconds Throttle window. | |
| 812 | + * @param int $ID Attachment ID. | |
| 813 | + */ | |
| 814 | + $window = (int) apply_filters( 'wpatt_download_throttle', 5 * MINUTE_IN_SECONDS, $ID ); | |
| 815 | + if ( $window < 1 ) { | |
| 816 | + return true; | |
| 817 | + } | |
| 142 | 818 | |
| 143 | -function wpatt_plugin_menu() | |
| 144 | - { | |
| 145 | - | |
| 146 | - add_options_page('WP Attachments - Settings', 'WP Attachments', 'manage_options', 'wpatt-option-page', 'wpatt_plugin_options'); | |
| 147 | - | |
| 819 | + $key = 'wpa_dl_' . wp_hash( $ID . '|' . $ip ); | |
| 820 | + | |
| 821 | + if ( get_transient( $key ) ) { | |
| 822 | + return false; | |
| 148 | 823 | } |
| 149 | 824 | |
| 825 | + set_transient( $key, 1, $window ); | |
| 150 | 826 | |
| 827 | + return true; | |
| 828 | +} | |
| 151 | 829 | |
| 152 | -function wpatt_plugin_options() | |
| 153 | - { | |
| 154 | - | |
| 155 | - if (!current_user_can('manage_options')) | |
| 156 | - { | |
| 157 | - | |
| 158 | - wp_die(__('You do not have sufficient permissions to access this page.')); | |
| 159 | - | |
| 160 | - } | |
| 161 | - | |
| 162 | - | |
| 163 | - | |
| 164 | - if (isset($_POST['Submit'])) | |
| 165 | - { | |
| 166 | - | |
| 167 | - $wpatt_option_localization_get = $_POST["wpatt_option_localization_n"]; | |
| 168 | - | |
| 169 | - update_option('wpatt_option_localization', $wpatt_option_localization_get); | |
| 170 | - | |
| 171 | - | |
| 172 | - | |
| 173 | - if (isset($_POST['wpatt_option_showdate_n'])) | |
| 174 | - { | |
| 175 | - | |
| 176 | - update_option('wpatt_option_showdate', '1'); | |
| 177 | - | |
| 178 | - } | |
| 179 | - else | |
| 180 | - { | |
| 181 | - | |
| 182 | - update_option('wpatt_option_showdate', '0'); | |
| 183 | - | |
| 184 | - } | |
| 185 | - | |
| 186 | - | |
| 187 | - | |
| 188 | - } | |
| 189 | - | |
| 190 | - | |
| 191 | - | |
| 192 | - echo '<div class="wrap">'; | |
| 193 | - | |
| 194 | - screen_icon(); | |
| 195 | - | |
| 196 | - echo '<h2>Settings</h2>'; | |
| 197 | - | |
| 198 | - echo '<div id="welcome-panel" class="welcome-panel">'; | |
| 199 | - | |
| 200 | - echo '<form method="post" name="options" target="_self">'; | |
| 201 | - | |
| 202 | - settings_fields('wpatt_option_group'); | |
| 203 | - | |
| 204 | - echo ' | |
| 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'); | |
| 205 | 834 | |
| 206 | - <table class="form-table"> | |
| 835 | +add_action('admin_menu', function() { | |
| 836 | + add_options_page('WP Attachments - Settings', 'WP Attachments', 'manage_options', 'wpatt-option-page', 'wpatt_plugin_options'); | |
| 837 | +}); | |
| 207 | 838 | |
| 208 | - | |
| 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 | + */ | |
| 846 | +function wpa_register_initial_settings() { | |
| 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') ); | |
| 209 | 851 | |
| 210 | - <tr valign="top"> | |
| 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()); | |
| 211 | 858 | |
| 212 | - <th scope="row">Label for list header</th> | |
| 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 | +} | |
| 213 | 863 | |
| 214 | - <td><input type="text" name="wpatt_option_localization_n" value="'; | |
| 215 | - | |
| 216 | - echo get_option('wpatt_option_localization'); | |
| 217 | - | |
| 218 | - echo '" /> Insert here the label you want to use for the title of the attachments list. Default "<b>Attachments</b>"</td></tr>'; | |
| 219 | - | |
| 220 | - | |
| 221 | - | |
| 222 | - echo '<tr><th scope="row">Show date?</th> | |
| 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 | +} | |
| 223 | 880 | |
| 224 | - <td><input type="checkbox" name="wpatt_option_showdate_n" '; | |
| 225 | - | |
| 226 | - $wpatt_option_showdate_get = get_option('wpatt_option_showdate'); | |
| 227 | - | |
| 228 | - if ($wpatt_option_showdate_get == '1') | |
| 229 | - { | |
| 230 | - | |
| 231 | - echo 'checked=\'checked\''; | |
| 232 | - | |
| 233 | - } | |
| 234 | - | |
| 235 | - echo '/> Check this if you want to show when the file has been uploaded.</td>'; | |
| 236 | - | |
| 237 | - echo '</tr></table>'; | |
| 238 | - | |
| 239 | - | |
| 240 | - | |
| 241 | - echo '</table><p class="submit"><input type="submit" name="Submit" value="Update" /></p>'; | |
| 242 | - | |
| 243 | - echo '</form></div></div>'; | |
| 244 | - | |
| 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; | |
| 245 | 896 | } |
| 897 | + if ( ! in_array( $opts['layout'], array( 'grid', 'rows' ), true ) ) { | |
| 898 | + $opts['layout'] = 'grid'; | |
| 899 | + } | |
| 900 | + if ( ! in_array( $opts['width'], array( 'compact', 'normal', 'wide' ), true ) ) { | |
| 901 | + $opts['width'] = 'normal'; | |
| 902 | + } | |
| 246 | 903 | |
| 247 | -?> | |
| 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'; | |
| 916 | +} | |
| 917 | + | |
| 918 | +?> | |