PluginProbe ʕ •ᴥ•ʔ
Media Cleaner: Clean your WordPress! / 6.7.4
Media Cleaner: Clean your WordPress! v6.7.4
7.1.1 7.1.0 7.0.9 7.0.8 trunk 3.6.8 3.6.9 3.7.0 3.8.0 3.9.0 4.0.0 4.0.2 4.0.4 4.0.6 4.0.7 4.1.0 4.2.0 4.2.2 4.2.3 4.2.4 4.2.5 4.4.0 4.4.2 4.4.4 4.4.6 4.4.7 4.4.8 4.5.0 4.5.4 4.5.6 4.5.7 4.5.8 4.6.2 4.6.3 4.8.0 4.8.4 5.0.0 5.0.1 5.1.0 5.1.1 5.1.3 5.2.0 5.2.1 5.2.4 5.4.0 5.4.1 5.4.2 5.4.3 5.4.4 5.4.5 5.4.6 5.4.9 5.5.0 5.5.1 5.5.2 5.5.3 5.5.4 5.5.7 5.5.8 5.6.1 5.6.2 5.6.3 5.6.4 6.0.1 6.0.2 6.0.3 6.0.4 6.0.5 6.0.6 6.0.7 6.0.8 6.0.9 6.1.2 6.1.3 6.1.4 6.1.5 6.1.6 6.1.7 6.1.8 6.1.9 6.2.0 6.2.1 6.2.3 6.2.4 6.2.5 6.2.6 6.2.7 6.2.8 6.3.0 6.3.1 6.3.2 6.3.4 6.3.5 6.3.7 6.3.8 6.3.9 6.4.0 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.4.7 6.4.8 6.4.9 6.5.0 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.5.8 6.5.9 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.6.6 6.6.7 6.6.8 6.6.9 6.7.0 6.7.1 6.7.2 6.7.3 6.7.4 6.7.5 6.7.6 6.7.7 6.7.8 6.7.9 6.8.0 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.8.6 6.8.7 6.8.8 6.8.9 6.9.0 6.9.1 6.9.2 6.9.3 6.9.4 6.9.5 6.9.6 6.9.7 6.9.8 6.9.9 7.0.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.0.6 7.0.7
media-cleaner / classes / core.php
media-cleaner / classes Last commit date
parsers 3 years ago admin.php 2 years ago core.php 2 years ago engine.php 2 years ago init.php 2 years ago parsers.php 5 years ago rest.php 2 years ago support.php 2 years ago ui.php 3 years ago
core.php
1835 lines
1 <?php
2
3 class Meow_WPMC_Core {
4
5 public $admin = null;
6 public $is_rest = false;
7 public $is_cli = false;
8 public $is_pro = false;
9 public $engine = null;
10 public $catch_timeout = true; // This will halt the plugin before reaching the PHP timeout.
11 public $types = "jpg|jpeg|jpe|gif|png|tiff|bmp|csv|svg|pdf|xls|xlsx|doc|docx|odt|wpd|rtf|tiff|mp3|mp4|mov|wav|lua";
12 public $current_method = 'media';
13 public $servername = null; // meowapps.com (site URL without http/https)
14 public $site_url = null; // https://meowapps.com
15 public $upload_path = null; // /www/wp-content/uploads (path to uploads)
16 public $upload_url = null; // wp-content/uploads (uploads without domain)
17 private $option_name = 'wpmc_options';
18
19 private $regex_file = '/[A-Za-z0-9-_,.\(\)\s]+[.]{1}(MIMETYPES)/';
20 private $refcache = array();
21 private $check_content = null;
22 private $debug_logs = null;
23 private $multilingual = false;
24 private $languages = array();
25
26 public function __construct() {
27 add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ) );
28 add_action( 'init', array( $this, 'init' ) );
29 add_action( 'delete_attachment', array( $this, 'delete_attachment_related_data' ), 10, 1 );
30 add_action( 'trashed_post', array( $this, 'delete_attachment_related_data' ), 10, 1 );
31 }
32
33 function plugins_loaded() {
34
35 // Variables
36 $this->site_url = get_site_url();
37 $this->multilingual = $this->is_multilingual();
38 $this->languages = $this->get_languages();
39 $this->current_method = $this->get_option( 'method' );
40 $this->regex_file = str_replace( "MIMETYPES", $this->types, $this->regex_file );
41 $this->servername = str_replace( 'http://', '', str_replace( 'https://', '', $this->site_url ) );
42 $uploaddir = wp_upload_dir();
43 $this->upload_path = $uploaddir['basedir'];
44 $this->upload_url = substr( $uploaddir['baseurl'], strlen( $this->site_url ) );
45 $this->check_content = $this->get_option( 'content' );
46 $this->debug_logs = $this->get_option( 'debuglogs' );
47 $this->is_rest = MeowCommon_Helpers::is_rest();
48 $this->is_cli = defined( 'WP_CLI' ) && WP_CLI;
49
50 global $wpmc;
51 $wpmc = $this;
52
53 // Language
54 load_plugin_textdomain( WPMC_DOMAIN, false, basename( WPMC_PATH ) . '/languages' );
55
56 // Admin
57 $this->admin = new Meow_WPMC_Admin( $this );
58
59 // Advanced core
60 if ( class_exists( 'MeowPro_WPMC_Core' ) ) {
61 new MeowPro_WPMC_Core( $this );
62 }
63
64 // Install hooks and engine only if they might be used
65 if ( is_admin() || $this->is_rest || $this->is_cli ) {
66 add_action( 'wpmc_initialize_parsers', array( $this, 'initialize_parsers' ), 10, 0 );
67 add_filter( 'wp_unique_filename', array( $this, 'wp_unique_filename' ), 10, 3 );
68 $this->engine = new Meow_WPMC_Engine( $this, $this->admin );
69 }
70
71 // Only for REST
72 if ( $this->is_rest ) {
73 new Meow_WPMC_Rest( $this, $this->admin );
74 }
75
76 if ( is_admin() ) {
77 new Meow_WPMC_UI( $this );
78 }
79 }
80
81 function init() {
82 remove_action( 'wp_scheduled_delete', 'wp_scheduled_delete' );
83 }
84
85 function initialize_parsers() {
86 include_once( 'parsers.php' );
87 new Meow_WPMC_Parsers();
88 }
89
90 function deepsleep( $seconds ) {
91 $start_time = time();
92 while( true ) {
93 if ( ( time() - $start_time ) > $seconds ) {
94 return false;
95 }
96 get_post( array( 'posts_per_page' => 50 ) );
97 }
98 }
99
100 private $start_time;
101 private $time_elapsed = 0;
102 private $time_remaining = 0;
103 private $item_scan_avg_time = 0;
104 private $wordpress_init_time = 0.5;
105 private $max_execution_time;
106 private $items_checked = 0;
107 private $items_count = 0;
108
109 function get_max_execution_time() {
110 if ( isset( $this->max_execution_time ) )
111 return $this->max_execution_time;
112
113 $this->max_execution_time = ini_get( "max_execution_time" );
114 if ( empty( $this->max_execution_time ) || $this->max_execution_time < 5 )
115 $this->max_execution_time = 30;
116
117 return $this->max_execution_time;
118 }
119
120 function timeout_check_start( $count ) {
121 $this->start_time = time();
122 $this->items_count = $count;
123 $this->get_max_execution_time();
124 }
125
126 function timeout_get_elapsed() {
127 return $this->time_elapsed . 'ms';
128 }
129
130 function timeout_check() {
131 $this->time_elapsed = time() - $this->start_time;
132 $this->time_remaining = $this->max_execution_time - $this->wordpress_init_time - $this->time_elapsed;
133 if ( $this->catch_timeout ) {
134 if ( $this->time_remaining - $this->item_scan_avg_time < 0 ) {
135 error_log("Media Cleaner Timeout! Check the Media Cleaner logs for more info.");
136 $this->log( "😵 Timeout! Some info for debug:" );
137 $this->log( "🍀 Elapsed time: $this->time_elapsed" );
138 $this->log( "🍀 WP init time: $this->wordpress_init_time" );
139 $this->log( "🍀 Remaining time: $this->time_remaining" );
140 $this->log( "🍀 Scan time per item: $this->item_scan_avg_time" );
141 $this->log( "🍀 PHP max_execution_time: $this->max_execution_time" );
142 header("HTTP/1.0 408 Request Timeout");
143 exit;
144 }
145 }
146 }
147
148 function delete_attachment_related_data( $post_id ) {
149 global $wpdb;
150 $table_name = $wpdb->prefix . "mclean_scan";
151 $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE postId = %d", $post_id ) );
152 }
153
154 function timeout_check_additem() {
155 $this->items_checked++;
156 $this->time_elapsed = time() - $this->start_time;
157 $this->item_scan_avg_time = ceil( ( $this->time_elapsed / $this->items_checked ) * 10 ) / 10;
158 }
159
160 // This checks if a new uploaded filename isn't the same one as a currently
161 // filename in the trash (that would cause issues)
162 function wp_unique_filename( $filename, $ext, $dir ) {
163 $fullpath = trailingslashit( $dir ) . $filename;
164 $relativepath = $this->clean_uploaded_filename( $fullpath );
165 $trashfilepath = trailingslashit( $this->get_trashdir() ) . $relativepath;
166 if ( file_exists( $trashfilepath ) ) {
167 $path_parts = pathinfo( $fullpath );
168 $filename_noext = $path_parts['filename'];
169 $new_filename = $filename_noext . '-' . date('Ymd-His', time()) . '.' . $path_parts['extension'];
170 //error_log( 'POTENTIALLY TRASH PATH: ' . $trashfilepath );
171 //error_log( 'POTENTIALLY NEW FILE: ' . $new_filename );
172 return $new_filename;
173 }
174 return $filename;
175 }
176
177 function array_to_ids_or_urls( &$meta, &$ids, &$urls ) {
178 foreach ( $meta as $k => $m ) {
179 if ( is_numeric( $m ) ) {
180 // Probably a Media ID
181 if ( $m > 0 )
182 array_push( $ids, $m );
183 }
184 else if ( is_array( $m ) ) {
185 // If it's an array with a width, probably that the index is the Media ID
186 if ( isset( $m['width'] ) && is_numeric( $k ) ) {
187 if ( $k > 0 )
188 array_push( $ids, $k );
189 }
190 }
191 else if ( !empty( $m ) ) {
192 // If it's a string, maybe it's a file (with an extension)
193 if ( preg_match( $this->regex_file, $m ) )
194 array_push( $urls, $m );
195 }
196 }
197 }
198
199 function get_favicon() {
200 // Yoast SEO plugin
201 $vals = get_option( 'wpseo_titles' );
202 if ( !empty( $vals ) ) {
203 $url = $vals['company_logo'];
204 if ( $this->is_url( $url ) )
205 return $this->clean_url( $url );
206 }
207 }
208
209 function get_shortcode_attributes( $shortcode_tag, $post ) {
210 if ( has_shortcode( $post->post_content, $shortcode_tag ) ) {
211 $output = array();
212 //get shortcode regex pattern wordpress function
213 $pattern = get_shortcode_regex( [ $shortcode_tag ] );
214 if ( preg_match_all( '/'. $pattern .'/s', $post->post_content, $matches ) )
215 {
216 $keys = array();
217 $output = array();
218 foreach( $matches[0] as $key => $value) {
219 // $matches[3] return the shortcode attribute as string
220 // replace space with '&' for parse_str() function
221 $get = str_replace(" ", "&" , trim( $matches[3][$key] ) );
222 $get = str_replace('"', '' , $get );
223 parse_str( $get, $sub_output );
224
225 //get all shortcode attribute keys
226 $keys = array_unique( array_merge( $keys, array_keys( $sub_output )) );
227 $output[] = $sub_output;
228 }
229 if ( $keys && $output ) {
230 // Loop the output array and add the missing shortcode attribute key
231 foreach ($output as $key => $value) {
232 // Loop the shortcode attribute key
233 foreach ($keys as $attr_key) {
234 $output[$key][$attr_key] = isset( $output[$key] ) && isset( $output[$key] ) ? $output[$key][$attr_key] : NULL;
235 }
236 //sort the array key
237 ksort( $output[$key]);
238 }
239 }
240 }
241 return $output;
242 }
243 else {
244 return false;
245 }
246 }
247
248 function get_urls_from_html( $html ) {
249 if ( empty( $html ) ) {
250 return array();
251 }
252
253 // Proposal/fix by @copytrans
254 // Discussion: https://wordpress.org/support/topic/bug-in-core-php/#post-11647775
255 // Modified by Jordy again in 2021 for those who don't have MB enabled
256 if ( function_exists( 'mb_convert_encoding' ) ) {
257 $html = mb_convert_encoding( $html, 'HTML-ENTITIES', 'UTF-8' );
258 }
259 else {
260 $html = htmlspecialchars_decode( utf8_decode( htmlentities( $html, ENT_COMPAT, 'utf-8', false ) ) );
261 }
262
263 // Resolve src-set and shortcodes
264 if ( !$this->get_option( 'shortcodes_disabled' ) ) {
265 $html = do_shortcode( $html );
266 }
267
268 // TODO: Since WP 5.5, wp_filter_content_tags should be used instead of wp_make_content_images_responsive.
269 $html = function_exists( 'wp_filter_content_tags' ) ? wp_filter_content_tags( $html ) :
270 wp_make_content_images_responsive( $html );
271
272 // Create the DOM Document
273 if ( !class_exists("DOMDocument") ) {
274 error_log( 'Media Cleaner: The DOM extension for PHP is not installed.' );
275 throw new Error( 'The DOM extension for PHP is not installed.' );
276 }
277
278 if ( empty( $html ) ) {
279 return array();
280 }
281
282 libxml_use_internal_errors(true);
283 $dom = new DOMDocument();
284 @$dom->loadHTML( $html );
285 libxml_clear_errors();
286 $results = array();
287
288 // <meta> tags in <head> area
289 $metas = $dom->getElementsByTagName( 'meta' );
290 foreach ( $metas as $meta ) {
291 $property = $meta->getAttribute( 'property' );
292 if ( $property == 'og:image' || $property == 'og:image:secure_url' || $property == 'twitter:image' ) {
293 $url = $meta->getAttribute( 'content' );
294 if ( $this->is_url( $url ) ) {
295 $src = $this->clean_url( $url );
296 if ( !empty( $src ) ) {
297 array_push( $results, $src );
298 }
299 }
300 }
301 }
302
303 // IFrames (by Mike Meinz)
304 $iframes = $dom->getElementsByTagName( 'iframe' );
305 foreach( $iframes as $iframe ) {
306 $iframe_src = $iframe->getAttribute( 'src' );
307 // Ignore if the iframe src is not on this server
308 if ( ( strpos( $iframe_src, $this->servername ) !== false) || ( substr( $iframe_src, 0, 1 ) == "/" ) ) {
309 // Create a new DOM Document to hold iframe
310 $iframe_doc = new DOMDocument();
311 // Load the url's contents into the DOM
312 libxml_use_internal_errors( true ); // ignore html formatting problems
313 $rslt = @$iframe_doc->loadHTMLFile( $iframe_src );
314 libxml_clear_errors();
315 libxml_use_internal_errors( false );
316 if ( $rslt ) {
317 // Get the resulting html
318 $iframe_html = $iframe_doc->saveHTML();
319 if ( $iframe_html !== false ) {
320 // Scan for links in the iframe
321 $iframe_urls = $this->get_urls_from_html( $iframe_html ); // Recursion
322 if ( !empty( $iframe_urls ) ) {
323 $results = array_merge( $results, $iframe_urls );
324 }
325 }
326 }
327 else {
328 $this->log( '🚫 Failed to load iframe: ' . $iframe_src );
329 }
330 }
331 }
332
333
334 // Images: src, srcset
335 $imgs = $dom->getElementsByTagName( 'img' );
336 foreach ( $imgs as $img ) {
337 //error_log($img->getAttribute('src'));
338 $src = $this->clean_url( $img->getAttribute('src') );
339 array_push( $results, $src );
340 $srcset = $img->getAttribute('srcset');
341 if ( !empty( $srcset ) ) {
342 $setImgs = explode( ',', trim( $srcset ) );
343 foreach ( $setImgs as $setImg ) {
344 $finalSetImg = explode( ' ', trim( $setImg ) );
345 if ( is_array( $finalSetImg ) ) {
346 array_push( $results, $this->clean_url( $finalSetImg[0] ) );
347 }
348 }
349 }
350 }
351
352 // Videos: src
353 $videos = $dom->getElementsByTagName( 'video' );
354 foreach ( $videos as $video ) {
355 //error_log($video->getAttribute('src'));
356 $src = $this->clean_url( $video->getAttribute('src') );
357 array_push( $results, $src );
358 }
359
360 // Audios: src
361 $audios = $dom->getElementsByTagName( 'audio' );
362 foreach ( $audios as $audio ) {
363 //error_log($audio->getAttribute('src'));
364 $src = $this->clean_url( $audio->getAttribute('src') );
365 array_push( $results, $src );
366 }
367
368 // Sources: src
369 $audios = $dom->getElementsByTagName( 'source' );
370 foreach ( $audios as $audio ) {
371 //error_log($audio->getAttribute('src'));
372 $src = $this->clean_url( $audio->getAttribute('src') );
373 array_push( $results, $src );
374 }
375
376 // Links, href
377 $urls = $dom->getElementsByTagName( 'a' );
378 foreach ( $urls as $url ) {
379 $url_href = $url->getAttribute('href'); // mm change
380 if ( $this->is_url( $url_href ) ) { // mm change
381 $src = $this->clean_url( $url_href ); // mm change
382 if ( !empty( $src ) )
383 array_push( $results, $src );
384 }
385 }
386
387 // <link> tags in <head> area
388 $urls = $dom->getElementsByTagName( 'link' );
389 foreach ( $urls as $url ) {
390 $url_href = $url->getAttribute( 'href' );
391 if ( $this->is_url( $url_href ) ) {
392 $src = $this->clean_url( $url_href );
393 if ( !empty( $src ) ) {
394 array_push( $results, $src );
395 }
396 }
397 }
398
399 // PDF
400 preg_match_all( "/((https?:\/\/)?[^\\&\#\[\] \"\?]+\.pdf)/", $html, $res );
401 if ( !empty( $res ) && isset( $res[1] ) && count( $res[1] ) > 0 ) {
402 foreach ( $res[1] as $url ) {
403 if ( $this->is_url( $url ) )
404 array_push( $results, $this->clean_url( $url ) );
405 }
406 }
407
408 // Background images
409 preg_match_all( "/url\(\'?\"?((https?:\/\/)?[^\\&\#\[\] \"\?]+\.(jpe?g|gif|png))\'?\"?/", $html, $res );
410 if ( !empty( $res ) && isset( $res[1] ) && count( $res[1] ) > 0 ) {
411 foreach ( $res[1] as $url ) {
412 if ( $this->is_url( $url ) )
413 array_push( $results, $this->clean_url( $url ) );
414 }
415 }
416
417 return $results;
418 }
419
420 // Parse a meta, visit all the arrays, look for the attributes, fill $ids and $urls arrays
421 // If rawMode is enabled, it will not check if the value is an ID or an URL, it will just returns it in URLs
422 function get_from_meta( $meta, $lookFor, &$ids, &$urls, $rawMode = false ) {
423 if ( !is_array( $meta ) && !is_object( $meta) ) {
424 return;
425 }
426 foreach ( $meta as $key => $value ) {
427 if ( is_object( $value ) || is_array( $value ) )
428 $this->get_from_meta( $value, $lookFor, $ids, $urls, $rawMode );
429 else if ( in_array( $key, $lookFor ) ) {
430 if ( empty( $value ) ) {
431 continue;
432 }
433 else if ( $rawMode ) {
434 array_push( $urls, $value );
435 }
436 else if ( is_numeric( $value ) ) {
437 // It this an ID?
438 array_push( $ids, $value );
439 }
440 else {
441 if ( $this->is_url( $value ) ) {
442 // Is this an URL?
443 array_push( $urls, $this->clean_url( $value ) );
444 }
445 else {
446 // Is this an array of IDs, encoded as a string? (like "20,13")
447 $pieces = explode( ',', $value );
448 foreach ( $pieces as $pval ) {
449 if ( is_numeric( $pval ) ) {
450 array_push( $ids, $pval );
451 }
452 }
453 }
454 }
455 }
456 }
457 }
458
459 function get_images_from_themes( &$ids, &$urls ) {
460 // USE CURRENT THEME AND WP API
461 $ch = get_custom_header();
462 if ( !empty( $ch ) && !empty( $ch->url ) ) {
463 array_push( $urls, $this->clean_url( $ch->url ) );
464 }
465 if ( $this->is_url( $ch->thumbnail_url ) ) {
466 array_push( $urls, $this->clean_url( $ch->thumbnail_url ) );
467 }
468 if ( !empty( $ch ) && !empty( $ch->attachment_id ) ) {
469 array_push( $ids, $ch->attachment_id );
470 }
471 $cl = get_custom_logo();
472 if ( $this->is_url( $cl ) ) {
473 $urls = array_merge( $this->get_urls_from_html( $cl ), $urls );
474 }
475 $custom_logo = get_theme_mod( 'custom_logo' );
476 if ( !empty( $custom_logo ) && is_numeric( $custom_logo ) ) {
477 array_push( $ids, (int)$custom_logo );
478 }
479 $si = get_site_icon_url();
480 if ( $this->is_url( $si ) ) {
481 array_push( $urls, $this->clean_url( $si ) );
482 }
483 $si_id = get_option( 'site_icon' );
484 if ( !empty( $si_id ) && is_numeric( $si_id ) ) {
485 array_push( $ids, (int)$si_id );
486 }
487 $cd = get_background_image();
488 if ( $this->is_url( $cd ) ) {
489 array_push( $urls, $this->clean_url( $cd ) );
490 }
491 $photography_hero_image = get_theme_mod( 'photography_hero_image' );
492 if ( !empty( $photography_hero_image ) ) {
493 array_push( $ids, $photography_hero_image );
494 }
495 $author_profile_picture = get_theme_mod( 'author_profile_picture' );
496 if ( !empty( $author_profile_picture ) ) {
497 array_push( $ids, $author_profile_picture );
498 }
499 if ( function_exists ( 'get_uploaded_header_images' ) ) {
500 $header_images = get_uploaded_header_images();
501 if ( !empty( $header_images ) ) {
502 foreach ( $header_images as $hi ) {
503 if ( !empty ( $hi['attachment_id'] ) ) {
504 array_push( $ids, $hi['attachment_id'] );
505 }
506 }
507 }
508 }
509 }
510
511 function logs_directory_check() {
512 if ( !file_exists( WPMC_PATH . '/logs/' ) ) {
513 mkdir( WPMC_PATH . '/logs/', 0777 );
514 }
515 }
516
517 function log( $data = null, $force = false ) {
518 if ( !$this->debug_logs && !$force )
519 return;
520
521 $php_logs = $this->get_option( 'php_error_logs' );
522 $log_file_path = $this->get_logs_path();
523
524 $fh = @fopen( $log_file_path, 'a' );
525 if ( !$fh ) { return false; }
526 $date = date( "Y-m-d H:i:s" );
527 if ( is_null( $data ) ) {
528 fwrite( $fh, "\n" );
529 }
530 else {
531 fwrite( $fh, "$date: {$data}\n" );
532 if ( $php_logs ) {
533 error_log( "[MEDIA CLEANER] " . $data );
534 }
535 }
536 fclose( $fh );
537 return true;
538 }
539
540 function get_logs_path() {
541 $path = $this->get_option( 'logs_path' );
542 if ( $path && file_exists( $path ) ) {
543 return $path;
544 }
545 $uploads_dir = wp_upload_dir();
546 $path = trailingslashit( $uploads_dir['basedir'] ) . WPMC_PREFIX . "_" . $this->random_ascii_chars() . ".log";
547 if ( !file_exists( $path ) ) {
548 touch( $path );
549 }
550 $options = $this->get_all_options();
551 $options['logs_path'] = $path;
552 $this->update_options( $options );
553 return $path;
554 }
555
556 private function random_ascii_chars( $length = 8 ) {
557 $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
558 $characters_length = count( $characters );
559 $random_string = '';
560
561 for ($i = 0; $i < $length; $i++) {
562 $random_string .= $characters[rand(0, $characters_length - 1)];
563 }
564
565 return $random_string;
566 }
567
568 /**
569 *
570 * HELPERS
571 *
572 */
573
574 function get_trashdir() {
575 return trailingslashit( $this->upload_path ) . 'wpmc-trash';
576 }
577
578 function get_trashurl() {
579 return trailingslashit( $this->upload_url ) . 'wpmc-trash';
580 }
581
582 function clean_ob(){
583 $disabled = $this->get_option( 'output_buffer_cleaning_disabled' );
584 $ob_content = ob_get_contents();
585 if ( !empty( trim( $ob_content ) ) ) {
586
587 if ( $disabled ) {
588 $this->log( "🚨 If the server's response was broken, try to let Output Buffer Cleaning enabled." );
589 return;
590 }
591
592 $this->log( "🧹 The response is broken due to output buffering, it will be cleaned." );
593 $this->log( "📄 Output buffer content: " . $ob_content );
594
595 ob_end_clean();
596 }
597 }
598
599 /**
600 *
601 * I18N RELATED HELPERS
602 *
603 */
604
605 function is_multilingual() {
606 return function_exists( 'icl_get_languages' );
607 }
608
609 function get_languages() {
610 $results = array();
611 if ( $this->is_multilingual() ) {
612 $languages = icl_get_languages();
613 foreach ( $languages as $language ) {
614 if ( isset( $language['code'] ) ) {
615 array_push( $results, $language['code'] );
616 }
617 else if ( isset( $language['language_code'] ) ) {
618 array_push( $results, $language['language_code'] );
619 }
620 }
621 }
622 return $results;
623 }
624
625 function get_translated_media_ids( $mediaId ) {
626 $translated_ids = array();
627 foreach ( $this->languages as $language ) {
628 $id = apply_filters( 'wpml_object_id', $mediaId, 'attachment', false, $language );
629 if ( !empty( $id ) ) {
630 array_push( $translated_ids, $id );
631 }
632 }
633 return $translated_ids;
634 }
635
636 /**
637 *
638 * DELETE / SCANNING / RESET
639 *
640 */
641
642 function recover_file( $path ) {
643 $originalPath = trailingslashit( $this->upload_path ) . $path;
644 $trashPath = trailingslashit( $this->get_trashdir() ) . $path;
645 if ( !file_exists( $trashPath ) ) {
646 $this->log( "🚫 The file $originalPath actually does not exist in the trash." );
647 return true;
648 }
649 $path_parts = pathinfo( $originalPath );
650 if ( !file_exists( $path_parts['dirname'] ) && !wp_mkdir_p( $path_parts['dirname'] ) ) {
651 die( 'Failed to create folder.' );
652 }
653 if ( !rename( $trashPath, $originalPath ) ) {
654 die( 'Failed to move the file.' );
655 }
656 return true;
657 }
658
659 function recover( $id ) {
660 global $wpdb;
661 $table_name = $wpdb->prefix . "mclean_scan";
662 $issue = $this->get_issue( $id );
663
664 if ( empty( $issue ) ) {
665 $this->log( "🚫 Issue #{$id} does not exist. Cannot recover this." );
666 return false;
667 }
668
669 // Files
670 if ( $issue->type === 0 ) {
671 $this->recover_file( $issue->path );
672 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET deleted = 0 WHERE id = %d", $id ) );
673 $this->log( "�
674 Recovered {$issue->path}." );
675 return true;
676 }
677 // Media
678 else if ( $issue->type === 1 ) {
679
680 // If there is no file attached, doesn't handle the files
681 $fullpath = get_attached_file( $issue->postId );
682 if ( empty( $fullpath ) ) {
683 $this->log( "🚫 Media #{$issue->postId} does not have attached file anymore." );
684 error_log( "Media #{$issue->postId} does not have attached file anymore." );
685 return false;
686 }
687
688 $paths = $this->get_paths_from_attachment( $issue->postId );
689 foreach ( $paths as $path ) {
690 if ( !$this->recover_file( $path ) ) {
691 $this->log( "🚫 Could not recover $path." );
692 error_log( "Media Cleaner: Could not recover $path." );
693 }
694 }
695 if ( !wp_update_post( array( 'ID' => $issue->postId, 'post_type' => 'attachment' ) ) ) {
696 $this->log( "🚫 Failed to Untrash Post {$issue->postId} (but deleted it from Cleaner DB)." );
697 error_log( "Media Cleaner: Failed to Untrash Post {$issue->postId} (but deleted it from Cleaner DB)." );
698 return false;
699 }
700 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET deleted = 0 WHERE id = %d", $id ) );
701 $this->log( "�
702 Recovered Media #{$issue->postId}." );
703 return true;
704 }
705 }
706
707 function trash_file( $fileIssuePath ) {
708 $originalPath = trailingslashit( $this->upload_path ) . $fileIssuePath;
709 $trashPath = trailingslashit( $this->get_trashdir() ) . $fileIssuePath;
710 $path_parts = pathinfo( $trashPath );
711
712 try {
713 if ( !file_exists( $path_parts['dirname'] ) && !wp_mkdir_p( $path_parts['dirname'] ) ) {
714 $this->log( "🚫 Could not create the trash directory for Media Cleaner." );
715 error_log( "Media Cleaner: Could not create the trash directory." );
716 return false;
717 }
718 // Rename the file (move). 'is_dir' is just there for security (no way we should move a whole directory)
719 if ( is_dir( $originalPath ) ) {
720 $this->log( "🚫 Attempted to delete a directory instead of a file ($originalPath). Can't do that." );
721 error_log( "Media Cleaner: Attempted to delete a directory instead of a file ($originalPath). Can't do that." );
722 return false;
723 }
724 if ( !file_exists( $originalPath ) ) {
725 $this->log( "🚫 The file $originalPath actually does not exist." );
726 error_log( "Media Cleaner: The file $originalPath actually does not exist." );
727 return true;
728 }
729 if ( !@rename( $originalPath, $trashPath ) ) {
730 error_log( "Media Cleaner: Unknown error occured while trying to delete a file ($originalPath)." );
731 return false;
732 }
733 }
734 catch ( Exception $e ) {
735 return false;
736 }
737 $this->clean_dir( dirname( $originalPath ) );
738 return true;
739 }
740
741 function repair( $id ) {
742 $repair = $this->get_repair( $id );
743 if ( empty( $repair ) ) {
744 $this->log( "🚫 Repair #{$id} does not exist. Cannot repair this." );
745 return false;
746 }
747 foreach ( $repair->child_ids as $child_id ) {
748 if ( !$this->delete( $child_id ) ) {
749 $this->log( "🚫 Failed to repair the file." );
750 return false;
751 }
752 }
753 $full_path = $this->get_full_upload_path( $repair->path );
754 $filetype = wp_check_filetype( basename( $full_path ), null );
755 $wp_upload_dir = wp_upload_dir();
756 $attachment = array(
757 'guid' => $wp_upload_dir['url'] . '/' . basename( $full_path ),
758 'post_mime_type' => $filetype['type'],
759 'post_title' => preg_replace( '/\.[^.]+$/', '', basename( $full_path ) ),
760 'post_content' => '',
761 'post_status' => 'inherit'
762 );
763
764 $attach_id = wp_insert_attachment( $attachment, $full_path );
765
766 require_once( ABSPATH . 'wp-admin/includes/image.php' );
767 $attach_data = wp_generate_attachment_metadata( $attach_id, $full_path );
768 wp_update_attachment_metadata( $attach_id, $attach_data );
769
770 global $wpdb;
771 $table_name = $wpdb->prefix . "mclean_scan";
772 $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE id = %d OR parentId = %d", $id, $id ) );
773 $this->log( "�
774 Repaired {$repair->path}." );
775 return true;
776 }
777
778 function ignore( $id, $ignore ) {
779 global $wpdb;
780 $table_name = $wpdb->prefix . "mclean_scan";
781 $issue = $this->get_issue( $id );
782
783 if ( empty( $issue ) ) {
784 $this->log( "🚫 Issue #{$id} does not exist. Cannot ignore this." );
785 return false;
786 }
787
788 if ( !$ignore ) {
789 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET ignored = 0 WHERE id = %d", $id ) );
790 }
791 else {
792 // If it is in trash, recover it
793 if ( $issue->deleted ) {
794 $this->recover( $id );
795 }
796 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET ignored = 1 WHERE id = %d", $id ) );
797 }
798 return true;
799 }
800
801 function endsWith( $haystack, $needle )
802 {
803 $length = strlen( $needle );
804 if ( $length == 0 )
805 return true;
806 return ( substr( $haystack, -$length ) === $needle );
807 }
808
809 function clean_dir( $dir ) {
810 if ( !file_exists( $dir ) )
811 return;
812 else if ( $this->endsWith( $dir, 'uploads' ) )
813 return;
814 $found = array_diff( scandir( $dir ), array( '.', '..' ) );
815 if ( count( $found ) < 1 ) {
816 if ( rmdir( $dir ) ) {
817 $this->clean_dir( dirname( $dir ) );
818 }
819 }
820 }
821
822 function get_issue( $id ) {
823 global $wpdb;
824 $table_name = $wpdb->prefix . "mclean_scan";
825 $issue = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id ), OBJECT );
826 if ( empty( $issue ) ) {
827 return false;
828 }
829 $issue->id = (int)$issue->id;
830 $issue->postId = (int)$issue->postId;
831 $issue->type = (int)$issue->type;
832 $issue->deleted = (int)$issue->deleted;
833 $issue->ignored = (int)$issue->ignored;
834 $issue->path = stripslashes( $issue->path );
835 return $issue;
836 }
837
838 function get_repair( $id ) {
839 global $wpdb;
840 $table_name = $wpdb->prefix . "mclean_scan";
841 $repair = $wpdb->get_row( $wpdb->prepare( "SELECT
842 main.id AS id,
843 main.path AS path,
844 GROUP_CONCAT(child.id) AS child_ids
845 FROM
846 $table_name AS main
847 LEFT JOIN
848 $table_name AS child ON main.id = child.parentId
849 WHERE main.id = %d", $id
850 ), OBJECT );
851 if ( empty( $repair ) ) {
852 return false;
853 }
854 $repair->id = (int)$repair->id;
855 $regex = "^(.*)(\\s\\(\\+.*)$";
856 $repair->path = preg_replace( '/' . $regex . '/i', '$1', stripslashes( $repair->path ) );
857 $repair->child_ids = $repair->child_ids ? explode( ',', $repair->child_ids ) : [];
858 return $repair;
859 }
860
861 function get_issues_to_repair( $order_by = 'id', $order = 'asc', $search = '', $skip = 0, $limit = 10 ) {
862 global $wpdb;
863 $table_name = $wpdb->prefix . "mclean_scan";
864
865 $search_clause = '';
866 if ( !empty( $search ) ) {
867 $search_clause = $wpdb->prepare("AND main.path LIKE %s", ( '%' . $search . '%' ));
868 }
869
870 $order_clause = 'ORDER BY main.id ASC';
871 if ( $order_by === 'path' ) {
872 $order_clause = 'ORDER BY main.path ' . ( $order === 'asc' ? 'ASC' : 'DESC' );
873 }
874 else if ( $order_by === 'issue' ) {
875 $order_clause = 'ORDER BY main.issue ' . ( $order === 'asc' ? 'ASC' : 'DESC' );
876 }
877 else if ( $order_by === 'size' ) {
878 $order_clause = 'ORDER BY main.size ' . ( $order === 'asc' ? 'ASC' : 'DESC' );
879 }
880
881 $result = $wpdb->get_results( $wpdb->prepare( "SELECT
882 main.id AS id,
883 main.path AS path,
884 GROUP_CONCAT(child.id) AS child_ids,
885 GROUP_CONCAT(child.path) AS child_paths,
886 main.type AS type,
887 main.postId AS postId,
888 main.size AS size,
889 main.ignored AS ignored,
890 main.deleted AS deleted,
891 main.issue AS issue
892 FROM
893 $table_name AS main
894 LEFT JOIN
895 $table_name AS child ON main.id = child.parentId
896 WHERE
897 main.path IS NOT NULL AND main.parentId IS NULL
898 AND main.deleted = 0 AND main.ignored = 0
899 AND main.type = 0
900 $search_clause
901 GROUP BY main.id
902 $order_clause
903 LIMIT %d, %d;
904 ", $skip, $limit ) );
905
906 return $result;
907 }
908
909 function get_repair_ids ( $search = '' ) {
910 global $wpdb;
911 $table_name = $wpdb->prefix . "mclean_scan";
912
913 $search_clause = '';
914 if ( !empty( $search ) ) {
915 $search_clause = $wpdb->prepare("AND main.path LIKE %s", ( '%' . $search . '%' ));
916 }
917
918 return $wpdb->get_col( "SELECT DISTINCT main.id
919 FROM
920 $table_name AS main
921 LEFT JOIN $table_name AS child ON main.id = child.parentId
922 WHERE
923 main.path IS NOT NULL
924 AND main.parentId IS NULL
925 $search_clause
926 GROUP BY
927 main.id
928 ;"
929 );
930 }
931
932 function get_stats_of_issues_to_repair( $search = '' ) {
933 global $wpdb;
934 $table_name = $wpdb->prefix . "mclean_scan";
935
936 $search_clause = '';
937 if ( !empty( $search ) ) {
938 $search_clause = $wpdb->prepare("AND main.path LIKE %s", ( '%' . $search . '%' ));
939 }
940
941 return $wpdb->get_row( "SELECT
942 COUNT(id) AS entries,
943 SUM(size) AS size
944 FROM (
945 SELECT
946 COUNT(DISTINCT main.id) as id,
947 main.size as size
948 FROM
949 $table_name AS main
950 LEFT JOIN
951 $table_name AS child ON main.id = child.parentId
952 WHERE
953 main.path IS NOT NULL AND main.parentId IS NULL AND main.deleted = 0 AND main.ignored = 0
954 $search_clause
955 GROUP BY main.id
956 ) t;
957 " );
958 }
959
960 function get_count_of_issues_to_repair( $search ) {
961 $stats = $this->get_stats_of_issues_to_repair( $search );
962 return $stats->entries;
963 }
964
965 function delete( $id ) {
966 global $wpdb;
967 $table_name = $wpdb->prefix . "mclean_scan";
968 $issue = $this->get_issue( $id );
969
970 if ( empty( $issue ) ) {
971 $this->log( "🚫 Issue #{$id} does not exist. Cannot delete this." );
972 return false;
973 }
974
975 $regex = "^(.*)(\\s\\(\\+.*)$";
976 $issue->path = preg_replace( '/' . $regex . '/i', '$1', $issue->path ); // remove " (+ 6 files)" from path
977 $skip_trash = $this->get_option( 'skip_trash' );
978
979 if ( $issue->type === 0 ) {
980
981 // Delete file from the trash
982 if ( $issue->deleted === 1 ) {
983 $trashPath = trailingslashit( $this->get_trashdir() ) . $issue->path;
984 if ( unlink( $trashPath ) ) {
985 $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE id = %d", $id ) );
986 $this->clean_dir( dirname( $trashPath ) );
987 return true;
988 }
989 }
990 // Delete file without using trash
991 else if ( $skip_trash ) {
992 $originalPath = trailingslashit( $this->upload_path ) . $issue->path;
993 if ( unlink( $originalPath ) ) {
994 $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE id = %d", $id ) );
995 $this->clean_dir( dirname( $originalPath ) );
996 return true;
997 }
998 }
999 // Move file to the trash
1000 else if ( $this->trash_file( $issue->path ) ) {
1001 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET deleted = 1, ignored = 0 WHERE id = %d", $id ) );
1002 return true;
1003 }
1004
1005 $this->log( "🚫 Failed to delete/trash the file." );
1006 error_log( "Media Cleaner: Failed to delete/trash the file." );
1007 }
1008
1009 if ( $issue->type === 1 ) {
1010
1011 // Trash Media definitely by recovering it (to be like a normal Media) and remove it through the
1012 // standard WordPress workflow
1013 if ( $issue->deleted === 1 || $skip_trash ) {
1014 if ( $issue->deleted === 1 ) {
1015 $this->recover( $id );
1016 }
1017 wp_update_post( array( 'ID' => $issue->postId, 'post_type' => 'attachment' ) );
1018 wp_delete_attachment( $issue->postId, true );
1019 $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE id = %d", $id ) );
1020 return true;
1021 }
1022 else {
1023 // Move Media to trash
1024 // Let's copy the images to the trash so that it can be recovered.
1025 $paths = $this->get_paths_from_attachment( $issue->postId );
1026 foreach ( $paths as $path ) {
1027 if ( !$this->trash_file( $path ) ) {
1028 $this->log( "🚫 Could not trash $path." );
1029 error_log( "Media Cleaner: Could not trash $path." );
1030 return false;
1031 }
1032 }
1033 wp_update_post( array( 'ID' => $issue->postId, 'post_type' => 'wmpc-trash' ) );
1034 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET deleted = 1, ignored = 0 WHERE id = %d", $id ) );
1035 return true;
1036 }
1037 }
1038 return false;
1039 }
1040
1041 /**
1042 *
1043 * SCANNING / RESET
1044 *
1045 */
1046
1047 function add_reference_url( $urlOrUrls, $type, $origin = null, $extra = null ) {
1048 $urlOrUrls = !is_array( $urlOrUrls ) ? array( $urlOrUrls ) : $urlOrUrls;
1049 foreach ( $urlOrUrls as $url ) {
1050 // With files, we need both filename without resolution and filename with resolution, it's important
1051 // to make sure the original file is not deleted if a size exists for it.
1052 // With media, all URLs should be without resolution to make sure it matches Media.
1053 if ( $this->current_method == 'files' ) {
1054 $this->add_reference( null, $url, $type, $origin );
1055 $this->add_reference( 0, $this->clean_url_from_resolution( $url ), $type, $origin );
1056 }
1057 else {
1058 // 2021/11/08: I added this, the problem is that sometimes users create image filenames with the resolution
1059 // in it, even though it is the original.
1060 $this->add_reference( null, $url, $type, $origin );
1061
1062 $this->add_reference( 0, $this->clean_url_from_resolution( $url ), $type, $origin );
1063 }
1064 }
1065 }
1066
1067 function add_reference_id( $idOrIds, $type, $origin = null, $extra = null ) {
1068 $idOrIds = !is_array( $idOrIds ) ? array( $idOrIds ) : $idOrIds;
1069 foreach ( $idOrIds as $id ) {
1070 $this->add_reference( $id, "", $type, $origin );
1071 if ( $this->multilingual ) {
1072 $translatedIds = $this->get_translated_media_ids( (int)$id );
1073
1074 // Test for WPML
1075 // if ( $id === '350') {
1076 // $translatedIds = $this->get_translated_media_ids( (int)$id );
1077 // $count = count($translatedIds);
1078 // error_log( "${id} => ${count}" );
1079 // }
1080
1081 if ( !empty( $translatedIds ) ) {
1082 foreach ( $translatedIds as $translatedId ) {
1083 $this->add_reference( $translatedId, "", $type, $origin );
1084 }
1085 }
1086 }
1087 }
1088 }
1089
1090 private $cached_ids = array();
1091 private $cached_urls = array();
1092
1093 // Returns the reference with the type, origin, related to a Media ID it is referenced
1094 public function get_reference_for_media_id( $id ) {
1095 global $wpdb;
1096 $table_name = $wpdb->prefix . "mclean_refs";
1097 $refs = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_name WHERE mediaId = %d", $id ), OBJECT );
1098 if ( empty( $refs ) ) {
1099 return false;
1100 }
1101 $ref = $refs[0];
1102 $ref->id = (int)$ref->id;
1103 $ref->mediaId = (int)$ref->mediaId;
1104 $ref->originType = (int)$ref->originType;
1105 $ref->origin = stripslashes( $ref->origin );
1106 $ref->parentId = empty( $ref->parentId ) ? null : (int)$ref->parentId;
1107 return $ref;
1108 }
1109
1110 // Return the references related to a Post ID
1111 public function get_references_for_post_id( $id ) {
1112 global $wpdb;
1113 $table_name = $wpdb->prefix . "mclean_refs";
1114 $refs = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_name WHERE originType LIKE %s", "%[$id]" ), OBJECT );
1115 if ( empty( $refs ) ) {
1116 return [];
1117 }
1118 $fresh_refs = array();
1119 foreach ( $refs as $ref ) {
1120 $mediaId = (int)$ref->mediaId > 0 ? (int)$ref->mediaId : null;
1121 if ( !$mediaId && !empty( $ref->mediaUrl ) ) {
1122 $mediaId = $this->find_media_id_from_file( $ref->mediaUrl, false );
1123 $mediaId = !empty( $mediaId ) ? (int)$mediaId : null;
1124 }
1125 if ( !$mediaId ) {
1126 continue;
1127 }
1128 array_push( $fresh_refs, [
1129 'id' => (int)$ref->id,
1130 'mediaId' => $mediaId,
1131 'mediaUrl' => $ref->mediaUrl,
1132 'originType' => $ref->originType,
1133 'parentId' => empty( $ref->parentId ) ? null : (int)$ref->parentId,
1134 ] );
1135 }
1136 return $fresh_refs;
1137 }
1138
1139 // The references are actually not being added directly in the DB, they are being pushed
1140 // into a cache ($this->refcache).
1141 private function add_reference( $id, $url, $type, $origin = null, $extra = null ) {
1142
1143 if ( !empty( $origin ) ) {
1144 $type = $type . " [$origin]";
1145 }
1146
1147 if ( !empty( $id ) ) {
1148 if ( !in_array( $id, $this->cached_ids ) ) {
1149 array_push( $this->cached_ids, $id );
1150 array_push( $this->refcache, array( 'id' => $id, 'url' => null, 'type' => $type, 'origin' => $origin ) );
1151 }
1152 }
1153 if ( !empty( $url ) ) {
1154 // The URL shouldn't contain http, https, javascript at the beginning (and there are probably many more cases)
1155 // The URL must be cleaned before being passed as a reference.
1156 if ( substr( $url, 0, 5 ) === "http:" || substr( $url, 0, 6 ) === "https:" || substr( $url, 0, 11 ) === "javascript:" ) {
1157 return;
1158 }
1159 if ( !in_array( $url, $this->cached_urls ) ) {
1160 array_push( $this->cached_urls, $url );
1161 array_push( $this->refcache, array( 'id' => null, 'url' => $url, 'type' => $type, 'origin' => $origin ) );
1162 }
1163 }
1164 }
1165
1166 function insert_references($entries)
1167 {
1168 global $wpdb;
1169 $table = $wpdb->prefix . "mclean_refs";
1170 $values = array();
1171 $place_holders = array();
1172 $query = "INSERT INTO $table (mediaId, mediaUrl, originType, parentId) VALUES ";
1173
1174 foreach ( $entries as $value ) {
1175 if ( !is_null($value['id'] ) ) {
1176 // Media Reference
1177 array_push( $values, $value['id'], $value['type'] );
1178 $place_holders[] = "('%d', NULL, '%s', NULL)";
1179
1180 if ($this->debug_logs) {
1181 $this->log("+ Media #{$value['id']} (as ID)");
1182 }
1183 }
1184 else if ( !is_null($value['url'] ) ) {
1185 // File Reference
1186 array_push( $values, $value['url'], $value['type'] );
1187 if ( isset( $value['parentId'] ) ) {
1188 array_push( $values, $value['parentId'] );
1189 $place_holders[] = "(NULL, '%s', '%s', '%d')";
1190 if ( $this->debug_logs ) {
1191 $this->log( "{$value['url']} (as URL) (ParentID: {$value['parentId']})" );
1192 }
1193 } else {
1194 $place_holders[] = "(NULL, '%s', '%s', NULL)";
1195 if ( $this->debug_logs ) {
1196 $this->log("{$value['url']} (as URL)");
1197 }
1198 }
1199 }
1200 }
1201
1202 if ( !empty( $values ) ) {
1203 $query .= implode( ', ', $place_holders );
1204 $prepared = $wpdb->prepare( "$query ", $values );
1205 $wpdb->query( $prepared );
1206 }
1207 }
1208
1209
1210 // The cache containing the references is wrote to the DB.
1211 function write_references() {
1212 global $wpdb;
1213 $table = $wpdb->prefix . "mclean_refs";
1214
1215 $potential_parents = array();
1216 $potential_children = array();
1217
1218 foreach ( $this->refcache as $value ) {
1219 $potentialParentPath = !is_null( $value['url'] ) ? $this->clean_url_from_resolution( $value['url'] ) : null;
1220 if ( $potentialParentPath === $value['url'] ) {
1221 $potential_parents[] = $value;
1222 }
1223 else {
1224 $potential_children[] = $value;
1225 }
1226 }
1227
1228 $this->insert_references( $potential_parents );
1229
1230 // Resolve parentId for potential children
1231 foreach ( $potential_children as &$child ) {
1232 $potentialParentPath = $this->clean_url_from_resolution( $child['url'] );
1233 $parentId = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table WHERE mediaUrl = %s", $potentialParentPath ) );
1234 if ( !empty( $parentId ) ) {
1235 $child['parentId'] = (int)$parentId;
1236 }
1237 }
1238
1239 // Insert potential children with resolved parentIds
1240 $this->insert_references( $potential_children );
1241 $this->refcache = array();
1242 }
1243
1244 function check_is_ignore( $file ) {
1245 global $wpdb;
1246 $table_name = $wpdb->prefix . "mclean_scan";
1247 $count = $wpdb->get_var( "SELECT COUNT(*)
1248 FROM $table_name
1249 WHERE ignored = 1
1250 AND path LIKE '%". esc_sql( $wpdb->esc_like( $file ) ) . "%'" );
1251 if ( $count > 0 ) {
1252 $this->log( "🚫 Could not trash $file." );
1253 }
1254 return ($count > 0);
1255 }
1256
1257 function find_media_id_from_file( $file, $doLog ) {
1258 global $wpdb;
1259 $postmeta_table_name = $wpdb->prefix . 'postmeta';
1260 $file = $this->clean_uploaded_filename( $file );
1261 $sql = $wpdb->prepare( "SELECT post_id
1262 FROM {$postmeta_table_name}
1263 WHERE meta_key = '_wp_attached_file'
1264 AND meta_value = %s", $file
1265 );
1266 $ret = $wpdb->get_var( $sql );
1267 if ( $doLog ) {
1268 if ( empty( $ret ) )
1269 $this->log( "🚫 File $file not found as _wp_attached_file (Library)." );
1270 else {
1271 $this->log( "�
1272 File $file found as Media $ret." );
1273 }
1274 }
1275
1276 return $ret;
1277 }
1278
1279 function get_image_sizes() {
1280 $sizes = array();
1281 global $_wp_additional_image_sizes;
1282 foreach ( get_intermediate_image_sizes() as $s ) {
1283 $crop = false;
1284 if ( isset( $_wp_additional_image_sizes[$s] ) ) {
1285 $width = intval( $_wp_additional_image_sizes[$s]['width'] );
1286 $height = intval( $_wp_additional_image_sizes[$s]['height'] );
1287 $crop = $_wp_additional_image_sizes[$s]['crop'];
1288 } else {
1289 $width = get_option( $s.'_size_w' );
1290 $height = get_option( $s.'_size_h' );
1291 $crop = get_option( $s.'_crop' );
1292 }
1293 $sizes[$s] = array( 'width' => $width, 'height' => $height, 'crop' => $crop );
1294 }
1295 return $sizes;
1296 }
1297
1298 function clean_url_from_resolution( $url ) {
1299 $pattern = '/[_-]\d+x\d+(?=\.[a-z]{3,4}$)/';
1300 $url = preg_replace( $pattern, '', $url );
1301 return $url;
1302 }
1303
1304 function is_url( $url ) {
1305 return ( (
1306 !empty( $url ) ) &&
1307 is_string( $url ) &&
1308 strlen( $url ) > 4 && (
1309 strtolower( substr( $url, 0, 4) ) == 'http' || $url[0] == '/'
1310 )
1311 );
1312 }
1313
1314 function clean_url_from_resolution_ref( &$url ) {
1315 $url = $this->clean_url_from_resolution( $url );
1316 }
1317
1318 // From a url to the shortened and cleaned url (for example '2013/02/file.png')
1319 function clean_url( $url ) {
1320 // if ( is_array( $url ) ) {
1321 // error_log( print_r( $url, 1 ) );
1322 // }
1323 $dirIndex = strpos( $url, $this->upload_url );
1324 if ( empty( $url ) || $dirIndex === false ) {
1325 $finalUrl = null;
1326 }
1327 else {
1328 $finalUrl = urldecode( substr( $url, 1 + strlen( $this->upload_url ) + $dirIndex ) );
1329 }
1330 return $finalUrl;
1331 }
1332
1333 // From a fullpath to the shortened and cleaned path (for example '2013/02/file.png')
1334 // Original version by Jordy
1335 // function clean_uploaded_filename( $fullpath ) {
1336 // $basedir = $this->upload_path;
1337 // $file = str_replace( $basedir, '', $fullpath );
1338 // $file = str_replace( "./", "", $file );
1339 // $file = trim( $file, "/" );
1340 // return $file;
1341 // }
1342
1343 // From a fullpath to the shortened and cleaned path (for example '2013/02/file.png')
1344 // Faster version, more difficult to read, by Mike Meinz
1345 function clean_uploaded_filename( $fullpath ) {
1346 $dirIndex = strpos( $fullpath, $this->upload_url );
1347 if ( $dirIndex == false ) {
1348 $file = $fullpath;
1349 }
1350 else {
1351 // Remove first part of the path leaving yyyy/mm/filename.ext
1352 $file = substr( $fullpath, 1 + strlen( $this->upload_url ) + $dirIndex );
1353 }
1354 if ( substr( $file, 0, 2 ) == './' ) {
1355 $file = substr( $file, 2 );
1356 }
1357 if ( substr( $file, 0, 1 ) == '/' ) {
1358 $file = substr( $file, 1 );
1359 }
1360 return $file;
1361 }
1362
1363 /*
1364 Check if the file or the Media ID is used in the install.
1365 That file or ID will be checked against the database of references created by the plugin
1366 by the parsers.
1367 */
1368 public function reference_exists( $file, $mediaId ) {
1369 global $wpdb;
1370 $table = $wpdb->prefix . "mclean_refs";
1371 $row = null;
1372 if ( !empty( $mediaId ) ) {
1373 $row = $wpdb->get_row( $wpdb->prepare( "SELECT originType FROM $table WHERE mediaId = %d", $mediaId ) );
1374 if ( !empty( $row ) ) {
1375 $origin = $row->originType === 'MEDIA LIBRARY' ? 'Media Library' : 'content';
1376 $this->log( "�
1377 Media #{$mediaId} used by {$origin}" );
1378 return $row->originType;
1379 }
1380 }
1381 if ( !empty( $file ) ) {
1382 $row = $wpdb->get_row( $wpdb->prepare( "SELECT originType FROM $table WHERE mediaUrl = %s", $file ) );
1383 if ( !empty( $row ) ) {
1384 $origin = $row->originType === 'MEDIA LIBRARY' ? 'Media Library' : 'content';
1385 $this->log( "�
1386 File {$file} used by {$origin}" );
1387 return $row->originType;
1388 }
1389 }
1390 return false;
1391 }
1392
1393 function get_full_upload_path( $relative_path ) {
1394 $wp_upload_dir = wp_upload_dir();
1395 $full_path = trailingslashit( $wp_upload_dir['basedir'] ) . $relative_path;
1396 return $full_path;
1397 }
1398
1399 function get_paths_from_attachment( $attachmentId ) {
1400 $paths = array();
1401 $fullpath = get_attached_file( $attachmentId );
1402 if ( empty( $fullpath ) ) {
1403 error_log( 'Media Cleaner: Could not find attached file for Media ID ' . $attachmentId );
1404 return array();
1405 }
1406 $mainfile = $this->clean_uploaded_filename( $fullpath );
1407 array_push( $paths, $mainfile );
1408 $baseUp = pathinfo( $mainfile );
1409 $filespath = trailingslashit( $this->upload_path ) . trailingslashit( $baseUp['dirname'] );
1410 $meta = wp_get_attachment_metadata( $attachmentId );
1411 if ( isset( $meta['original_image'] ) ) {
1412 $original_image = $this->clean_uploaded_filename( $filespath . $meta['original_image'] );
1413 array_push( $paths, $original_image );
1414 }
1415 $isImage = isset( $meta, $meta['width'], $meta['height'] );
1416 $sizes = $this->get_image_sizes();
1417 if ( $isImage && isset( $meta['sizes'] ) ) {
1418 foreach ( $meta['sizes'] as $name => $attr ) {
1419 if ( isset( $attr['file'] ) ) {
1420 $file = $this->clean_uploaded_filename( $filespath . $attr['file'] );
1421 array_push( $paths, $file );
1422 }
1423 }
1424 }
1425 return $paths;
1426 }
1427
1428 function is_media_ignored( $attachmentId ) {
1429 global $wpdb;
1430 $table_name = $wpdb->prefix . "mclean_scan";
1431 $issue = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE postId = %d", $attachmentId ), OBJECT );
1432 //error_log( $attachmentId );
1433 //error_log( print_r( $issue, 1 ) );
1434 if ( $issue && $issue->ignored )
1435 return true;
1436 return false;
1437 }
1438
1439 function check_media( $attachmentId, $checkOnly = false ) {
1440
1441 // Is Media ID ignored, consider as used.
1442 if ( $this->is_media_ignored( $attachmentId ) ) {
1443 return true;
1444 }
1445
1446 // Remove everything related to this media from the database.
1447 if ( !$checkOnly ) {
1448 $this->delete_attachment_related_data( $attachmentId );
1449 }
1450
1451 $size = 0;
1452 $countfiles = 0;
1453 $check_broken_media = !$this->check_content;
1454 $fullpath = get_attached_file( $attachmentId );
1455 $is_broken = apply_filters( 'wpmc_is_file_broken', !file_exists( $fullpath ), $attachmentId );
1456
1457 // It's a broken-only scan
1458 if ( $check_broken_media && !$is_broken ) {
1459 $is_considered_used = apply_filters( 'wpmc_check_media', true, $attachmentId, false );
1460 return $is_considered_used;
1461 }
1462
1463 // Let's analyze the usage of each path (thumbnails included) for this Media ID.
1464 $issue = 'NO_CONTENT';
1465 $paths = $this->get_paths_from_attachment( $attachmentId );
1466 foreach ( $paths as $path ) {
1467
1468 // If it's found in the content, we stop the scan right away
1469 if ( $this->check_content && $this->reference_exists( $path, $attachmentId ) ) {
1470 $is_considered_used = apply_filters( 'wpmc_check_media', true, $attachmentId, false );
1471 if ( $is_considered_used ) {
1472 return true;
1473 }
1474 }
1475
1476 // Let's count the size of the files for later, in case it's unused
1477 $filepath = trailingslashit( $this->upload_path ) . $path;
1478 if ( file_exists( $filepath ) )
1479 $size += filesize( $filepath );
1480 $countfiles++;
1481 }
1482
1483 // This Media ID seems not in used (or broken)
1484 // Let's double-check through the filter (overridable by users)
1485 $is_considered_used = apply_filters( 'wpmc_check_media', false, $attachmentId, $is_broken );
1486 if ( !$is_considered_used ) {
1487 if ( $is_broken ) {
1488 $this->log( "🚫 File {$fullpath} does not exist." );
1489 $issue = 'ORPHAN_MEDIA';
1490 }
1491 if ( !$checkOnly ) {
1492 global $wpdb;
1493 $table_name = $wpdb->prefix . "mclean_scan";
1494 $mainfile = $this->clean_uploaded_filename( $fullpath );
1495 $wpdb->insert( $table_name,
1496 array(
1497 'time' => current_time('mysql'),
1498 'type' => 1,
1499 'size' => $size,
1500 'path' => $mainfile . ( $countfiles > 0 ? ( " (+ " . $countfiles . " files)" ) : "" ),
1501 'postId' => $attachmentId,
1502 'issue' => $issue
1503 )
1504 );
1505 }
1506 }
1507 return $is_considered_used;
1508 }
1509
1510 // Delete all issues
1511 function reset_issues( $includingIgnored = false ) {
1512 global $wpdb;
1513 $table_name = $wpdb->prefix . "mclean_scan";
1514 if ( $includingIgnored ) {
1515 $wpdb->query( "DELETE FROM $table_name WHERE deleted = 0" );
1516 }
1517 else {
1518 $wpdb->query( "DELETE FROM $table_name WHERE ignored = 0 AND deleted = 0" );
1519 }
1520 if ( file_exists( WPMC_PATH . '/logs/media-cleaner.log' ) ) {
1521 file_put_contents( WPMC_PATH . '/logs/media-cleaner.log', '' );
1522 }
1523 }
1524
1525 function reset_references() {
1526 global $wpdb;
1527 $table_name = $wpdb->prefix . "mclean_refs";
1528 $wpdb->query("TRUNCATE $table_name");
1529 }
1530
1531 function get_issue_for_postId( $postId ) {
1532 global $wpdb;
1533 $table_name = $wpdb->prefix . "mclean_scan";
1534 $issue = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE postId = %d", $postId ), OBJECT );
1535 return $issue;
1536 }
1537
1538 function echo_issue( $issue ) {
1539 if ( $issue == 'NO_CONTENT' ) {
1540 _e( "Not found in content", 'media-cleaner' );
1541 }
1542 else if ( $issue == 'ORPHAN_FILE' ) {
1543 _e( "Not in Library", 'media-cleaner' );
1544 }
1545 else if ( $issue == 'ORPHAN_RETINA' ) {
1546 _e( "Orphan Retina", 'media-cleaner' );
1547 }
1548 else if ( $issue == 'ORPHAN_WEBP' ) {
1549 _e( "Orphan WebP", 'media-cleaner' );
1550 }
1551 else if ( $issue == 'ORPHAN_MEDIA' ) {
1552 _e( "No attached file", 'media-cleaner' );
1553 }
1554 else {
1555 echo $issue;
1556 }
1557 }
1558
1559 function get_uploads_directory_hierarchy() {
1560 $uploads_dir = wp_upload_dir();
1561 $base_dir = $uploads_dir['basedir'];
1562 $root = '/' . wp_basename( $base_dir );
1563 $directories = array();
1564
1565 // Get all subdirectories of the base directory
1566 $dir_iterator = new RecursiveDirectoryIterator( $base_dir, FilesystemIterator::KEY_AS_PATHNAME|FilesystemIterator::CURRENT_AS_FILEINFO|FilesystemIterator::SKIP_DOTS );
1567 $iterator = new RecursiveIteratorIterator( $dir_iterator, RecursiveIteratorIterator::SELF_FIRST );
1568 foreach ( $iterator as $file ) {
1569 if ( $file->isDir() ) {
1570 // Remove base_dir from path
1571 $directory = str_replace( $base_dir, '', $file->getPathname() );
1572 if ( $directory ) {
1573 $directories[] = $root . $directory;
1574 }
1575 }
1576 }
1577
1578 // Return the hierarchy as a JSON file
1579 return json_encode( $directories );
1580 }
1581
1582 /**
1583 *
1584 * Roles & Access Rights
1585 *
1586 */
1587 public function can_access_settings() {
1588 return apply_filters( 'wpmc_allow_setup', current_user_can( 'manage_options' ) );
1589 }
1590
1591 public function can_access_features() {
1592 return apply_filters( 'wpmc_allow_usage', current_user_can( 'administrator' ) );
1593 }
1594
1595 #region Options
1596
1597 function list_options() {
1598 return array(
1599 'method' => 'media',
1600 'content' => true,
1601 'filesystem_content' => false,
1602 'media_library' => true,
1603 'live_content' => false,
1604 'debuglogs' => false,
1605 'images_only' => false,
1606 'attach_is_use' => false,
1607 'thumbnails_only' => false,
1608 'dirs_filter' => '',
1609 'files_filter' => '',
1610 'hide_thumbnails' => false,
1611 'hide_warning' => false,
1612 'skip_trash' => false,
1613 'medias_buffer' => 100,
1614 'posts_buffer' => 5,
1615 'analysis_buffer' => 100,
1616 'file_op_buffer' => 20,
1617 'delay' => 100,
1618 'shortcodes_disabled' => false,
1619 'output_buffer_cleaning_disabled' => false,
1620 'php_error_logs' => false,
1621 'posts_per_page' => 10,
1622 'clean_uninstall' => false,
1623 'repair_mode' => false,
1624 'expert_mode' => false,
1625 'logs_path' => null,
1626 );
1627 }
1628
1629 function reset_options() {
1630 delete_option( $this->option_name );
1631 }
1632
1633 function get_option( $option ) {
1634 $options = $this->get_all_options();
1635 return $options[$option];
1636 }
1637
1638 function get_all_options() {
1639 $options = get_option( $this->option_name, null );
1640 $options = $this->check_options( $options );
1641 return $options;
1642 }
1643
1644 // Let's work on this function if we need it.
1645 // Right now, it looks like the options are all updated at the same time.
1646
1647 // function update_option( $option, $value ) {
1648 // if ( !array_key_exists( $name, $options ) ) {
1649 // return new WP_REST_Response([ 'success' => false, 'message' => 'This option does not exist.' ], 200 );
1650 // }
1651 // $value = is_bool( $params['value'] ) ? ( $params['value'] ? '1' : '' ) : $params['value'];
1652 // }
1653
1654 function update_options( $options ) {
1655 if ( !update_option( $this->option_name, $options, false ) ) {
1656 return false;
1657 }
1658 $options = $this->sanitize_options();
1659 return $options;
1660 }
1661
1662 // Upgrade from the old way of storing options to the new way.
1663 function check_options( $options = [] ) {
1664 $plugin_options = $this->list_options();
1665 $options = empty( $options ) ? [] : $options;
1666 $hasChanges = false;
1667 foreach ( $plugin_options as $option => $default ) {
1668 // The option already exists
1669 if ( isset( $options[$option] ) ) {
1670 continue;
1671 }
1672 // The option does not exist, so we need to add it.
1673 // Let's use the old value if any, or the default value.
1674 $options[$option] = get_option( 'wpmc_' . $option, $default );
1675 delete_option( 'wpmc_' . $option );
1676 $hasChanges = true;
1677 }
1678 if ( $hasChanges ) {
1679 update_option( $this->option_name , $options );
1680 }
1681 return $options;
1682 }
1683
1684 // Validate and keep the options clean and logical.
1685 function sanitize_options() {
1686 $options = $this->get_all_options();
1687 $medias = $options['medias_buffer'];
1688 $posts = $options['posts_buffer'];
1689 $analysis = $options['analysis_buffer'];
1690 $fileOp = $options['file_op_buffer'];
1691 $delay = $options['delay'];
1692 $hasChanges = false;
1693 if ( $medias === '' ) {
1694 $options['medias_buffer'] = 100;
1695 $hasChanges = true;
1696 }
1697 if ( $posts === '' ) {
1698 $options['posts_buffer'] = 5;
1699 $hasChanges = true;
1700 }
1701 if ( $analysis === '' ) {
1702 $options['analysis_buffer'] = 100;
1703 $hasChanges = true;
1704 }
1705 if ( $fileOp === '' ) {
1706 $options['file_op_buffer'] = 20;
1707 $hasChanges = true;
1708 }
1709 if ( $delay === '' ) {
1710 $options['delay'] = 100;
1711 $hasChanges = true;
1712 }
1713 if ( $hasChanges ) {
1714 update_option( $this->option_name, $options, false );
1715 }
1716 return $options;
1717 }
1718
1719 #endregion
1720 }
1721
1722 // Check the DB. If does not exist, let's create it.
1723 // TODO: When PHP 7 only, let's clean this and use anonymous functions.
1724 function wpmc_check_database() {
1725 global $wpdb;
1726 static $wpmc_check_database_done = false;
1727 if ( $wpmc_check_database_done ) {
1728 return true;
1729 }
1730 $table_refs = $wpdb->prefix . "mclean_refs";
1731 $table_scan = $wpdb->prefix . "mclean_scan";
1732 $db_init = !( strtolower( $wpdb->get_var( "SHOW TABLES LIKE '$table_refs'" ) ) != strtolower( $table_refs )
1733 || strtolower( $wpdb->get_var( "SHOW TABLES LIKE '$table_scan'" ) ) != strtolower( $table_scan ) );
1734 if ( !$db_init ) {
1735 wpmc_create_database();
1736 $db_init = !( strtolower( $wpdb->get_var( "SHOW TABLES LIKE '$table_refs'" ) ) != strtolower( $table_refs )
1737 || strtolower( $wpdb->get_var( "SHOW TABLES LIKE '$table_scan'" ) ) != strtolower( $table_scan ) );
1738 }
1739
1740 // Check if parentId column exists in the table
1741 // TODO: Delete this after June 2024
1742 $parentIdExists = $wpdb->get_var( "SHOW COLUMNS FROM $table_refs LIKE 'parentId'" );
1743 if ( !$parentIdExists ) {
1744 $wpdb->query( "ALTER TABLE $table_refs ADD parentId BIGINT(20) NULL;" );
1745 $wpdb->query( "ALTER TABLE $table_scan ADD parentId BIGINT(20) NULL;" );
1746 }
1747
1748 $wpmc_check_database_done = true;
1749 }
1750
1751 function wpmc_create_database() {
1752 global $wpdb;
1753 $table_name = $wpdb->prefix . "mclean_scan";
1754 $charset_collate = $wpdb->get_charset_collate();
1755 $sql = "CREATE TABLE $table_name (
1756 id BIGINT(20) NOT NULL AUTO_INCREMENT,
1757 time DATETIME DEFAULT '0000-00-00 00:00:00' NOT NULL,
1758 type TINYINT(1) NOT NULL,
1759 postId BIGINT(20) NULL,
1760 path TINYTEXT NULL,
1761 size INT(9) NULL,
1762 ignored TINYINT(1) NOT NULL DEFAULT 0,
1763 deleted TINYINT(1) NOT NULL DEFAULT 0,
1764 issue TINYTEXT NOT NULL,
1765 parentId BIGINT(20) NULL,
1766 PRIMARY KEY (id)
1767 ) " . $charset_collate . ";" ;
1768 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
1769 dbDelta( $sql );
1770 $sql="ALTER TABLE $table_name ADD INDEX IgnoredIndex (ignored) USING BTREE;";
1771 $wpdb->query($sql);
1772 $table_name = $wpdb->prefix . "mclean_refs";
1773 $charset_collate = $wpdb->get_charset_collate();
1774 // This key doesn't work on too many installs because of the 'Specified key was too long' issue
1775 // KEY mediaLookUp (mediaId, mediaUrl)
1776 $sql = "CREATE TABLE $table_name (
1777 id BIGINT(20) NOT NULL AUTO_INCREMENT,
1778 mediaId BIGINT(20) NULL,
1779 mediaUrl TINYTEXT NULL,
1780 originType TINYTEXT NOT NULL,
1781 parentId BIGINT(20) NULL,
1782 PRIMARY KEY (id)
1783 ) " . $charset_collate . ";";
1784 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
1785 dbDelta( $sql );
1786 }
1787
1788 function wpmc_remove_database() {
1789 global $wpdb;
1790 $table_name1 = $wpdb->prefix . "mclean_scan";
1791 $table_name2 = $wpdb->prefix . "mclean_refs";
1792 $table_name3 = $wpdb->prefix . "wpmcleaner";
1793 $sql = "DROP TABLE IF EXISTS $table_name1, $table_name2, $table_name3;";
1794 $wpdb->query( $sql );
1795 }
1796
1797 #region Install / Uninstall
1798
1799 /*
1800 INSTALL / UNINSTALL
1801 */
1802
1803 function wpmc_init( $mainfile ) {
1804 //register_activation_hook( $mainfile, 'wpmc_install' );
1805 //register_deactivation_hook( $mainfile, 'wpmc_uninstall' );
1806 register_uninstall_hook( $mainfile, 'wpmc_uninstall' );
1807 }
1808
1809 function wpmc_install() {
1810 wpmc_create_database();
1811 }
1812
1813 function wpmc_reset () {
1814 wpmc_remove_database();
1815 wpmc_create_database();
1816 }
1817
1818 function wpmc_remove_options() {
1819 global $wpdb;
1820 $options = $wpdb->get_results( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE 'wpmc_%'" );
1821 foreach( $options as $option ) {
1822 delete_option( $option->option_name );
1823 }
1824 }
1825
1826 function wpmc_uninstall () {
1827 $options = get_option( 'wpmc_options', [] );
1828 $cleanUninstall = $options['clean_uninstall'];
1829 if ($cleanUninstall) {
1830 wpmc_remove_options();
1831 wpmc_remove_database();
1832 }
1833 }
1834
1835 #endregion