PluginProbe
WP EXtra – One Click Optimize / trunk
WP EXtra – One Click Optimize vtrunk
8.7.1 8.6.8 8.7.0 trunk 5.9 8.0 8.5.0 8.5.4 8.5.5 8.6.0 8.6.1 8.6.2 8.6.3 8.6.5
wp-extra / src / Modules / Backend / Media.php

Media.php in WP EXtra – One Click Optimize trunk, at src/Modules/Backend/Media.php

629 lines 22.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace WPEXtra\Modules\Backend;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 use WPEXtra\Helper;
9 use WPEXtra\Base;
10
11 class Media extends Base {
12
13 public function __construct() {
14 parent::__construct();
15 }
16
17 protected $features = [
18 'meta_images',
19 'autoupload',
20 'image_limit',
21 'image_quality',
22 'media_thumbnails',
23 'media_functions',
24 'big_image_threshold',
25 'save_images',
26 'autoset',
27 'allow_filetype',
28 'rename_images',
29 'media_default',
30 ];
31
32 public function meta_images() {
33 add_action('add_attachment', [$this, 'update_image_metadata']);
34 }
35
36 public function autoupload() {
37 add_action('wp_handle_upload', [$this, 'auto_upload_images']);
38 }
39
40 public function image_limit() {
41 add_filter('wp_handle_upload_prefilter', [$this, 'validate_image_limit']);
42 }
43
44 public function image_quality() {
45 add_filter('jpeg_quality', [$this, 'jpeg_quality']);
46 add_filter('wp_editor_set_quality', [$this, 'jpeg_quality']);
47 }
48
49 public function media_thumbnails() {
50 add_filter('intermediate_image_sizes_advanced', [$this, 'remove_image_sizes']);
51 }
52
53 public function media_functions() {
54 $functions = (array) Helper::get_option('media_functions', []);
55 if (in_array('threshold', $functions, true) || Helper::is_feature_active('big_image_threshold')) {
56 add_filter('big_image_size_threshold', '__return_false');
57 }
58 }
59
60 public function big_image_threshold() {
61 add_filter('big_image_size_threshold', '__return_false');
62 }
63
64 public function save_images() {
65 add_action('save_post', [$this, 'save_post_images'], 10, 3);
66 }
67
68 public function autoset() {
69 add_action('save_post', [$this, 'auto_featured_image']);
70 }
71
72 public function allow_filetype() {
73 add_filter('wp_check_filetype_and_ext', [$this, 'allow_svg_filetype'], 10, 4);
74 add_filter('upload_mimes', [$this, 'allow_svg_mimes']);
75 add_filter('mime_types', [$this, 'allow_svg_mimes']);
76 add_filter('wp_handle_upload_prefilter', [$this, 'sanitize_svg_upload']);
77 add_action('admin_head', [$this, 'svg_admin_css']);
78 }
79
80 public function allow_svg_mimes($mimes) {
81 $mimes['svg'] = 'image/svg+xml';
82 $mimes['svgz'] = 'image/svg+xml';
83 $mimes['webp'] = 'image/webp';
84 $mimes['ico'] = 'image/x-icon';
85 return $mimes;
86 }
87
88 public function allow_svg_filetype($checked, $file, $filename, $mimes) {
89 if (!$checked['type']) {
90 $check = wp_check_filetype($filename, $mimes);
91 $ext = $check['ext'];
92 $type = $check['type'];
93 if ($ext === 'svg' || $ext === 'svgz') {
94 $checked = [
95 'ext' => $ext,
96 'type' => 'image/svg+xml',
97 'proper_filename' => $filename,
98 ];
99 }
100 }
101 return $checked;
102 }
103
104 public function sanitize_svg_upload($file) {
105 $ext = strtolower(pathinfo($file['name'] ?? '', PATHINFO_EXTENSION));
106 if ($ext === 'svg' || (isset($file['type']) && $file['type'] === 'image/svg+xml')) {
107 if (!current_user_can('upload_files')) {
108 $file['error'] = esc_html__('Permission denied.', 'wp-extra');
109 return $file;
110 }
111
112 if (!empty($file['tmp_name']) && file_exists($file['tmp_name'])) {
113 $content = @file_get_contents($file['tmp_name']);
114 if ($content !== false) {
115 // Check for XXE (XML External Entity Injection)
116 if (preg_match('/<!ENTITY|<!DOCTYPE|SYSTEM\s*["\']|PUBLIC\s*["\']/i', $content)) {
117 $file['error'] = esc_html__('Security Error: Uploaded SVG contains suspicious scripts or event handlers.', 'wp-extra');
118 return $file;
119 }
120
121 // Check for dangerous tags (script, foreignObject, iframe, embed, object, meta, link, applet)
122 if (preg_match('/<\s*(?:script|foreignObject|iframe|embed|object|meta|link|applet)\b/i', $content)) {
123 $file['error'] = esc_html__('Security Error: Uploaded SVG contains suspicious scripts or event handlers.', 'wp-extra');
124 return $file;
125 }
126
127 // Check for dangerous protocols and data URIs in attributes
128 if (preg_match('/(?:href|src|xlink:href)\s*=\s*["\']?\s*(?:javascript|vbscript|data:\s*text\/html|data:\s*application\/javascript):/i', $content)) {
129 $file['error'] = esc_html__('Security Error: Uploaded SVG contains suspicious scripts or event handlers.', 'wp-extra');
130 return $file;
131 }
132
133 // Check for all inline JavaScript event handlers (e.g. onload, onerror, onclick, onmouseover, onbegin, etc.)
134 if (preg_match('/\bon[a-z0-9_-]+\s*=/i', $content)) {
135 $file['error'] = esc_html__('Security Error: Uploaded SVG contains suspicious scripts or event handlers.', 'wp-extra');
136 return $file;
137 }
138 }
139 }
140 }
141 return $file;
142 }
143
144 public function svg_admin_css() {
145 echo '<style>
146 .media-icon img[src$=".svg"],
147 .attachment-preview img[src$=".svg"],
148 .thumbnail img[src$=".svg"] {
149 width: 100% !important;
150 height: auto !important;
151 }
152 </style>';
153 }
154
155 public function media_default() {
156 add_filter('get_post_metadata', [$this, 'set_media_default'], 10, 4);
157 }
158
159 public function set_media_default($null, $object_id, $meta_key, $single) {
160 if (is_admin() || (defined('DOING_AJAX') && DOING_AJAX) || (defined('REST_REQUEST') && REST_REQUEST)) {
161 return $null;
162 }
163
164 if ($meta_key !== '_thumbnail_id') {
165 return $null;
166 }
167
168 $post_type = get_post_type($object_id);
169 if (!$post_type || !post_type_supports($post_type, 'thumbnail')) {
170 return $null;
171 }
172
173 $meta_cache = wp_cache_get($object_id, 'post_meta');
174 if (!$meta_cache) {
175 $meta_cache = update_meta_cache('post', [$object_id]);
176 $meta_cache = $meta_cache[$object_id] ?? [];
177 }
178
179 if (!empty($meta_cache['_thumbnail_id'][0])) {
180 return $null;
181 }
182
183 $default_thumbnail_id = Helper::get_option('media_default');
184 if (empty($default_thumbnail_id)) {
185 return $null;
186 }
187
188 $meta_cache['_thumbnail_id'][0] = $default_thumbnail_id;
189 wp_cache_set($object_id, $meta_cache, 'post_meta');
190
191 return $default_thumbnail_id;
192 }
193
194 public function save_post_images($post_id, $post, $update) {
195 $flip = (bool) Helper::get_option('autoflip');
196 $crop_w = Helper::get_option('crop_width', '');
197 $crop_h = Helper::get_option('crop_height', '');
198 $set_quality = intval(Helper::get_option('image_quality', 90));
199
200 if (!Helper::get_option('save_images')) return;
201 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
202 if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) return;
203 if (!is_object($post) || $post->post_status !== 'publish') return;
204
205 $post_content = $post->post_content;
206
207 preg_match_all(
208 '#<img[^>]+(?:src|data-src|data-lazy|data-original|data-srcset|srcset)\s*=\s*["\']([^"\']+)["\'][^>]*?(?:alt\s*=\s*["\']([^"\']*)["\'])?[^>]*>#i',
209 stripslashes($post_content),
210 $matches,
211 PREG_SET_ORDER
212 );
213
214 if (empty($matches)) return;
215
216 $urls = [];
217 $alts = [];
218
219 foreach ($matches as $m) {
220 $raw = trim($m[1]);
221 if (strpos($raw, ',') !== false) {
222 $raw = trim(explode(',', $raw)[0]);
223 }
224 $raw = trim(explode(' ', $raw)[0]);
225
226 if ($raw) {
227 $urls[] = html_entity_decode($raw, ENT_QUOTES, 'UTF-8');
228 $alts[] = !empty($m[2]) ? wp_strip_all_tags($m[2]) : $post->post_title;
229 }
230 }
231
232 if (empty($urls)) return;
233
234 $unique_map = [];
235 foreach ($urls as $i => $u) {
236 if (!isset($unique_map[$u])) {
237 $unique_map[$u] = $alts[$i] ?? $post->post_title;
238 }
239 }
240
241 $upload_dir = wp_upload_dir();
242 $changed = false;
243 $index = 0;
244
245 foreach ($unique_map as $url => $alt_text) {
246 $url_clean = strtok($url, '?');
247 $url_clean = strtok($url_clean, '#');
248
249 $img_host = wp_parse_url($url_clean, PHP_URL_HOST);
250 $site_host = wp_parse_url(home_url(), PHP_URL_HOST);
251
252 if (!$img_host || strcasecmp($img_host, $site_host) === 0) continue;
253 if (attachment_url_to_postid($url_clean)) continue;
254
255 $index++;
256 if ($index > 20) break; // Limit to 20 images per save to avoid timeouts
257
258 $parsed = wp_parse_url($url);
259 if (empty($parsed['path'])) continue;
260
261 $try_urls = [$url];
262 $url_no_query = strtok($url, '?');
263 if ($url_no_query && $url_no_query !== $url) {
264 $try_urls[] = $url_no_query;
265 }
266 if (preg_match('#https?://i[0-2]\.wp\.com/#i', $url)) {
267 $try_urls[] = preg_replace('#https?://i[0-2]\.wp\.com/#i', 'https://', $url);
268 }
269 $try_urls = array_unique($try_urls);
270
271 if (Helper::get_option('rename_images')) {
272 $base_slug = sanitize_title($post->post_name ?: $post->post_title);
273 $img_slug = $base_slug . '-' . $index;
274 } else {
275 $filename = pathinfo(basename($parsed['path']), PATHINFO_FILENAME);
276 $img_slug = $filename ?: 'image-' . $index;
277 }
278 $img_slug = sanitize_file_name($img_slug);
279
280 $img_path = false;
281 foreach ($try_urls as $try_url) {
282 $img_path = $this->create_img(
283 $try_url,
284 $img_slug,
285 $flip,
286 $crop_w,
287 $crop_h,
288 $set_quality
289 );
290 if ($img_path) break;
291 }
292
293 if (!$img_path) continue;
294
295 $filetype = wp_check_filetype(basename($img_path), null);
296 if (empty($filetype['type'])) continue;
297
298 $target_dir = dirname($img_path);
299 $base_name = basename($img_path);
300 $unique = wp_unique_filename($target_dir, $base_name);
301 if ($unique !== $base_name) {
302 $new_path = trailingslashit($target_dir) . $unique;
303 @rename($img_path, $new_path);
304 if (file_exists($new_path)) $img_path = $new_path;
305 }
306
307 $url_new = str_replace($upload_dir['basedir'], $upload_dir['baseurl'], $img_path);
308 $url_new = str_replace('\\', '/', $url_new);
309
310 $attachment = [
311 'guid' => $url_new,
312 'post_mime_type' => $filetype['type'],
313 'post_title' => mb_substr($alt_text, 0, 200),
314 'post_content' => $alt_text,
315 'post_excerpt' => $alt_text,
316 'post_status' => 'inherit',
317 ];
318
319 $attachment_id = attachment_url_to_postid($url_new);
320 if (!$attachment_id) {
321 $attachment_id = wp_insert_attachment($attachment, $img_path, $post_id);
322 if (!is_wp_error($attachment_id)) {
323 require_once ABSPATH . 'wp-admin/includes/image.php';
324 $meta = wp_generate_attachment_metadata($attachment_id, $img_path);
325 wp_update_attachment_metadata($attachment_id, $meta);
326 }
327 }
328
329 if ($attachment_id && !is_wp_error($attachment_id)) {
330 update_post_meta($attachment_id, '_wp_attachment_image_alt', $alt_text);
331 }
332
333 foreach ($try_urls as $old) {
334 if (strpos($post_content, $old) !== false) {
335 $post_content = str_replace($old, $url_new, $post_content);
336 $changed = true;
337 }
338 }
339 }
340
341 if ($changed) {
342 remove_action('save_post', [$this, 'save_post_images'], 10);
343 wp_update_post([
344 'ID' => $post_id,
345 'post_content' => $post_content
346 ]);
347 add_action('save_post', [$this, 'save_post_images'], 10, 3);
348 }
349 }
350
351 public function create_img($url, $file_name, $flip = false, $crop_w = '', $crop_h = '', $set_quality = 90) {
352 $allowed = ['jpg','jpeg','jpe','png','gif','webp','bmp','tif','tiff','jfif'];
353 $allowed = array_map('preg_quote', $allowed);
354
355 if (!preg_match('/\.(' . implode('|', $allowed) . ')(\?|$)/i', $url, $m)) {
356 return false;
357 }
358
359 $ext = strtolower($m[1]);
360 if ($ext === 'jfif') $ext = 'jpg';
361
362 if (!function_exists('download_url')) {
363 require_once ABSPATH . 'wp-admin/includes/file.php';
364 }
365
366 $tmp = download_url($url, 10);
367
368 if (is_wp_error($tmp)) {
369 $response = wp_safe_remote_get($url, ['timeout' => 10]);
370 if (is_wp_error($response)) return false;
371
372 $mime = wp_remote_retrieve_header($response, 'content-type');
373 if (strpos($mime, 'image/') !== 0) return false;
374
375 $body = wp_remote_retrieve_body($response);
376 $tmp = wp_tempnam($url);
377 file_put_contents($tmp, $body);
378 }
379
380 $finfo = finfo_open(FILEINFO_MIME_TYPE);
381 $mime = finfo_file($finfo, $tmp);
382 finfo_close($finfo);
383 if (strpos($mime, 'image/') !== 0) {
384 wp_delete_file($tmp);
385 return false;
386 }
387
388 $upload = wp_upload_dir();
389 $path = $upload['path'] . '/' . $file_name . '.' . $ext;
390 for ($i = 1; file_exists($path); $i++) {
391 $path = $upload['path'] . '/' . $file_name . '-' . $i . '.' . $ext;
392 }
393
394 if (!@copy($tmp, $path)) {
395 wp_delete_file($tmp);
396 return false;
397 }
398 wp_delete_file($tmp);
399
400 if ($flip || ($crop_w && $crop_h)) {
401 $editor = wp_get_image_editor($path);
402 if (!is_wp_error($editor)) {
403 $editor->set_quality($set_quality);
404 if ($flip) {
405 $editor->flip(true, false);
406 }
407 if ($crop_w >= 100 && $crop_h >= 100) {
408 $editor->resize((int)$crop_w, (int)$crop_h, true);
409 }
410 $editor->save($path);
411 }
412 }
413
414 return $path;
415 }
416
417 public function auto_featured_image($post_id) {
418 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
419 if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) return;
420 if (!has_post_thumbnail($post_id)) {
421 $attached_images = get_children([
422 'post_parent' => $post_id,
423 'post_type' => 'attachment',
424 'post_mime_type' => 'image',
425 'posts_per_page' => 1,
426 ]);
427 if (!empty($attached_images)) {
428 $first_image = reset($attached_images);
429 set_post_thumbnail($post_id, $first_image->ID);
430 return;
431 }
432
433 // Fallback: Check first <img> src embedded inside post_content
434 $post = get_post($post_id);
435 if ($post && !empty($post->post_content)) {
436 if (preg_match('/<img[^>]+src=["\']([^"\']+)["\']/i', $post->post_content, $match)) {
437 $img_url = strtok($match[1], '?');
438 $attachment_id = attachment_url_to_postid($img_url);
439 if ($attachment_id) {
440 set_post_thumbnail($post_id, $attachment_id);
441 }
442 }
443 }
444 }
445 }
446
447 public function remove_image_sizes($sizes) {
448 $list_thumbnails = get_intermediate_image_sizes();
449 $disablethumbnails = (array) Helper::get_option('media_thumbnails', []);
450 foreach ($list_thumbnails as $value) {
451 if (in_array($value, $disablethumbnails, true)) {
452 unset($sizes[$value]);
453 }
454 }
455 return $sizes;
456 }
457
458 public function auto_upload_images($image_data) {
459 $autoconverter = Helper::get_option('autoconverter');
460 $max_width = intval(Helper::get_option('image_max_width', 0));
461 $max_height = intval(Helper::get_option('image_max_height', 0));
462 $quality = intval(Helper::get_option('image_quality', 90));
463
464 if (
465 $image_data['type'] === 'image/gif'
466 && $this->is_animated_gif($image_data['file'])
467 ) {
468 return $image_data;
469 }
470
471 $image_editor = wp_get_image_editor($image_data['file']);
472
473 if (!is_wp_error($image_editor)) {
474 $image_editor->set_quality($quality);
475 $sizes = $image_editor->get_size();
476
477 if (
478 ($max_width && $sizes['width'] > $max_width) ||
479 ($max_height && $sizes['height'] > $max_height)
480 ) {
481 $image_editor->resize($max_width, $max_height, false);
482 }
483
484 if ($autoconverter === 'webp' && $image_data['type'] !== 'image/webp') {
485 [$newPath, $newUrl] = $this->generate_new_path($image_data, 'webp');
486 $saved = $image_editor->save($newPath, 'image/webp');
487 if (!is_wp_error($saved)) {
488 wp_delete_file($image_data['file']);
489 $image_data['file'] = $saved['path'];
490 $image_data['url'] = $newUrl;
491 $image_data['type'] = 'image/webp';
492 }
493 } elseif ($autoconverter === 'jpg' && $image_data['type'] === 'image/png') {
494 [$newPath, $newUrl] = $this->generate_new_path($image_data, 'jpg');
495 $saved = $image_editor->save($newPath, 'image/jpeg');
496 if (!is_wp_error($saved)) {
497 wp_delete_file($image_data['file']);
498 $image_data['file'] = $saved['path'];
499 $image_data['url'] = $newUrl;
500 $image_data['type'] = 'image/jpeg';
501 }
502 } else {
503 $image_editor->save($image_data['file']);
504 }
505 }
506
507 return $image_data;
508 }
509
510 private function generate_new_path($params, $ext) {
511 $basePath = preg_replace('/\.[^.]+$/', '', $params['file']);
512 $baseUrl = preg_replace('/\.[^.]+$/', '', $params['url']);
513
514 $newPath = $basePath . '.' . $ext;
515 $newUrl = $baseUrl . '.' . $ext;
516
517 for ($i = 1; file_exists($newPath); $i++) {
518 $newPath = $basePath . "-$i.$ext";
519 $newUrl = $baseUrl . "-$i.$ext";
520 }
521
522 return [$newPath, $newUrl];
523 }
524
525 private function is_animated_gif($filename) {
526 if (!($fh = @fopen($filename, 'rb'))) {
527 return false;
528 }
529 $count = 0;
530 $chunk = false;
531
532 while (!feof($fh) && $count < 2) {
533 $chunk = ($chunk ? substr($chunk, -20) : "") . fread($fh, 1024 * 100);
534 $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
535 }
536
537 fclose($fh);
538 return $count > 1;
539 }
540
541 public function validate_image_limit($file) {
542 $limit = intval(Helper::get_option('image_limit'));
543 if (!$limit) {
544 return $file;
545 }
546 $image_size = ($file['size'] ?? 0) / 1024;
547 $is_image = isset($file['type']) && strpos($file['type'], 'image') !== false;
548 if ($image_size > $limit && $is_image) {
549 $file['error'] = sprintf(
550 __('Your picture is too large. It has to be smaller than %dKB.', 'wp-extra'),
551 $limit
552 );
553 }
554 return $file;
555 }
556
557 public function jpeg_quality($quality) {
558 return intval(Helper::get_option('image_quality', 90));
559 }
560
561 public function update_image_metadata($attachment_ID) {
562 if (Helper::get_option('meta_images')) {
563 if (!current_user_can('edit_post', $attachment_ID)) {
564 return;
565 }
566 if (!isset($_SERVER['HTTP_REFERER']) || strpos($_SERVER['HTTP_REFERER'], home_url()) !== 0) {
567 return;
568 }
569 if (!empty($_REQUEST['post_id']) && !Helper::get_option('meta_images_filename')) {
570 $post_id = (int)$_REQUEST['post_id'];
571 } else {
572 $post_id = $attachment_ID;
573 }
574 $post_object = get_post($post_id);
575 $post_title = isset($post_object->post_title) ? $post_object->post_title : '';
576 if (!empty($post_title)) {
577 $post_title = preg_replace('/\s*[-_\s]+\s*/', ' ', $post_title);
578 $post_title = ucwords(strtolower($post_title));
579 $post_data = [
580 'ID' => $attachment_ID,
581 'post_title' => $post_title,
582 'post_content' => $post_title,
583 'post_excerpt' => $post_title,
584 ];
585 update_post_meta($attachment_ID, '_wp_attachment_image_alt', $post_title);
586 wp_update_post($post_data);
587 }
588 }
589 }
590
591 public function rename_images() {
592 add_filter('sanitize_file_name', [$this, 'update_image_filename_from_post_slug'], 10, 1);
593 }
594
595 public function update_image_filename_from_post_slug($filename) {
596 $filename = Helper::normalizeString($filename);
597
598 if (!empty($_REQUEST['post_id'])) {
599 $post_id = (int)$_REQUEST['post_id'];
600 $exists = get_post_status($post_id);
601 $info = pathinfo($filename);
602
603 if (isset($exists) && !empty($info['extension']) && in_array(strtolower($info['extension']), ['jpg', 'jpeg', 'jpe', 'png', 'gif', 'webp', 'bmp', 'tif', 'tiff'], true)) {
604 $post_object = get_post($post_id);
605 $ext = empty($info['extension']) ? '' : '.' . $info['extension'];
606 $name = '';
607 $opt = Helper::get_option('rename_images');
608 if ($opt === 'date') {
609 $name = '-' . date('Y-m-d');
610 } elseif ($opt === 'filename') {
611 $name = '-' . basename($filename, $ext);
612 }
613
614 $post_name = isset($post_object->post_name) ? $post_object->post_name : '';
615 $post_title = isset($post_object->post_title) ? $post_object->post_title : '';
616
617 if (!empty($post_name)) {
618 $filename = strtolower($post_name . $name . $ext);
619 } elseif (!empty($post_title)) {
620 $normalized_post_title = Helper::normalizeString($post_title);
621 $filename = strtolower($normalized_post_title . $name . $ext);
622 }
623 }
624 }
625
626 return $filename;
627 }
628 }
629