PluginProbe
WP Attachments / 6.0.3
WP Attachments v6.0.3
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 3.5.4 All 52 releases
wp-attachments / wp-attachments.php

wp-attachments.php in WP Attachments 6.0.3, at wp-attachments.php

420 lines 15.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: WP Attachments
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 Author: Marco Milesi
7 Author URI: https://www.marcomilesi.com
8 Version: 6.0.3
9 Requires at least: 4.4
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
14 */
15
16 // Keep in sync with the Version header above.
17 define( 'WPATT_VERSION', '6.0' );
18
19 require_once( plugin_dir_path(__FILE__) . 'inc/attach_unattach_reattach.php' );
20
21 add_action('init', function () {
22 load_plugin_textdomain( 'wp-attachments' );
23
24 $wpa_ict = (int) get_option('wpa_ict', 0);
25 wp_enqueue_style('wpa-css', plugin_dir_url(__FILE__) . 'styles/' . $wpa_ict . '/wpa.css');
26 });
27
28 /**
29 * Handle ?download=ID hits: count the click, then redirect to the real file.
30 *
31 * Runs on 'template_redirect' because conditional tags such as is_attachment()
32 * are not reliable before the main query has run.
33 */
34 add_action('template_redirect', function () {
35 if ( ! get_option('wpatt_counter') || ! isset($_GET['download']) ) {
36 return;
37 }
38
39 if ( is_attachment() ) {
40 return;
41 }
42
43 $download_id = absint( wp_unslash($_GET['download']) );
44 if ( ! $download_id || get_post_type($download_id) !== 'attachment' ) {
45 return;
46 }
47
48 $excludelogged = true;
49 if ( get_option('wpatt_excludelogged_counter') ) {
50 $excludelogged = !is_user_logged_in();
51 }
52
53 if ( $excludelogged && wpa_is_countable_request() && wpa_is_valid_download($download_id) ) {
54 $newcounter = intval(get_post_meta($download_id, "wpa-download", true));
55 $newcounter++;
56 update_post_meta($download_id, 'wpa-download', $newcounter );
57 }
58
59 $redirect_url = wp_get_attachment_url($download_id);
60 if ($redirect_url) {
61 wp_safe_redirect(esc_url_raw($redirect_url));
62 exit;
63 }
64 });
65
66 /**
67 * Handle redirect after deleting an attachment from the metabox
68 */
69 add_action('deleted_post', function($post_id, $post) {
70 if ($post->post_type !== 'attachment') {
71 return;
72 }
73
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 }
80 }
81 }, 10, 2);
82
83 add_action('admin_init', function() {
84 load_plugin_textdomain( 'wp-attachments', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
85
86 require_once(plugin_dir_path(__FILE__) . 'inc/settings.php');
87 require_once(plugin_dir_path(__FILE__) . 'inc/meta-box.php');
88 require_once(plugin_dir_path(__FILE__) . 'inc/post-columns.php');
89 if (get_option('wpatt_counter')) { require_once(plugin_dir_path(__FILE__) . 'inc/counter.php'); }
90
91 // get_plugin_data() re-read and parsed this file on every admin request.
92 update_option( 'wpa_version_number', WPATT_VERSION );
93 } );
94
95 /**
96 * Format a byte count for display.
97 *
98 * Thin wrapper around core size_format(); kept as a function for
99 * back-compat with themes that may already call it.
100 *
101 * @param int $a_bytes Size in bytes.
102 * @param int $decimals Decimal places to show.
103 * @return string
104 */
105 function wpatt_format_bytes($a_bytes, $decimals = 0) {
106 $a_bytes = (int) $a_bytes;
107
108 // size_format() would render this as '0.0 B' when decimals are requested.
109 if ( $a_bytes <= 0 ) {
110 return size_format( 0 );
111 }
112
113 $formatted = size_format( $a_bytes, $decimals );
114
115 return ( false === $formatted ) ? size_format( 0 ) : $formatted;
116 }
117
118 add_action('woocommerce_order_details_after_customer_details', function( $order ) {
119 // "My Account" > "Order View". Under HPOS an order is not a post, so it has
120 // no ->ID and must not be passed through the the_content filter.
121 if ( ! is_object( $order ) || ! method_exists( $order, 'get_id' ) ) {
122 return;
123 }
124 if ( ! is_wc_endpoint_url( 'view-order' ) ) {
125 return;
126 }
127
128 echo wpatt_get_attachments_html( $order->get_id() );
129 }, 10, 1 );
130
131 /**
132 * Tell WooCommerce this plugin is safe with High-Performance Order Storage.
133 * Without it WooCommerce lists the plugin as incompatible.
134 */
135 add_action('before_woocommerce_init', function() {
136 if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
137 \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
138 }
139 });
140
141
142 add_filter('the_content', 'wpatt_content_filter');
143
144 function wpatt_content_filter( $content, $post = null ) {
145 if ( !$post ) {
146 global $post;
147 }
148
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() ) ) {
150 return $content;
151 }
152
153 $enabled = get_option('wpatt_enable_metabox_' . $post->post_type, '1');
154 if ( $enabled !== '1' ) {
155 return $content;
156 }
157
158 return $content . wpatt_get_attachments_html( $post->ID );
159 }
160
161 /**
162 * Build the attachments list for a given parent ID.
163 *
164 * Kept separate from the the_content filter so callers that are not posts --
165 * WooCommerce orders under HPOS, for instance -- can render the same list
166 * without faking a WP_Post object.
167 *
168 * @param int $parent_id Parent object ID.
169 * @return string HTML, or an empty string when there is nothing to show.
170 */
171 function wpatt_get_attachments_html( $parent_id ) {
172 $parent_id = absint( $parent_id );
173 if ( ! $parent_id ) {
174 return '';
175 }
176
177 $content = '';
178
179 $orderby = sanitize_text_field(get_query_var('orderby'));
180 $order = 'ASC';
181 if ($orderby === 'date') {
182 $order = 'DESC';
183 } elseif ($orderby !== 'title') {
184 $orderby = 'menu_order';
185 }
186
187 $attachments = get_posts(array(
188 'post_type' => 'attachment',
189 'orderby' => $orderby,
190 'order' => $order,
191 'posts_per_page' => -1,
192 'post_status' => 'any',
193 'post_parent' => $parent_id
194 ));
195
196 $toShow = 0;
197
198 $orderby_html = '';
199 if ( get_option('wpatt_show_orderby') != 0 && count($attachments) > 1 ) {
200 $sort_links = array(
201 'menu_order' => array( esc_html__( 'Default', 'wp-attachments' ), remove_query_arg( 'orderby' ) ),
202 'date' => array( esc_html__( 'Date', 'wp-attachments' ), add_query_arg( 'orderby', 'date' ) ),
203 'title' => array( esc_html__( 'Name', 'wp-attachments' ), add_query_arg( 'orderby', 'title' ) ),
204 );
205
206 $sort_items = '';
207 foreach ( $sort_links as $sort_key => $sort_link ) {
208 $is_current = ( $orderby === $sort_key );
209 $sort_items .= '<a class="wpa-orderby-link' . ( $is_current ? ' is-current' : '' ) . '"'
210 . ' href="' . esc_url( $sort_link[1] ) . '"'
211 . ( $is_current ? ' aria-current="true"' : '' ) . '>'
212 . $sort_link[0] . '</a>';
213 }
214
215 $orderby_html = '<span class="wpa-orderby"><span class="wpa-orderby-label">'
216 . esc_html__( 'Sort by:', 'wp-attachments' ) . '</span>' . $sort_items . '</span>';
217 }
218
219 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">';
227
228 foreach ($attachments as $attachment) {
229 $include_images = get_option('wpatt_option_includeimages');
230 if ($include_images !== '1' && wp_attachment_is_image( $attachment->ID )) {
231 continue;
232 }
233
234 if ( !apply_filters( 'wpatt_accepted_formats', sanitize_title($attachment->post_mime_type) ) ) {
235 continue;
236 }
237
238 $class = "post-attachment mime-" . sanitize_title($attachment->post_mime_type);
239
240 $file_path = get_attached_file($attachment->ID);
241 $wpatt_fs = (file_exists($file_path)) ? wpatt_format_bytes(filesize($file_path)) : 'ERROR';
242
243 $wpatt_date = new DateTime($attachment->post_date);
244
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 }
262
263 $wpattachments_string = apply_filters( 'wpatt_before_entry_html', $wpattachments_string );
264
265 if ( get_option('wpatt_option_targetblank') ) {
266 $wpattachments_string = str_replace('<a href', '<a target="_blank" rel="noopener noreferrer" href', $wpattachments_string);
267 }
268
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 }
274
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);
287
288 $content_l .= '<li class="' . esc_attr($class) . '">' . apply_filters( 'wpatt_after_entry_html', $wpattachments_string ) . '</li>';
289 $toShow = 1;
290 }
291 $content_l .= '</ul></div>';
292 if ( $toShow ) {
293 $content .= apply_filters( 'wpatt_list_html', $content_l );
294 }
295 }
296 return $content;
297 }
298
299
300 /* Register Settings */
301
302 function wpa_get_downloads($ID) {
303 if (get_post_meta($ID, "wpa-download", true)) {
304 return get_post_meta($ID, "wpa-download", true);
305 } else { return 0; }
306 }
307 /**
308 * Should this request be allowed to move a download counter at all?
309 *
310 * Filters out browser speculative loads and obvious automation, which would
311 * otherwise inflate the numbers without anybody having clicked anything.
312 *
313 * @return bool
314 */
315 function wpa_is_countable_request() {
316 // Chrome and Firefox announce prefetch / prerender / preview loads.
317 $speculative_headers = array( 'HTTP_SEC_PURPOSE', 'HTTP_PURPOSE', 'HTTP_X_PURPOSE', 'HTTP_X_MOZ' );
318 foreach ( $speculative_headers as $header ) {
319 if ( ! empty( $_SERVER[ $header ] )
320 && preg_match( '/prefetch|prerender|preview/i', (string) $_SERVER[ $header ] ) ) {
321 return false;
322 }
323 }
324
325 $agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? (string) $_SERVER['HTTP_USER_AGENT'] : '';
326
327 // No user agent at all is almost always a script.
328 $countable = ( '' !== $agent );
329
330 if ( $countable ) {
331 $bots = '/bot|crawl|spider|slurp|curl|wget|python-requests|okhttp|headless'
332 . '|facebookexternalhit|whatsapp|telegram|monitor|uptime|pingdom|lighthouse|preview/i';
333 $countable = ! preg_match( $bots, $agent );
334 }
335
336 /**
337 * Filter whether the current request may increment a download counter.
338 *
339 * @param bool $countable Whether the request looks like a real visitor.
340 */
341 return (bool) apply_filters( 'wpatt_count_download_request', $countable );
342 }
343
344 /**
345 * Has this visitor already been counted for this file recently?
346 *
347 * The throttle key is a salted hash held in a transient, so nothing
348 * identifying is written anywhere and the record expires by itself. The old
349 * implementation stored a plain text IP address in post meta, kept only one
350 * address per attachment -- which meant two visitors alternating cancelled
351 * each other's throttle -- and never expired.
352 *
353 * @param int $ID Attachment ID.
354 * @return bool True when the hit should be counted.
355 */
356 function wpa_is_valid_download( $ID ) {
357 $ID = absint( $ID );
358 if ( ! $ID ) {
359 return false;
360 }
361
362 // REMOTE_ADDR only: HTTP_CLIENT_IP and X_FORWARDED_FOR are attacker
363 // controlled, so trusting them made the throttle trivial to bypass.
364 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? (string) $_SERVER['REMOTE_ADDR'] : '';
365
366 // No usable address: count the hit rather than silently drop it.
367 if ( '' === $ip ) {
368 return true;
369 }
370
371 /**
372 * Filter how long the same visitor is ignored for the same file.
373 *
374 * @param int $seconds Throttle window.
375 * @param int $ID Attachment ID.
376 */
377 $window = (int) apply_filters( 'wpatt_download_throttle', 5 * MINUTE_IN_SECONDS, $ID );
378 if ( $window < 1 ) {
379 return true;
380 }
381
382 $key = 'wpa_dl_' . wp_hash( $ID . '|' . $ip );
383
384 if ( get_transient( $key ) ) {
385 return false;
386 }
387
388 set_transient( $key, 1, $window );
389
390 return true;
391 }
392
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 });
399
400 add_action('admin_menu', function() {
401 add_options_page('WP Attachments - Settings', 'WP Attachments', 'manage_options', 'wpatt-option-page', 'wpatt_plugin_options');
402 });
403
404 function wpa_register_initial_settings() {
405 if ( !get_option('wpatt_option_localization') ) {
406 update_option('wpatt_option_localization', __('Attachments','wp-attachments'));
407 }
408 if ( !get_option('wpatt_option_date_localization') ) {
409 update_option('wpatt_option_date_localization', 'd.m.Y');
410 }
411 if ( !get_option('wpa_ict') ) {
412 update_option('wpa_ict', '0');
413 }
414 if ( !get_option('wpa_template') ) {
415 update_option('wpa_template', '0');
416 }
417 }
418
419 ?>
420