PluginProbe ʕ •ᴥ•ʔ
Media Cleaner: Clean your WordPress! / 7.1.0
Media Cleaner: Clean your WordPress! v7.1.0
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 1 month ago admin.php 7 months ago core.php 1 month ago engine.php 1 month ago init.php 7 months ago parsers.php 10 months ago rest.php 1 month ago support.php 1 month ago ui.php 3 years ago
core.php
2539 lines
1 <?php
2
3 class Meow_WPMC_Core {
4
5
6 public $admin = null;
7 public $is_rest = false;
8 public $is_cli = false;
9 public $is_pro = false;
10 public $engine = null;
11 public $catch_timeout = true; // This will halt the plugin before reaching the PHP timeout.
12 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|webp|avif|ico";
13 public $current_method = 'media';
14 public $servername = null; // meowapps.com (site URL without http/https)
15 public $site_url = null; // https://meowapps.com
16 public $upload_path = null; // /www/wp-content/uploads (path to uploads)
17 public $upload_url = null; // wp-content/uploads (uploads without domain)
18 private $option_name = 'wpmc_options';
19 private $nonce = null; // Nonce for the REST API
20
21 private $regex_file = '/[A-Za-z0-9-_,.\(\)\s]+[.]{1}(MIMETYPES)/';
22
23 private $refcache = array();
24 private $progress_key = 'wpmc_progress';
25
26 private $check_content = null;
27 private $debug_logs = null;
28 private $multilingual = false;
29 private $languages = array();
30 private $shortcode_analysis = false;
31
32 public function get_shortcode_analysis() {
33 return $this->shortcode_analysis;
34 }
35
36 public function is_debug() {
37 return $this->debug_logs;
38 }
39
40 public function __construct() {
41 add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ) );
42 add_action( 'init', array( $this, 'init' ) );
43 add_action( 'delete_attachment', array( $this, 'delete_attachment_related_data' ), 10, 1 );
44 add_action( 'trashed_post', array( $this, 'delete_attachment_related_data' ), 10, 1 );
45 }
46
47 function plugins_loaded() {
48
49
50 if ( is_admin() ) {
51 new Meow_WPMC_UI( $this );
52 }
53
54 // Admin
55 $this->admin = new Meow_WPMC_Admin( $this );
56
57 // Advanced core
58 if ( class_exists( 'MeowPro_WPMC_Core' ) ) {
59 new MeowPro_WPMC_Core( $this );
60 }
61
62 // Only initialize variables if we are on a relevant screen
63 $pages = [ 'wpmc_dashboard', 'wpmc_settings' ];
64 $page = isset( $_GET["page"] ) ? sanitize_text_field( $_GET["page"] ) : null;
65 $is_wpmc_screen = in_array( $page, $pages );
66
67 // Check if this is a REST request specifically for Media Cleaner
68 $is_wpmc_rest = false;
69 if ( MeowKit_WPMC_Helpers::is_rest() ) {
70 $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';
71 $is_wpmc_rest = strpos( $request_uri, '/media-cleaner/v1' ) !== false;
72 }
73
74 // Variables
75 $this->site_url = get_site_url();
76 $this->multilingual = $this->is_multilingual();
77 $this->languages = $this->get_languages();
78 $this->current_method = $this->get_option( 'method' );
79 $this->regex_file = str_replace( "MIMETYPES", $this->types, $this->regex_file );
80 $this->servername = str_replace( 'http://', '', str_replace( 'https://', '', $this->site_url ) );
81 $uploaddir = wp_upload_dir();
82 $this->upload_path = $uploaddir['basedir'];
83 $this->upload_url = substr( $uploaddir['baseurl'], strlen( $this->site_url ) );
84 $this->check_content = $this->get_option( 'content' );
85 $this->debug_logs = $this->get_option( 'debuglogs' );
86 $this->is_rest = $is_wpmc_rest;
87 $this->is_cli = defined( 'WP_CLI' ) && WP_CLI;
88 $this->shortcode_analysis = !$this->get_option( 'shortcodes_disabled' );
89
90 global $wpmc;
91 $wpmc = $this;
92
93 $shouldLoad = ( defined( 'WP_CLI' ) && WP_CLI ) || $is_wpmc_screen || $is_wpmc_rest;
94
95 if ( ! $shouldLoad ) {
96 return;
97 }
98
99 // Language
100 load_plugin_textdomain( WPMC_DOMAIN, false, basename( WPMC_PATH ) . '/languages' );
101
102 // Install hooks and engine only if they might be used
103 if ( is_admin() || $this->is_rest || $this->is_cli ) {
104 add_action( 'wpmc_initialize_parsers', array( $this, 'initialize_parsers' ), 10, 0 );
105 add_filter( 'wp_unique_filename', array( $this, 'wp_unique_filename' ), 10, 3 );
106 $this->engine = new Meow_WPMC_Engine( $this, $this->admin );
107 }
108
109 // Only for REST
110 if ( $this->is_rest ) {
111 new Meow_WPMC_Rest( $this, $this->admin );
112 }
113
114
115 }
116
117 function init() {
118 remove_action( 'wp_scheduled_delete', 'wp_scheduled_delete' );
119 }
120
121 public function get_nonce( $force = false ) {
122 if ( !$force && !is_user_logged_in() ) {
123 return null;
124 }
125 if ( isset( $this->nonce ) ) {
126 return $this->nonce;
127 }
128
129 $this->nonce = wp_create_nonce( 'wp_rest' );
130 return $this->nonce;
131 }
132
133 function initialize_parsers() {
134 include_once( 'parsers.php' );
135 new Meow_WPMC_Parsers();
136 }
137
138 function deepsleep( $seconds ) {
139 $start_time = time();
140 while( true ) {
141 if ( ( time() - $start_time ) > $seconds ) {
142 return false;
143 }
144 get_post( array( 'posts_per_page' => 50 ) );
145 }
146 }
147
148 private $start_time;
149 private $time_elapsed = 0;
150 private $time_remaining = 0;
151 private $item_scan_avg_time = 0;
152 private $wordpress_init_time = 0.5;
153 private $max_execution_time;
154 private $items_checked = 0;
155 private $items_count = 0;
156
157 function get_max_execution_time() {
158 if ( isset( $this->max_execution_time ) )
159 return $this->max_execution_time;
160
161 $this->max_execution_time = ini_get( "max_execution_time" );
162 if ( empty( $this->max_execution_time ) || $this->max_execution_time < 5 )
163 $this->max_execution_time = 30;
164
165 return $this->max_execution_time;
166 }
167
168 function timeout_check_start( $count ) {
169 $this->start_time = time();
170 $this->items_count = $count;
171 $this->get_max_execution_time();
172 }
173
174 function timeout_get_elapsed() {
175 return $this->time_elapsed . 'ms';
176 }
177
178 function timeout_check() {
179 $this->time_elapsed = time() - $this->start_time;
180 $this->time_remaining = $this->max_execution_time - $this->wordpress_init_time - $this->time_elapsed;
181 if ( $this->catch_timeout ) {
182 if ( $this->time_remaining - $this->item_scan_avg_time < 0 ) {
183 error_log("Media Cleaner Timeout! Check the Media Cleaner logs for more info.");
184 $this->log( "😵 Timeout! Some info for debug:" );
185 $this->log( "🍀 Elapsed time: $this->time_elapsed" );
186 $this->log( "🍀 WP init time: $this->wordpress_init_time" );
187 $this->log( "🍀 Remaining time: $this->time_remaining" );
188 $this->log( "🍀 Scan time per item: $this->item_scan_avg_time" );
189 $this->log( "🍀 PHP max_execution_time: $this->max_execution_time" );
190 header("HTTP/1.0 408 Request Timeout");
191 exit;
192 }
193 }
194 }
195
196 function delete_attachment_related_data( $post_id ) {
197
198 if ( empty( $post_id ) ) return;
199
200 global $wpdb;
201 $table_name = $wpdb->prefix . "mclean_scan";
202
203 if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $wpdb->esc_like( $table_name ) ) ) === $table_name ) {
204 $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE postId = %d", $post_id ) );
205 } else {
206 // Table does not exist
207 }
208 }
209
210 function timeout_check_additem() {
211 $this->items_checked++;
212 $this->time_elapsed = time() - $this->start_time;
213 $this->item_scan_avg_time = ceil( ( $this->time_elapsed / $this->items_checked ) * 10 ) / 10;
214 }
215
216 // This checks if a new uploaded filename isn't the same one as a currently
217 // filename in the trash (that would cause issues)
218 function wp_unique_filename( $filename, $ext, $dir ) {
219 $fullpath = trailingslashit( $dir ) . $filename;
220 $relativepath = $this->clean_uploaded_filename( $fullpath );
221 $trashfilepath = trailingslashit( $this->get_trashdir() ) . $relativepath;
222 if ( file_exists( $trashfilepath ) ) {
223 $path_parts = pathinfo( $fullpath );
224 $filename_noext = $path_parts['filename'];
225 $new_filename = $filename_noext . '-' . date('Ymd-His', time()) . '.' . $path_parts['extension'];
226 //error_log( 'POTENTIALLY TRASH PATH: ' . $trashfilepath );
227 //error_log( 'POTENTIALLY NEW FILE: ' . $new_filename );
228 return $new_filename;
229 }
230 return $filename;
231 }
232
233 function array_to_ids_or_urls( $meta, &$ids, &$urls, $recursive = false, $filters = array() ) {
234 foreach ( $meta as $k => $m ) {
235
236 if ( is_numeric( $m ) ) {
237
238 if ( !empty( $filters ) && is_array( $filters ) && !in_array( $k, $filters ) ) {
239 continue;
240 }
241
242 // Probably a Media ID
243 if ( $m > 0 )
244 {
245 array_push( $ids, $m );
246 }
247 }
248
249 else if ( is_array( $m ) ) {
250
251
252 if ( $recursive ) {
253 // If it's an array, we need to go deeper
254 $this->array_to_ids_or_urls( $m, $ids, $urls, true, $filters );
255 }
256
257 }
258 else if ( !empty( $m ) ) {
259
260 if ( !empty( $filters ) && is_array( $filters ) && !in_array( $k, $filters ) ) {
261 continue;
262 }
263
264 if ( is_string( $m ) && preg_match( '/^[\d\s,]+$/', $m ) && strpos( $m, ',' ) !== false ) {
265 // If this is a string that contains only digits, spaces, and commas, and contains at least one comma
266 // it is probably a list of IDs. So we should explode it to make an array
267 // Remove any spaces
268
269 $m = str_replace( ' ', '', $m );
270 $m = explode( ',', $m );
271
272 foreach ( $m as $mv ) {
273 if ( is_numeric( $mv ) && !in_array( (int)$mv, $ids ) ) {
274 array_push( $ids, (int)$mv );
275 }
276 }
277
278 continue;
279 }
280
281 // If it's a string, maybe it's a file (with an extension)
282 if ( preg_match( $this->regex_file, $m ) )
283 {
284 $clean_url = $this->clean_url( $m );
285 array_push( $urls, $clean_url );
286 }
287 }
288 }
289 }
290
291 function get_favicon() {
292 // Yoast SEO plugin
293 $vals = get_option( 'wpseo_titles' );
294 if ( !empty( $vals ) && isset( $vals['company_logo'] ) ) {
295 $url = $vals['company_logo'];
296 if ( $this->is_url( $url ) )
297 return $this->clean_url( $url );
298 }
299 }
300
301 function get_all_shortcodes_attributes( $html, $ids_attr = array(), $urls_attr = array() ) {
302 // Get all the shortcodes from html, and check for each attributes of the shortcode if it is an ID or a URL and add the value in an array to return
303 $urls_values = array();
304 $ids_values = array();
305
306 $pattern = get_shortcode_regex();
307 if ( preg_match_all( '/'. $pattern .'/s', $html, $matches ) )
308 {
309 foreach( $matches[0] as $key => $value) {
310 // $matches[3] return the shortcode attribute as string
311 // replace space with '&' for parse_str() function
312 $get = str_replace(" ", "&" , trim( $matches[3][$key] ) );
313 $get = str_replace('"', '' , $get );
314 parse_str( $get, $sub_output );
315
316 foreach ( $sub_output as $attr_key => $attr_value ) {
317
318 if ( in_array( $attr_key, $ids_attr ) ) {
319 if ( is_numeric( $attr_value ) && !in_array( (int)$attr_value, $ids_values ) ) {
320 array_push( $ids_values, (int)$attr_value );
321 }
322
323 // In case of separated by commas
324 else if ( strpos( $attr_value, ',' ) !== false ) {
325 $attr_value = str_replace(' ', '', $attr_value );
326 $pieces = explode( ',', $attr_value );
327 foreach ( $pieces as $pval ) {
328 if ( is_numeric( $pval ) && !in_array( (int)$pval, $ids_values ) ) {
329 array_push( $ids_values, (int)$pval );
330 }
331 }
332 }
333 }
334
335 else if ( in_array( $attr_key, $urls_attr ) ) {
336 if ( !empty( trim( $attr_value ) ) && !in_array( trim( $attr_value ), $urls_values ) && !is_numeric( trim( $attr_value ) ) && strpos( trim( $attr_value ), 'http' ) !== false ) {
337 array_push( $urls_values, trim( $this->clean_url( $attr_value ) ) );
338 }
339 }
340 }
341 }
342 }
343
344 // Remove duplicates
345 $urls_values = array_unique( $urls_values );
346 $ids_values = array_unique( $ids_values );
347
348 // Return the values
349 $values = array(
350 'urls' => $urls_values,
351 'ids' => $ids_values
352 );
353
354 return $values;
355
356 }
357
358
359
360 /**
361 * Recursively transforms a string with WordPress shortcodes into a
362 * hierarchical tree structure (an Abstract Syntax Tree).
363 *
364 * @param string $content The string containing the shortcodes.
365 * @return array An array of nodes, where each node can be a shortcode with its
366 * own 'children' array, or a simple text node.
367 */
368 function nested_shortcodes_to_array(string $content): array
369 {
370 $nodes = [];
371 $last_pos = 0;
372
373 $pattern = '/\\[' . '(\\[?)' . '([\w-]+)' . '(?![\\w-])' . '(' . '[^\\]\\/]*' . '(?:' . '\\/(?!\\])' . '[^\\]\\/]*' . ')*?' . ')' . '(?:' . '(\\/)' . '\\]' . '|' . '\\]' . '(?:' . '(' . '[^\\[]*+' . '(?:' . '\\[(?!\\/\\2\\])' . '[^\\[]*+' . ')*+' . ')' . '\\[\\/\\2\\]' . ')?' . ')' . '(\\]?)/s';
374
375 // preg_match_all with PREG_OFFSET_CAPTURE is key to tracking positions.
376 if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
377 foreach ($matches as $match) {
378 // Get the position and content of the full shortcode match
379 $match_start_pos = $match[0][1];
380 $match_full_string = $match[0][0];
381 $match_end_pos = $match_start_pos + strlen($match_full_string);
382
383 // 1. Capture any text that appeared *before* this shortcode
384 if ($match_start_pos > $last_pos) {
385 $text_content = substr($content, $last_pos, $match_start_pos - $last_pos);
386 if (trim($text_content) !== '') {
387 $nodes[] = [
388 'type' => 'text',
389 'content' => $text_content
390 ];
391 }
392 }
393
394 // 2. Process the shortcode match itself
395 $tag = $match[2][0];
396 $attributes_string = $match[3][0];
397 // Use isset since self-closing tags won't have inner content (group 5)
398 $inner_content = isset($match[5]) ? $match[5][0] : null;
399
400 // Parse attributes from the attribute string
401 $parsed_attributes = [];
402 if (preg_match_all('/([\w-]+)\s*=\s*(["\'])([^"\']*?)\2/', $attributes_string, $attr_matches)) {
403 foreach ($attr_matches[1] as $attr_index => $key) {
404 $parsed_attributes[$key] = $attr_matches[3][$attr_index];
405 }
406 }
407
408 $shortcode_node = [
409 'type' => 'shortcode',
410 'tag' => $tag,
411 'attributes' => $parsed_attributes,
412 ];
413
414 // 3. This is the recursion!
415 // If there is inner content, parse it with the same function.
416 if ($inner_content !== null) {
417 $children = $this->nested_shortcodes_to_array($inner_content);
418 if (!empty($children)) {
419 $shortcode_node['children'] = $children;
420 }
421 }
422
423 $nodes[] = $shortcode_node;
424
425 // Update the last position to the end of the current match
426 $last_pos = $match_end_pos;
427 }
428 }
429
430 // 4. Capture any remaining text after the very last shortcode
431 if ($last_pos < strlen($content)) {
432 $text_content = substr($content, $last_pos);
433 if (trim($text_content) !== '') {
434 $nodes[] = [
435 'type' => 'text',
436 'content' => $text_content
437 ];
438 }
439 }
440
441 return $nodes;
442 }
443
444
445
446
447 function get_shortcode_attributes( $shortcode_tag, $post ) {
448 if ( has_shortcode( $post->post_content, $shortcode_tag ) ) {
449 $output = array();
450 //get shortcode regex pattern wordpress function
451 $pattern = get_shortcode_regex( [ $shortcode_tag ] );
452 if ( preg_match_all( '/'. $pattern .'/s', $post->post_content, $matches ) )
453 {
454 $keys = array();
455 $output = array();
456 foreach( $matches[0] as $key => $value) {
457 // $matches[3] return the shortcode attribute as string
458 // replace space with '&' for parse_str() function
459 $get = str_replace(" ", "&" , trim( $matches[3][$key] ) );
460 $get = str_replace('"', '' , $get );
461 parse_str( $get, $sub_output );
462
463 //get all shortcode attribute keys
464 $keys = array_unique( array_merge( $keys, array_keys( $sub_output )) );
465 $output[] = $sub_output;
466 }
467 if ( $keys && $output ) {
468 // Loop the output array and add the missing shortcode attribute key
469 foreach ($output as $key => $value) {
470 // Loop the shortcode attribute key
471 foreach ($keys as $attr_key) {
472 $output[$key][$attr_key] = isset( $output[$key] ) && isset( $output[$key] ) ? $output[$key][$attr_key] : NULL;
473 }
474 //sort the array key
475 ksort( $output[$key]);
476 }
477 }
478 }
479 return $output;
480 }
481 else {
482 return false;
483 }
484 }
485
486 // SImply use regex to get URLs from a string return an array of URLs
487 function get_urls_from_string( $string ) {
488 $urls = array();
489 // Replace the satinized urls with the real ones to be sure to get them in the regex
490 $string = str_replace( '\\', '', $string );
491
492
493 $pattern = '/(https?:\/\/[^\s\"\'\>\<\?\#]+\.(' . $this->types . '))/i';
494 if ( preg_match_all( $pattern, $string, $matches ) ) {
495 foreach ( $matches[0] as $match ) {
496 $clean_url = $this->clean_url( $match );
497 array_push( $urls, $clean_url );
498 }
499 }
500 return $urls;
501 }
502
503 function get_urls_from_html( $html ) {
504 if ( empty( $html ) ) {
505 return array();
506 }
507
508
509 // Proposal/fix by @copytrans
510 // Discussion: https://wordpress.org/support/topic/bug-in-core-php/#post-11647775
511 // Modified by Jordy again in 2021 for those who don't have MB enabled
512 if ( function_exists( 'mb_encode_numericentity' ) ) {
513 $convmap = [0x80, 0xffff, 0, 0xffff];
514 $html = mb_encode_numericentity( $html, $convmap, 'UTF-8' );
515 } else {
516 $html = preg_replace_callback(
517 '/[\x80-\xFF]/',
518 function( $match ) {
519 return '&#' . ord( $match[0] ) . ';';
520 },
521 $html
522 );
523 }
524
525 // Remove any base64 src from the HTML to prevent regex from getting stuck and crashing the site
526 // Handles both proper (data:image/...) and malformed (image/jpeg;base64,...) base64
527 // Also handles HTML-encoded quotes (&quot;) and multiline base64
528 $html = preg_replace( '/src=["\'](?:data:)?(?:image|video|audio)\/[^"\']+;base64,[^"\']*["\']/', '', $html );
529 $html = preg_replace( '/src=&quot;(?:data:)?(?:image|video|audio)\/[^&]+;base64,[^&]*&quot;/', '', $html );
530 // Catch any remaining base64 data that might cause regex issues (greedy catch-all)
531 $html = preg_replace( '/;base64,[a-zA-Z0-9+\/=\s]{1000,}/', '', $html );
532
533
534 // Resolve src-set and shortcodes
535 if ( $this->get_shortcode_analysis() ) {
536 $html = do_shortcode( $html );
537 }
538
539 // TODO: Since WP 5.5, wp_filter_content_tags should be used instead of wp_make_content_images_responsive.
540 $html = function_exists( 'wp_filter_content_tags' ) ? wp_filter_content_tags( $html ) :
541 wp_make_content_images_responsive( $html );
542
543 // Create the DOM Document
544 if ( !class_exists("DOMDocument") ) {
545 error_log( 'Media Cleaner: The DOM extension for PHP is not installed.' );
546 throw new Error( 'The DOM extension for PHP is not installed.' );
547 }
548
549
550 if ( empty( $html ) ) {
551 return array();
552 }
553
554 libxml_use_internal_errors(true);
555 $dom = new DOMDocument();
556 @$dom->loadHTML( $html );
557 libxml_clear_errors();
558 $results = array();
559
560 // <meta> tags in <head> area
561 $metas = $dom->getElementsByTagName( 'meta' );
562 foreach ( $metas as $meta ) {
563 $property = $meta->getAttribute( 'property' );
564 if ( $property == 'og:image' || $property == 'og:image:secure_url' || $property == 'twitter:image' ) {
565 $url = $meta->getAttribute( 'content' );
566 if ( $this->is_url( $url ) ) {
567 $src = $this->clean_url( $url );
568 if ( !empty( $src ) ) {
569 array_push( $results, $src );
570 }
571 }
572 }
573 }
574
575
576
577 // IFrames (by Mike Meinz)
578 $iframes = $dom->getElementsByTagName( 'iframe' );
579 foreach( $iframes as $iframe ) {
580 $iframe_src = $iframe->getAttribute( 'src' );
581 // Only process iframes that are local to this server (strict host match to prevent SSRF)
582 $parsed_url = wp_parse_url( $iframe_src );
583 $is_relative = ( substr( $iframe_src, 0, 1 ) === '/' && substr( $iframe_src, 0, 2 ) !== '//' );
584 $is_local = ( isset( $parsed_url['host'] ) && $parsed_url['host'] === $this->servername );
585 if ( $is_relative || $is_local ) {
586 // Create a new DOM Document to hold iframe
587 $iframe_doc = new DOMDocument();
588 // Load the url's contents into the DOM
589 libxml_use_internal_errors( true ); // ignore html formatting problems
590 $rslt = @$iframe_doc->loadHTMLFile( $iframe_src );
591 libxml_clear_errors();
592 libxml_use_internal_errors( false );
593 if ( $rslt ) {
594 // Get the resulting html
595 $iframe_html = $iframe_doc->saveHTML();
596 if ( $iframe_html !== false ) {
597 // Scan for links in the iframe
598 $iframe_urls = $this->get_urls_from_html( $iframe_html ); // Recursion
599 if ( !empty( $iframe_urls ) ) {
600 $results = array_merge( $results, $iframe_urls );
601 }
602 }
603 }
604 else {
605 $this->log( '🚫 Failed to load iframe: ' . $iframe_src );
606 }
607 }
608 }
609
610
611 // Images: src, srcset
612 $imgs = $dom->getElementsByTagName( 'img' );
613 foreach ( $imgs as $img ) {
614 //error_log($img->getAttribute('src'));
615 $src = $this->clean_url( $img->getAttribute('src') );
616 array_push( $results, $src );
617 $srcset = $img->getAttribute('srcset');
618 if ( !empty( $srcset ) ) {
619 $setImgs = explode( ',', trim( $srcset ) );
620 foreach ( $setImgs as $setImg ) {
621 $finalSetImg = explode( ' ', trim( $setImg ) );
622 if ( is_array( $finalSetImg ) ) {
623 array_push( $results, $this->clean_url( $finalSetImg[0] ) );
624 }
625 }
626 }
627 }
628
629 // Videos: src, poster, and attached file
630 $videos = $dom->getElementsByTagName( 'video' );
631 foreach ($videos as $video) {
632 // Get src attribute
633 $raw_video_src = $video->getAttribute( 'src' );
634 $src = $this->clean_url( $raw_video_src );
635 if ( !empty( $src ) ) {
636 $video_id = $this->custom_attachment_url_to_postid( $raw_video_src );
637
638 $attached_file = get_post_meta( $video_id, '_wp_attached_file', true );
639 if ( !empty( $attached_file ) ) {
640 array_push( $results, $attached_file );
641 }
642 }
643
644 // Get poster attribute
645 $raw_poster_src = $video->getAttribute( 'poster' );
646 $poster = $this->clean_url( $raw_poster_src );
647 if ( !empty( $poster ) ) {
648 $poster_id = $this->custom_attachment_url_to_postid( $raw_poster_src );
649
650 $attached_file = get_post_meta( $poster_id, '_wp_attached_file', true );
651 if ( !empty( $attached_file ) ) {
652 array_push( $results, $attached_file );
653 }
654 }
655
656 }
657
658 // Audios: src
659 $audios = $dom->getElementsByTagName( 'audio' );
660 foreach ( $audios as $audio ) {
661 //error_log($audio->getAttribute('src'));
662 $src = $this->clean_url( $audio->getAttribute('src') );
663 array_push( $results, $src );
664 }
665
666 // Sources: src
667 $audios = $dom->getElementsByTagName( 'source' );
668 foreach ( $audios as $audio ) {
669 //error_log($audio->getAttribute('src'));
670 $src = $this->clean_url( $audio->getAttribute('src') );
671 array_push( $results, $src );
672 }
673
674 // Links, href
675 $urls = $dom->getElementsByTagName( 'a' );
676 foreach ( $urls as $url ) {
677 $url_href = $url->getAttribute('href'); // mm change
678 if ( $this->is_url( $url_href ) ) { // mm change
679 $src = $this->clean_url( $url_href ); // mm change
680 if ( !empty( $src ) )
681 array_push( $results, $src );
682 }
683 }
684
685 // <link> tags in <head> area
686 $urls = $dom->getElementsByTagName( 'link' );
687 foreach ( $urls as $url ) {
688 $url_href = $url->getAttribute( 'href' );
689 if ( $this->is_url( $url_href ) ) {
690 $src = $this->clean_url( $url_href );
691 if ( !empty( $src ) ) {
692 array_push( $results, $src );
693 }
694 }
695 }
696
697 // PDF
698 preg_match_all( "/((https?:\/\/)?[^\\&\#\[\] \"\?]+\.pdf)/", $html, $res );
699 if ( !empty( $res ) && isset( $res[1] ) && count( $res[1] ) > 0 ) {
700 foreach ( $res[1] as $url ) {
701 if ( $this->is_url( $url ) )
702 array_push( $results, $this->clean_url( $url ) );
703 }
704 }
705
706 // Background images
707 preg_match_all( "/url\(\'?\"?((https?:\/\/)?[^\\&\#\[\] \"\?]+\.(jpe?g|gif|png))\'?\"?/", $html, $res );
708 if ( !empty( $res ) && isset( $res[1] ) && count( $res[1] ) > 0 ) {
709 foreach ( $res[1] as $url ) {
710 if ( $this->is_url( $url ) )
711 array_push( $results, $this->clean_url( $url ) );
712 }
713 }
714
715 return $results;
716 }
717
718 /**
719 *
720 * Get the IDs and URLs from the blocks of a post.
721 *
722 * @param string $html The HTML content of the post.
723 * @param string $prefix The prefix of the blocks to look for.
724 * @param array $keys The keys to look for in the blocks.
725 * @param array $urls The array to fill with the URLs.
726 * @param array $ids The array to fill with the IDs.
727 *
728 */
729 function get_from_blocks( $html, $prefix, $keys, &$urls, &$ids ) {
730
731 $blocks = parse_blocks( $html );
732
733 if ( ! is_array( $blocks ) || ! isset( $blocks[0] ) ) {
734 return;
735 }
736
737
738 foreach ( $blocks as $block ) {
739
740 if ( strpos( $block['blockName'], $prefix ) === false ) {
741 continue;
742 }
743
744 $this->array_to_ids_or_urls( $block, $ids, $urls, true, $keys );
745
746 }
747
748
749 }
750 // Parse a meta, visit all the arrays, look for the attributes, fill $ids and $urls arrays
751 // If rawMode is enabled, it will not check if the value is an ID or an URL, it will just returns it in URLs
752 function get_from_meta( $meta, $lookFor, &$ids, &$urls, $rawMode = false ) {
753 if ( !is_array( $meta ) && !is_object( $meta) ) {
754 return;
755 }
756 foreach ( $meta as $key => $value ) {
757 if ( is_object( $value ) || is_array( $value ) )
758 $this->get_from_meta( $value, $lookFor, $ids, $urls, $rawMode );
759 else if ( in_array( $key, $lookFor ) ) {
760 if ( empty( $value ) ) {
761 continue;
762 }
763 else if ( $rawMode ) {
764 array_push( $urls, $value );
765 }
766 else if ( is_numeric( $value ) ) {
767 // It this an ID?
768 array_push( $ids, $value );
769 }
770 else {
771 if ( $this->is_url( $value ) ) {
772 // Is this an URL?
773 array_push( $urls, $this->clean_url( $value ) );
774 }
775 else {
776 // Is this an array of IDs, encoded as a string? (like "20,13")
777 $pieces = explode( ',', $value );
778 foreach ( $pieces as $pval ) {
779 if ( is_numeric( $pval ) ) {
780 array_push( $ids, $pval );
781 }
782 }
783 }
784 }
785 }
786 }
787 }
788
789 function get_images_from_themes( &$ids, &$urls ) {
790 // USE CURRENT THEME AND WP API
791 $ch = get_custom_header();
792 if ( !empty( $ch ) && !empty( $ch->url ) ) {
793 array_push( $urls, $this->clean_url( $ch->url ) );
794 }
795 if ( $this->is_url( $ch->thumbnail_url ) ) {
796 array_push( $urls, $this->clean_url( $ch->thumbnail_url ) );
797 }
798 if ( !empty( $ch ) && !empty( $ch->attachment_id ) ) {
799 array_push( $ids, $ch->attachment_id );
800 }
801 $cl = get_custom_logo();
802 if ( $this->is_url( $cl ) ) {
803 $urls = array_merge( $this->get_urls_from_html( $cl ), $urls );
804 }
805 $custom_logo = get_theme_mod( 'custom_logo' );
806 if ( !empty( $custom_logo ) && is_numeric( $custom_logo ) ) {
807 array_push( $ids, (int)$custom_logo );
808 }
809 $si = get_site_icon_url();
810 if ( $this->is_url( $si ) ) {
811 array_push( $urls, $this->clean_url( $si ) );
812 }
813 $si_id = get_option( 'site_icon' );
814 if ( !empty( $si_id ) && is_numeric( $si_id ) ) {
815 array_push( $ids, (int)$si_id );
816 }
817 $cd = get_background_image();
818 if ( $this->is_url( $cd ) ) {
819 array_push( $urls, $this->clean_url( $cd ) );
820 }
821 $photography_hero_image = get_theme_mod( 'photography_hero_image' );
822 if ( !empty( $photography_hero_image ) ) {
823 array_push( $ids, $photography_hero_image );
824 }
825 $author_profile_picture = get_theme_mod( 'author_profile_picture' );
826 if ( !empty( $author_profile_picture ) ) {
827 array_push( $ids, $author_profile_picture );
828 }
829 if ( function_exists ( 'get_uploaded_header_images' ) ) {
830 $header_images = get_uploaded_header_images();
831 if ( !empty( $header_images ) ) {
832 foreach ( $header_images as $hi ) {
833 if ( !empty ( $hi['attachment_id'] ) ) {
834 array_push( $ids, $hi['attachment_id'] );
835 }
836 }
837 }
838 }
839 }
840
841 #region LOGS
842
843 function log( $data = null, $force = false ) {
844 if ( !$this->debug_logs && !$force )
845 return;
846
847 $php_logs = $this->get_option( 'php_error_logs' );
848 $log_file_path = $this->get_logs_path();
849
850 $fh = @fopen( $log_file_path, 'a' );
851 if ( !$fh ) { return false; }
852 $date = date( "Y-m-d H:i:s" );
853 if ( is_null( $data ) ) {
854 fwrite( $fh, "\n" );
855 }
856 else {
857 fwrite( $fh, "$date: {$data}\n" );
858 if ( $php_logs ) {
859 error_log( "[MEDIA CLEANER] " . $data );
860 }
861 }
862 fclose( $fh );
863 return true;
864 }
865
866 //WPMC_PREFIX
867
868 function get_logs_path() {
869 $uploads_dir = wp_upload_dir();
870 $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
871
872 $path = $this->get_option( 'logs_path' );
873
874 if ( $path && file_exists( $path ) ) {
875 // make sure the path is legal (within the uploads directory with the WPMC_PREFIX prefix and log extension)
876 if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, WPMC_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
877 $path = null;
878 } else {
879 return $path;
880 }
881 }
882
883 if ( !$path ) {
884 $path = $uploads_dir_path . WPMC_PREFIX . "_" . $this->random_ascii_chars() . ".log";
885 if ( !file_exists( $path ) ) {
886 touch( $path );
887 }
888
889 $options = $this->get_all_options();
890 $options['logs_path'] = $path;
891 $this->update_options( $options );
892 }
893
894 return $path;
895 }
896
897
898 function get_logs() {
899 $log_file_path = $this->get_logs_path();
900
901 if ( !file_exists( $log_file_path ) ) {
902 return "No logs found.";
903 }
904
905 $content = file_get_contents( $log_file_path );
906 $lines = explode( "\n", $content );
907 $lines = array_filter( $lines );
908 $lines = array_reverse( $lines );
909 $content = implode( "\n", $lines );
910 return $content;
911 }
912
913 function clear_logs() {
914 $logPath = $this->get_logs_path();
915 if ( file_exists( $logPath ) ) {
916 unlink( $logPath );
917 }
918
919 $options = $this->get_all_options();
920 $options['logs_path'] = null;
921 $this->update_options( $options );
922 }
923
924 #endregion
925
926 /**
927 *
928 * HELPERS
929 *
930 */
931
932 private function random_ascii_chars($length = 8)
933 {
934 $characters = array_merge(range('A', 'Z'), range('a', 'z'), range('0', '9'));
935 $characters_length = count($characters);
936 $random_string = '';
937
938 for ($i = 0; $i < $length; $i++) {
939 $random_string .= $characters[rand(0, $characters_length - 1)];
940 }
941
942 return $random_string;
943 }
944
945 function get_trashdir() {
946 return trailingslashit( $this->upload_path ) . 'wpmc-trash';
947 }
948
949 function get_trashurl() {
950 return trailingslashit( $this->upload_url ) . 'wpmc-trash';
951 }
952
953 function clean_ob(){
954 $disabled = $this->get_option( 'output_buffer_cleaning_disabled' );
955 $ob_content = ob_get_contents();
956 if ( !empty( trim( $ob_content ) ) ) {
957
958 if ( $disabled ) {
959 $this->log( "🚨 If the server's response was broken, try to let Output Buffer Cleaning enabled." );
960 return;
961 }
962
963 $this->log( "🧹 The response is broken due to output buffering, it will be cleaned." );
964 $this->log( "📄 Output buffer content: " . $ob_content );
965
966 ob_end_clean();
967 }
968 }
969
970 /**
971 *
972 * I18N RELATED HELPERS
973 *
974 */
975
976 function is_multilingual() {
977 return function_exists( 'icl_get_languages' );
978 }
979
980 function get_languages() {
981 $results = array();
982 if ( $this->is_multilingual() ) {
983 $languages = icl_get_languages();
984 foreach ( $languages as $language ) {
985 if ( isset( $language['code'] ) ) {
986 array_push( $results, $language['code'] );
987 }
988 else if ( isset( $language['language_code'] ) ) {
989 array_push( $results, $language['language_code'] );
990 }
991 }
992 }
993 return $results;
994 }
995
996 function get_translated_media_ids( $mediaId ) {
997 $translated_ids = array();
998 foreach ( $this->languages as $language ) {
999 $id = apply_filters( 'wpml_object_id', $mediaId, 'attachment', false, $language );
1000 if ( !empty( $id ) ) {
1001 array_push( $translated_ids, $id );
1002 }
1003 }
1004 return $translated_ids;
1005 }
1006
1007 /**
1008 *
1009 * DELETE / SCANNING / RESET
1010 *
1011 */
1012
1013 function recover_file( $path ) {
1014 $originalPath = trailingslashit( $this->upload_path ) . $path;
1015 $trashPath = trailingslashit( $this->get_trashdir() ) . $path;
1016 if ( !file_exists( $trashPath ) ) {
1017 $this->log( "🚫 The file $originalPath actually does not exist in the trash." );
1018 return true;
1019 }
1020 $path_parts = pathinfo( $originalPath );
1021 if ( !file_exists( $path_parts['dirname'] ) && !wp_mkdir_p( $path_parts['dirname'] ) ) {
1022 die( 'Failed to create folder.' );
1023 }
1024 if ( !rename( $trashPath, $originalPath ) ) {
1025 die( 'Failed to move the file.' );
1026 }
1027 return true;
1028 }
1029
1030 function recover( $id ) {
1031 global $wpdb;
1032 $table_name = $wpdb->prefix . "mclean_scan";
1033 $issue = $this->get_issue( $id );
1034
1035 if ( empty( $issue ) ) {
1036 $this->log( "🚫 Issue #{$id} does not exist. Cannot recover this." );
1037 return false;
1038 }
1039
1040 // Files
1041 if ( $issue->type === 0 ) {
1042 $this->recover_file( $issue->path );
1043 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET deleted = 0 WHERE id = %d", $id ) );
1044 $this->log( "�
1045 Recovered {$issue->path}." );
1046 return true;
1047 }
1048 // Media
1049 else if ( $issue->type === 1 ) {
1050
1051 // If there is no file attached, doesn't handle the files
1052 $fullpath = get_attached_file( $issue->postId );
1053 if ( empty( $fullpath ) ) {
1054 $this->log( "🚫 Media #{$issue->postId} does not have attached file anymore." );
1055 error_log( "Media #{$issue->postId} does not have attached file anymore." );
1056 return false;
1057 }
1058
1059 $paths = $this->get_paths_from_attachment( $issue->postId );
1060 foreach ( $paths as $path ) {
1061 if ( !$this->recover_file( $path ) ) {
1062 $this->log( "🚫 Could not recover $path." );
1063 error_log( "Media Cleaner: Could not recover $path." );
1064 }
1065 }
1066 if ( !wp_update_post( array( 'ID' => $issue->postId, 'post_type' => 'attachment' ) ) ) {
1067 $this->log( "🚫 Failed to Untrash Post {$issue->postId} (but deleted it from Cleaner DB)." );
1068 error_log( "Media Cleaner: Failed to Untrash Post {$issue->postId} (but deleted it from Cleaner DB)." );
1069 return false;
1070 }
1071 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET deleted = 0 WHERE id = %d", $id ) );
1072 $this->log( "�
1073 Recovered Media #{$issue->postId}." );
1074 return true;
1075 }
1076 }
1077
1078 function trash_file( $fileIssuePath ) {
1079 $originalPath = trailingslashit( $this->upload_path ) . $fileIssuePath;
1080 $trashPath = trailingslashit( $this->get_trashdir() ) . $fileIssuePath;
1081 $path_parts = pathinfo( $trashPath );
1082
1083 try {
1084 if ( !file_exists( $path_parts['dirname'] ) && !wp_mkdir_p( $path_parts['dirname'] ) ) {
1085 $this->log( "🚫 Could not create the trash directory for Media Cleaner." );
1086 error_log( "Media Cleaner: Could not create the trash directory." );
1087 return false;
1088 }
1089 // Rename the file (move). 'is_dir' is just there for security (no way we should move a whole directory)
1090 if ( is_dir( $originalPath ) ) {
1091 $this->log( "🚫 Attempted to delete a directory instead of a file ($originalPath). Can't do that." );
1092 error_log( "Media Cleaner: Attempted to delete a directory instead of a file ($originalPath). Can't do that." );
1093 return false;
1094 }
1095 if ( !file_exists( $originalPath ) ) {
1096 $this->log( "🚫 The file $originalPath actually does not exist." );
1097 error_log( "Media Cleaner: The file $originalPath actually does not exist." );
1098 return true;
1099 }
1100 if ( !@rename( $originalPath, $trashPath ) ) {
1101 error_log( "Media Cleaner: Unknown error occured while trying to delete a file ($originalPath)." );
1102 return false;
1103 }
1104 }
1105 catch ( Exception $e ) {
1106 return false;
1107 }
1108 $this->clean_dir( dirname( $originalPath ) );
1109 return true;
1110 }
1111
1112 function repair( $id ) {
1113 $repair = $this->get_repair( $id );
1114 if ( empty( $repair ) ) {
1115 $this->log( "🚫 Repair #{$id} does not exist. Cannot repair this." );
1116 return false;
1117 }
1118 foreach ( $repair->child_ids as $child_id ) {
1119 if ( !$this->delete( $child_id ) ) {
1120 $this->log( "🚫 Failed to repair the file." );
1121 return false;
1122 }
1123 }
1124 $full_path = $this->get_full_upload_path( $repair->path );
1125 $filetype = wp_check_filetype( basename( $full_path ), null );
1126 $wp_upload_dir = wp_upload_dir();
1127 $attachment = array(
1128 'guid' => $wp_upload_dir['url'] . '/' . basename( $full_path ),
1129 'post_mime_type' => $filetype['type'],
1130 'post_title' => preg_replace( '/\.[^.]+$/', '', basename( $full_path ) ),
1131 'post_content' => '',
1132 'post_status' => 'inherit'
1133 );
1134
1135 $attach_id = wp_insert_attachment( $attachment, $full_path );
1136
1137 require_once( ABSPATH . 'wp-admin/includes/image.php' );
1138 $attach_data = wp_generate_attachment_metadata( $attach_id, $full_path );
1139 wp_update_attachment_metadata( $attach_id, $attach_data );
1140
1141 global $wpdb;
1142 $table_name = $wpdb->prefix . "mclean_scan";
1143 $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE id = %d OR parentId = %d", $id, $id ) );
1144 $this->log( "�
1145 Repaired {$repair->path}." );
1146 return true;
1147 }
1148
1149 function ignore( $id, $ignore ) {
1150 global $wpdb;
1151 $table_name = $wpdb->prefix . "mclean_scan";
1152 $issue = $this->get_issue( $id );
1153
1154 if ( empty( $issue ) ) {
1155 $this->log( "🚫 Issue #{$id} does not exist. Cannot ignore this." );
1156 return false;
1157 }
1158
1159 if ( !$ignore ) {
1160 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET ignored = 0 WHERE id = %d", $id ) );
1161 }
1162 else {
1163 // If it is in trash, recover it
1164 if ( $issue->deleted ) {
1165 $this->recover( $id );
1166 }
1167 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET ignored = 1 WHERE id = %d", $id ) );
1168 }
1169 return true;
1170 }
1171
1172 function endsWith( $haystack, $needle )
1173 {
1174 $length = strlen( $needle );
1175 if ( $length == 0 )
1176 return true;
1177 return ( substr( $haystack, -$length ) === $needle );
1178 }
1179
1180 function clean_dir( $dir ) {
1181 if ( !file_exists( $dir ) )
1182 return;
1183 else if ( $this->endsWith( $dir, 'uploads' ) )
1184 return;
1185 $found = array_diff( scandir( $dir ), array( '.', '..' ) );
1186 if ( count( $found ) < 1 ) {
1187 if ( rmdir( $dir ) ) {
1188 $this->clean_dir( dirname( $dir ) );
1189 }
1190 }
1191 }
1192
1193 function get_issue( $id ) {
1194 global $wpdb;
1195 $table_name = $wpdb->prefix . "mclean_scan";
1196 $issue = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id ), OBJECT );
1197 if ( empty( $issue ) ) {
1198 return false;
1199 }
1200 $issue->id = (int)$issue->id;
1201 $issue->postId = (int)$issue->postId;
1202 $issue->type = (int)$issue->type;
1203 $issue->deleted = (int)$issue->deleted;
1204 $issue->ignored = (int)$issue->ignored;
1205 $issue->path = stripslashes( $issue->path );
1206 return $issue;
1207 }
1208
1209 function get_repair( $id ) {
1210 global $wpdb;
1211 $table_name = $wpdb->prefix . "mclean_scan";
1212 $repair = $wpdb->get_row( $wpdb->prepare( "SELECT
1213 main.id AS id,
1214 main.path AS path,
1215 GROUP_CONCAT(child.id) AS child_ids
1216 FROM
1217 $table_name AS main
1218 LEFT JOIN
1219 $table_name AS child ON main.id = child.parentId
1220 WHERE main.id = %d", $id
1221 ), OBJECT );
1222 if ( empty( $repair ) ) {
1223 return false;
1224 }
1225
1226 // If $repair->path is null or empty return false
1227 if ( empty( $repair->path ) ) {
1228 $this->log( "🚫 Repair #{$id} does not have a path. Cannot repair this." );
1229 return false;
1230 }
1231
1232
1233 $repair->id = (int)$repair->id;
1234 $regex = "^(.*)(\\s\\(\\+.*)$";
1235 $repair->path = preg_replace( '/' . $regex . '/i', '$1', stripslashes( $repair->path ) );
1236 $repair->child_ids = $repair->child_ids ? explode( ',', $repair->child_ids ) : [];
1237 return $repair;
1238 }
1239
1240 function get_issues_to_repair( $order_by = 'id', $order = 'asc', $search = '', $skip = 0, $limit = 10 ) {
1241 global $wpdb;
1242 $table_name = $wpdb->prefix . "mclean_scan";
1243
1244 $search_clause = '';
1245 if ( !empty( $search ) ) {
1246 $search_clause = $wpdb->prepare("AND main.path LIKE %s", ( '%' . $search . '%' ));
1247 }
1248
1249 $order_clause = 'ORDER BY main.id ASC';
1250 if ( $order_by === 'path' ) {
1251 $order_clause = 'ORDER BY main.path ' . ( $order === 'asc' ? 'ASC' : 'DESC' );
1252 }
1253 else if ( $order_by === 'issue' ) {
1254 $order_clause = 'ORDER BY main.issue ' . ( $order === 'asc' ? 'ASC' : 'DESC' );
1255 }
1256 else if ( $order_by === 'size' ) {
1257 $order_clause = 'ORDER BY main.size ' . ( $order === 'asc' ? 'ASC' : 'DESC' );
1258 }
1259
1260 $result = $wpdb->get_results( $wpdb->prepare( "SELECT
1261 main.id AS id,
1262 main.path AS path,
1263 GROUP_CONCAT(child.id) AS child_ids,
1264 GROUP_CONCAT(child.path) AS child_paths,
1265 main.type AS type,
1266 main.postId AS postId,
1267 main.size AS size,
1268 main.ignored AS ignored,
1269 main.deleted AS deleted,
1270 main.issue AS issue
1271 FROM
1272 $table_name AS main
1273 LEFT JOIN
1274 $table_name AS child ON main.id = child.parentId
1275 WHERE
1276 main.path IS NOT NULL AND main.parentId IS NULL
1277 AND main.deleted = 0 AND main.ignored = 0
1278 AND main.type = 0
1279 $search_clause
1280 GROUP BY main.id
1281 $order_clause
1282 LIMIT %d, %d;
1283 ", $skip, $limit ) );
1284
1285 return $result;
1286 }
1287
1288 function get_repair_ids ( $search = '' ) {
1289 global $wpdb;
1290 $table_name = $wpdb->prefix . "mclean_scan";
1291
1292 $search_clause = '';
1293 if ( !empty( $search ) ) {
1294 $search_clause = $wpdb->prepare("AND main.path LIKE %s", ( '%' . $search . '%' ));
1295 }
1296
1297 return $wpdb->get_col( "SELECT DISTINCT main.id
1298 FROM
1299 $table_name AS main
1300 LEFT JOIN $table_name AS child ON main.id = child.parentId
1301 WHERE
1302 main.path IS NOT NULL
1303 AND main.parentId IS NULL
1304 $search_clause
1305 GROUP BY
1306 main.id
1307 ;"
1308 );
1309 }
1310
1311 function get_stats_of_issues_to_repair( $search = '' ) {
1312 global $wpdb;
1313 $table_name = $wpdb->prefix . "mclean_scan";
1314
1315 $search_clause = '';
1316 if ( !empty( $search ) ) {
1317 $search_clause = $wpdb->prepare("AND main.path LIKE %s", ( '%' . $search . '%' ));
1318 }
1319
1320 return $wpdb->get_row( "SELECT
1321 COUNT(id) AS entries,
1322 SUM(size) AS size
1323 FROM (
1324 SELECT
1325 COUNT(DISTINCT main.id) as id,
1326 main.size as size
1327 FROM
1328 $table_name AS main
1329 LEFT JOIN
1330 $table_name AS child ON main.id = child.parentId
1331 WHERE
1332 main.path IS NOT NULL AND main.parentId IS NULL AND main.deleted = 0 AND main.ignored = 0
1333 $search_clause
1334 GROUP BY main.id
1335 ) t;
1336 " );
1337 }
1338
1339 function get_count_of_issues_to_repair( $search ) {
1340 $stats = $this->get_stats_of_issues_to_repair( $search );
1341 return $stats->entries;
1342 }
1343
1344 function delete( $id ) {
1345 global $wpdb;
1346 $table_name = $wpdb->prefix . "mclean_scan";
1347 $issue = $this->get_issue( $id );
1348
1349 if ( empty( $issue ) ) {
1350 $this->log( "🚫 Issue #{$id} does not exist. Cannot delete this." );
1351 return false;
1352 }
1353
1354 $regex = "^(.*)(\\s\\(\\+.*)$";
1355 $issue->path = preg_replace( '/' . $regex . '/i', '$1', $issue->path ); // remove " (+ 6 files)" from path
1356 $skip_trash = $this->get_option( 'skip_trash' );
1357
1358 if ( $issue->type === 0 ) {
1359
1360 // Delete file from the trash
1361 if ( $issue->deleted === 1 ) {
1362 $trashPath = trailingslashit( $this->get_trashdir() ) . $issue->path;
1363 if ( unlink( $trashPath ) ) {
1364 $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE id = %d", $id ) );
1365 $this->clean_dir( dirname( $trashPath ) );
1366 return true;
1367 }
1368 }
1369 // Delete file without using trash
1370 else if ( $skip_trash ) {
1371 $originalPath = trailingslashit( $this->upload_path ) . $issue->path;
1372 if ( unlink( $originalPath ) ) {
1373 $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE id = %d", $id ) );
1374 $this->clean_dir( dirname( $originalPath ) );
1375 return true;
1376 }
1377 }
1378 // Move file to the trash
1379 else if ( $this->trash_file( $issue->path ) ) {
1380 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET deleted = 1, ignored = 0, time = NOW() WHERE id = %d", $id ) );
1381 return true;
1382 }
1383
1384 $this->log( "🚫 Failed to delete/trash the file." );
1385 error_log( "Media Cleaner: Failed to delete/trash the file." );
1386 }
1387
1388 if ( $issue->type === 1 ) {
1389
1390 // Trash Media definitely by recovering it (to be like a normal Media) and remove it through the
1391 // standard WordPress workflow
1392 if ( $issue->deleted === 1 || $skip_trash ) {
1393 if ( $issue->deleted === 1 ) {
1394 $this->recover( $id );
1395 }
1396 wp_update_post( array( 'ID' => $issue->postId, 'post_type' => 'attachment' ) );
1397 wp_delete_attachment( $issue->postId, true );
1398 $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE id = %d", $id ) );
1399 return true;
1400 }
1401 else {
1402 // Move Media to trash
1403 // Let's copy the images to the trash so that it can be recovered.
1404 $paths = $this->get_paths_from_attachment( $issue->postId );
1405 foreach ( $paths as $path ) {
1406 if ( !$this->trash_file( $path ) ) {
1407 $this->log( "🚫 Could not trash $path." );
1408 error_log( "Media Cleaner: Could not trash $path." );
1409 return false;
1410 }
1411 }
1412 wp_update_post( array( 'ID' => $issue->postId, 'post_type' => 'wmpc-trash' ) );
1413 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET deleted = 1, ignored = 0, time = NOW() WHERE id = %d", $id ) );
1414 return true;
1415 }
1416 }
1417 return false;
1418 }
1419
1420 function delete_directory_recurcively( $dir ) {
1421 if ( !is_dir( $dir ) ) {
1422 return;
1423 }
1424 $files = array_diff( scandir( $dir ), array( '.', '..' ) );
1425 foreach ( $files as $file ) {
1426 if ( is_dir( "$dir/$file" ) ) {
1427 $this->delete_directory_recurcively( "$dir/$file" );
1428 }
1429 else {
1430 unlink( "$dir/$file" );
1431 }
1432 }
1433 rmdir( $dir );
1434 }
1435
1436 function force_trash() {
1437
1438 $res = [
1439 'message' => 'The trash folder has been emptied.',
1440 'success' => true
1441 ];
1442
1443 // Delete all the files in the trash folder.
1444 $trashDirPath = trailingslashit( $this->get_trashdir() );
1445 if ( file_exists( $trashDirPath ) && is_dir( $trashDirPath ) ) {
1446 $this->delete_directory_recurcively( $trashDirPath, true );
1447 }
1448
1449 // Clean the Database: DELETE FROM wp_mclean_scan WHERE deleted = 1
1450 global $wpdb;
1451 $table_name = $wpdb->prefix . "mclean_scan";
1452 $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE deleted = 1" ) );
1453
1454
1455 return $res;
1456 }
1457
1458 /**
1459 *
1460 * SCANNING / RESET
1461 *
1462 */
1463
1464 function add_reference_url( $urlOrUrls, $type, $origin = null, $extra = null ) {
1465 $urlOrUrls = !is_array( $urlOrUrls ) ? array( $urlOrUrls ) : $urlOrUrls;
1466 foreach ( $urlOrUrls as $url ) {
1467 // With files, we need both filename without resolution and filename with resolution, it's important
1468 // to make sure the original file is not deleted if a size exists for it.
1469 // With media, all URLs should be without resolution to make sure it matches Media.
1470 $no_res_url = $this->clean_url_from_resolution( $url );
1471
1472 $this->add_reference( null, $url, $type, $origin, $extra );
1473 $this->add_reference( 0, $no_res_url, $type, $origin, $extra );
1474
1475 if ( $this->multilingual ) {
1476 if ( $this->current_method == 'media' ) {
1477 $id = $this->get_id_from_clean_url( $no_res_url, false );
1478 if( $id ) $this->add_reference_id( $id, $type, $origin, $extra );
1479 }
1480 }
1481 }
1482 }
1483
1484 /**
1485 * Add an issue to the mclean_scan table.
1486 *
1487 * @param string $path The path to the file (relative to uploads).
1488 * @param string $issue The issue code/type.
1489 * @param int|null $postId Optional post ID related to the issue.
1490 */
1491 function add_issue( $path, $issue, $postId = null ) {
1492 global $wpdb;
1493 $table_name = $wpdb->prefix . "mclean_scan";
1494 $clean_path = $this->clean_uploaded_filename( $path );
1495 $filepath = trailingslashit( $this->upload_path ) . stripslashes( $path );
1496 $filesize = file_exists( $filepath ) ? filesize( $filepath ) : 0;
1497
1498 // Check if this issue already exists
1499 $existing = $wpdb->get_var( $wpdb->prepare(
1500 "SELECT id FROM $table_name WHERE path = %s AND issue = %s",
1501 $clean_path, $issue
1502 ) );
1503
1504 if ( $existing ) {
1505 return; // Issue already exists
1506 }
1507
1508 // Find potential parent
1509 $potentialParentPath = $this->clean_url_from_resolution( $clean_path );
1510 $parentId = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table_name WHERE path = %s", $potentialParentPath ) );
1511 $parentId = $parentId ? (int)$parentId : null;
1512
1513 $wpdb->insert( $table_name,
1514 array(
1515 'time' => current_time('mysql'),
1516 'type' => 0,
1517 'postId' => $postId,
1518 'path' => $clean_path,
1519 'size' => $filesize,
1520 'issue' => $issue,
1521 'parentId' => $parentId
1522 )
1523 );
1524 }
1525
1526 function add_reference_id( $idOrIds, $type, $origin = null, $extra = null ) {
1527 $idOrIds = !is_array( $idOrIds ) ? array( $idOrIds ) : $idOrIds;
1528 foreach ( $idOrIds as $id ) {
1529 $this->add_reference( $id, "", $type, $origin );
1530 if ( $this->multilingual ) {
1531 $translatedIds = $this->get_translated_media_ids( (int)$id );
1532
1533 // Test for WPML
1534 // if ( $id === '350') {
1535 // $translatedIds = $this->get_translated_media_ids( (int)$id );
1536 // $count = count($translatedIds);
1537 // error_log( "${id} => ${count}" );
1538 // }
1539
1540 if ( !empty( $translatedIds ) ) {
1541 foreach ( $translatedIds as $translatedId ) {
1542 $this->add_reference( $translatedId, "", $type, $origin );
1543 }
1544 }
1545 }
1546 }
1547 }
1548
1549
1550 // Returns the reference with the type, origin, related to a Media ID it is referenced
1551 public function get_reference_for_media_id( $id ) {
1552 global $wpdb;
1553 $table_name = $wpdb->prefix . "mclean_refs";
1554 $refs = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_name WHERE mediaId = %d", $id ), OBJECT );
1555 if ( empty( $refs ) ) {
1556 return false;
1557 }
1558 $ref = $refs[0];
1559 $ref->id = (int)$ref->id;
1560 $ref->mediaId = (int)$ref->mediaId;
1561 $ref->originType = (int)$ref->originType;
1562 $ref->origin = stripslashes( $ref->origin );
1563 $ref->parentId = empty( $ref->parentId ) ? null : (int)$ref->parentId;
1564 return $ref;
1565 }
1566
1567 // Return the references related to a Post ID
1568 public function get_references_for_post_id( $id ) {
1569 global $wpdb;
1570 $table_name = $wpdb->prefix . "mclean_refs";
1571 $refs = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_name WHERE originType LIKE %s", "%[$id]" ), OBJECT );
1572 if ( empty( $refs ) ) {
1573 return [];
1574 }
1575 $fresh_refs = array();
1576 foreach ( $refs as $ref ) {
1577 $mediaId = (int)$ref->mediaId > 0 ? (int)$ref->mediaId : null;
1578 if ( !$mediaId && !empty( $ref->mediaUrl ) ) {
1579 $mediaId = $this->find_media_id_from_file( $ref->mediaUrl, false );
1580 $mediaId = !empty( $mediaId ) ? (int)$mediaId : null;
1581 }
1582 if ( !$mediaId ) {
1583 continue;
1584 }
1585 array_push( $fresh_refs, [
1586 'id' => (int)$ref->id,
1587 'mediaId' => $mediaId,
1588 'mediaUrl' => $ref->mediaUrl,
1589 'originType' => $ref->originType,
1590 'origin' => $ref->origin,
1591 'parentId' => empty( $ref->parentId ) ? null : (int)$ref->parentId,
1592 ] );
1593 }
1594 return $fresh_refs;
1595 }
1596
1597 // The references are actually not being added directly in the DB, they are being pushed
1598 // into a cache ($this->refcache), then written to the database via write_references().
1599 private function add_reference( $id, $url, $type, $origin = null, $extra = null ) {
1600 if ( !empty( $id ) ) {
1601 array_push( $this->refcache, array( 'id' => $id, 'url' => null, 'type' => $type, 'origin' => $origin ) );
1602 }
1603 if ( !empty( $url ) ) {
1604 // The URL shouldn't contain http, https, javascript at the beginning (and there are probably many more cases)
1605 // The URL must be cleaned before being passed as a reference.
1606 if ( substr( $url, 0, 5 ) === "http:" || substr( $url, 0, 6 ) === "https:" || substr( $url, 0, 11 ) === "javascript:" ) {
1607 return;
1608 }
1609 array_push( $this->refcache, array( 'id' => null, 'url' => $url, 'type' => $type, 'origin' => $origin ) );
1610 }
1611 }
1612
1613 function insert_references($entries)
1614 {
1615 global $wpdb;
1616 $table = $wpdb->prefix . "mclean_refs";
1617
1618 $refs_buffer = $this->get_option( 'refs_buffer' );
1619 if ( empty( $refs_buffer ) || $refs_buffer < 1 ) {
1620 $refs_buffer = 500;
1621 }
1622
1623 $values = array();
1624 $place_holders = array();
1625 $entry_count = 0;
1626
1627 foreach ( $entries as $value ) {
1628 $origin = isset( $value['origin'] ) ? $value['origin'] : null;
1629 if ( !is_null($value['id'] ) ) {
1630 // Media Reference
1631 $hash = md5( $value['id'] . '|' . $value['type'] . '|' . $origin );
1632 array_push( $values, $value['id'], $value['type'], $origin, $hash );
1633 $place_holders[] = "('%d', NULL, '%s', '%s', NULL, '%s')";
1634
1635 if ($this->debug_logs) {
1636 $this->log("+ Media #{$value['id']} (as ID)");
1637 }
1638 $entry_count++;
1639 }
1640 else if ( !is_null($value['url'] ) ) {
1641 // File Reference
1642 $parentId = isset( $value['parentId'] ) ? $value['parentId'] : null;
1643 $hash = md5( '|' . $value['url'] . '|' . $value['type'] . '|' . $origin . '|' . $parentId );
1644 array_push( $values, $value['url'], $value['type'], $origin, $hash );
1645 if ( isset( $value['parentId'] ) ) {
1646 array_push( $values, $value['parentId'] );
1647 $place_holders[] = "(NULL, '%s', '%s', '%s', '%d', '%s')";
1648 if ( $this->debug_logs ) {
1649 $this->log( "{$value['url']} (as URL) (ParentID: {$value['parentId']})" );
1650 }
1651 } else {
1652 $place_holders[] = "(NULL, '%s', '%s', '%s', NULL, '%s')";
1653 if ( $this->debug_logs ) {
1654 $this->log("{$value['url']} (as URL)");
1655 }
1656 }
1657 $entry_count++;
1658 }
1659
1660 // Flush to DB when buffer is full
1661 if ( $entry_count >= $refs_buffer ) {
1662 $this->log( "Flushing $entry_count references to the database..." );
1663 $this->flush_references_to_db( $table, $values, $place_holders );
1664 $values = array();
1665 $place_holders = array();
1666 $entry_count = 0;
1667 }
1668 }
1669
1670 // Flush remaining entries
1671 if ( !empty( $values ) ) {
1672 $this->log( "Flushing remaining $entry_count references to the database..." );
1673 $this->flush_references_to_db( $table, $values, $place_holders );
1674 }
1675 }
1676
1677 function flush_references_to_db( $table, $values, $place_holders ) {
1678 global $wpdb;
1679 if ( empty( $values ) ) {
1680 return;
1681 }
1682 $query = "INSERT IGNORE INTO $table (mediaId, mediaUrl, originType, origin, parentId, ref_hash) VALUES ";
1683 $query .= implode( ', ', $place_holders );
1684 $prepared = $wpdb->prepare( "$query ", $values );
1685 $wpdb->query( $prepared );
1686 }
1687
1688 function reset_progress() {
1689 // Reset the progress by deleting the transient.
1690 delete_transient( $this->progress_key );
1691 }
1692
1693 function clear_step_progress() {
1694 // Clear step progress when scanning completes
1695 delete_transient( $this->progress_key );
1696 }
1697
1698 function save_progress( $step, $data = array() ) {
1699 // Save progress with step and optional data
1700 // Data can include type, limit, limitSize, and any other progress information
1701 $progress = array(
1702 'step' => $step,
1703 'time' => time(),
1704 'data' => $data
1705 );
1706
1707 set_transient( $this->progress_key, $progress, 0 );
1708 }
1709
1710 function get_progress() {
1711 return get_transient( $this->progress_key );
1712 }
1713
1714 function get_step_progress() {
1715 $options = $this->get_all_options();
1716 return isset( $options['step_progress'] ) ? $options['step_progress'] : null;
1717 }
1718
1719 // The cache containing the references is wrote to the DB.
1720 function write_references() {
1721 global $wpdb;
1722 $table = $wpdb->prefix . "mclean_refs";
1723
1724 $potential_parents = array();
1725 $potential_children = array();
1726
1727 foreach ( $this->refcache as $value ) {
1728 $potentialParentPath = !is_null( $value['url'] ) ? $this->clean_url_from_resolution( $value['url'] ) : null;
1729 if ( $potentialParentPath === $value['url'] ) {
1730 $potential_parents[] = $value;
1731 }
1732 else {
1733 $potential_children[] = $value;
1734 }
1735 }
1736
1737 $this->insert_references( $potential_parents );
1738
1739 // Resolve parentId for potential children
1740 foreach ( $potential_children as &$child ) {
1741 $potentialParentPath = $this->clean_url_from_resolution( $child['url'] );
1742 $parentId = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table WHERE mediaUrl = %s", $potentialParentPath ) );
1743 if ( !empty( $parentId ) ) {
1744 $child['parentId'] = (int)$parentId;
1745 }
1746 }
1747
1748 // Insert potential children with resolved parentIds
1749 $this->insert_references( $potential_children );
1750 $this->refcache = array();
1751 }
1752
1753 function check_is_ignore( $file ) {
1754 global $wpdb;
1755 $table_name = $wpdb->prefix . "mclean_scan";
1756 $count = $wpdb->get_var( "SELECT COUNT(*)
1757 FROM $table_name
1758 WHERE ignored = 1
1759 AND path LIKE '%". esc_sql( $wpdb->esc_like( $file ) ) . "%'" );
1760 if ( $count > 0 ) {
1761 $this->log( "🚫 Could not trash $file." );
1762 }
1763 return ($count > 0);
1764 }
1765
1766 function find_media_id_from_file( $file, $doLog ) {
1767 global $wpdb;
1768 $postmeta_table_name = $wpdb->prefix . 'postmeta';
1769 $file = $this->clean_uploaded_filename( $file );
1770 $sql = $wpdb->prepare( "SELECT post_id
1771 FROM {$postmeta_table_name}
1772 WHERE meta_key = '_wp_attached_file'
1773 AND meta_value = %s", $file
1774 );
1775 $ret = $wpdb->get_var( $sql );
1776 if ( $doLog ) {
1777 if ( empty( $ret ) )
1778 $this->log( "🚫 File $file not found as _wp_attached_file (Library)." );
1779 else {
1780 $this->log( "�
1781 File $file found as Media $ret." );
1782 }
1783 }
1784
1785 return $ret;
1786 }
1787
1788 function get_thumbnails_urls( $id, $sizes_as_key = false ) {
1789 $sizes = get_intermediate_image_sizes();
1790 // For each size use wp_get_attachment_image_src() to get the URL
1791 $urls = array();
1792 foreach ( $sizes as $size ) {
1793 $src = wp_get_attachment_image_src( $id, $size );
1794 if ( $src ) {
1795 $urls[$size] = $this->clean_url( $src[0] );
1796 }
1797 }
1798
1799 return $sizes_as_key ? $urls : array_values( $urls );
1800 }
1801
1802 function get_thumbnails_urls_from_srcset( $media, $size = 'full' ) {
1803
1804 $id = is_numeric( $media ) ? (int)$media : $this->get_id_from_clean_url( $media, false );
1805
1806 $image_size = $this->get_attachment_size_by_id( $id, $size );
1807
1808 $sizes = array_keys( $this->get_image_sizes() );
1809 $sizes[] = $image_size;
1810
1811 $urls = array();
1812 foreach ( $sizes as $image_size ) {
1813 $srcset = wp_get_attachment_image_srcset( $id, $image_size );
1814
1815 // Extract URLs from srcset
1816 if ( !empty( $srcset ) ) {
1817 $srcset = explode( ', ', $srcset );
1818 foreach ( $srcset as $src ) {
1819 $parts = explode( ' ', $src );
1820 $url = trim( $parts[0] );
1821 if ( !empty( $url ) ) {
1822 $urls[] = $this->clean_url( $url );
1823 }
1824 }
1825 }
1826 }
1827
1828 return $urls;
1829
1830 }
1831
1832 function get_attachment_size_by_id( $attachment_id, $default_size = 'full' ) {
1833
1834 if ( ! $attachment_id ) {
1835 return $default_size;
1836 }
1837
1838 $url = wp_get_attachment_url( $attachment_id );
1839 if ( ! $url ) {
1840 return $default_size;
1841 }
1842
1843 $metadata = wp_get_attachment_metadata( $attachment_id );
1844
1845 if ( ! is_array( $metadata ) ) {
1846 return $default_size;
1847 }
1848
1849 $size = $default_size;
1850
1851 if ( isset( $metadata['file'] ) && strpos( $url, $metadata['file'] ) === ( strlen( $url ) - strlen( $metadata['file'] ) ) ) {
1852 $size = array( $metadata['width'], $metadata['height'] );
1853 } elseif ( preg_match( '/-(\d+)x(\d+)\.(jpg|jpeg|gif|png|svg|webp)$/', $url, $match ) ) {
1854 // Get the image width and height.
1855 // Example: https://regex101.com/r/7JwGz7/1.
1856 $size = array( $match[1], $match[2] );
1857 }
1858
1859 return $size;
1860 }
1861
1862 function get_image_sizes() {
1863 $sizes = array();
1864 global $_wp_additional_image_sizes;
1865 foreach ( get_intermediate_image_sizes() as $s ) {
1866 $crop = false;
1867 if ( isset( $_wp_additional_image_sizes[$s] ) ) {
1868 $width = intval( $_wp_additional_image_sizes[$s]['width'] );
1869 $height = intval( $_wp_additional_image_sizes[$s]['height'] );
1870 $crop = $_wp_additional_image_sizes[$s]['crop'];
1871 } else {
1872 $width = get_option( $s.'_size_w' );
1873 $height = get_option( $s.'_size_h' );
1874 $crop = get_option( $s.'_crop' );
1875 }
1876 $sizes[$s] = array( 'width' => $width, 'height' => $height, 'crop' => $crop );
1877 }
1878 return $sizes;
1879 }
1880
1881 /**
1882 * Get all registered thumbnail sizes formatted for the UI.
1883 * Returns an array of sizes with name, shortname, width, and height.
1884 */
1885 function get_thumbnail_sizes() {
1886 $sizes = $this->get_image_sizes();
1887 $result = array();
1888 foreach ( $sizes as $name => $size ) {
1889 // Generate a shortname (first 2 letters uppercase)
1890 $shortname = strtoupper( substr( preg_replace( '/[^a-zA-Z]/', '', $name ), 0, 2 ) );
1891 $result[] = array(
1892 'name' => $name,
1893 'shortname' => $shortname,
1894 'width' => $size['width'] ? intval( $size['width'] ) : null,
1895 'height' => $size['height'] ? intval( $size['height'] ) : null,
1896 'crop' => $size['crop'],
1897 );
1898 }
1899 return $result;
1900 }
1901
1902 function clean_url_from_resolution( $url ) {
1903 if ( !isset( $url ) ) return $url;
1904
1905 $pattern = '/[_-]\d+x\d+(?=\.[a-z]{3,4}$)/';
1906 $url = preg_replace( $pattern, '', $url );
1907 return $url;
1908 }
1909
1910 function is_url( $url ) {
1911 return ( (
1912 !empty( $url ) ) &&
1913 is_string( $url ) &&
1914 strlen( $url ) > 4 && (
1915 strtolower( substr( $url, 0, 4) ) == 'http' || $url[0] == '/'
1916 )
1917 );
1918 }
1919
1920 function get_id_from_clean_url( $clean_url ) {
1921 $found = false;
1922 $id = 0;
1923
1924 if( !$found ) {
1925 $id = $this->find_media_id_from_file( $clean_url, false );
1926 if ( $id ) {
1927 $is_attachment = get_post_type( $id ) === 'attachment';
1928 if ( $is_attachment ) {
1929 $found = true;
1930 }
1931 }
1932 }
1933
1934 if( !$found ) {
1935 $id = $this->custom_attachment_url_to_postid( $clean_url );
1936 if ( $id ) {
1937 $is_attachment = get_post_type( $id ) === 'attachment';
1938 if ( $is_attachment ) {
1939 $found = true;
1940 }
1941 }
1942 }
1943
1944 if ( !$found ) {
1945 $id = $this->resolve_from_database( $clean_url );
1946 if ( $id ) {
1947 $is_attachment = get_post_type( $id ) === 'attachment';
1948 if ( $is_attachment ) {
1949 $found = true;
1950 }
1951 }
1952 }
1953
1954
1955 return $found ? $id : null;
1956 }
1957
1958 function resolve_from_database( $url ) {
1959 global $wpdb;
1960 $pattern = '/[_-]\d+x\d+(?=\.[a-z]{3,4}$)/';
1961 $url = preg_replace( $pattern, '', $url );
1962 $url = $this->get_pathinfo_from_image_src( $url );
1963 $query = $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE guid LIKE '%s'", '%' . $url . '%' );
1964 $attachment = $wpdb->get_col( $query );
1965 return empty( $attachment ) ? null : $attachment[0];
1966 }
1967
1968 function get_pathinfo_from_image_src( $image_src ) {
1969 $uploads = wp_upload_dir();
1970 $uploads_url = trailingslashit( $uploads['baseurl'] );
1971 if ( strpos( $image_src, $uploads_url ) === 0 )
1972 return ltrim( substr( $image_src, strlen( $uploads_url ) ), '/');
1973 else if ( strpos( $image_src, wp_make_link_relative( $uploads_url ) ) === 0 )
1974 return ltrim( substr( $image_src, strlen( wp_make_link_relative( $uploads_url ) ) ), '/');
1975 $img_info = parse_url( $image_src );
1976 return ltrim( $img_info['path'], '/' );
1977 }
1978
1979 function clean_url_from_resolution_ref( &$url ) {
1980 $url = $this->clean_url_from_resolution( $url );
1981 }
1982
1983 // From a url to the shortened and cleaned url (for example '2013/02/file.png')
1984 function clean_url( $url ) {
1985 // if ( is_array( $url ) ) {
1986 // error_log( print_r( $url, 1 ) );
1987 // }
1988 $dirIndex = strpos( $url, $this->upload_url );
1989 if ( empty( $url ) || $dirIndex === false ) {
1990 $finalUrl = null;
1991 }
1992 else {
1993 $finalUrl = urldecode( substr( $url, 1 + strlen( $this->upload_url ) + $dirIndex ) );
1994 }
1995 return $finalUrl;
1996 }
1997
1998 function custom_attachment_url_to_postid( $url ) {
1999 global $wpdb;
2000
2001 // Remove the query string
2002 $url = preg_replace('/\?.*/', '', $url);
2003
2004 // Try to find the attachment ID by matching the URL with the guid
2005 $attachment = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE guid LIKE %s AND post_type = 'attachment';", '%' . $wpdb->esc_like( $url ) ) );
2006
2007 // If found, return the first attachment ID
2008 if ( !empty( $attachment ) ) {
2009 return ( int )$attachment[0];
2010 }
2011
2012 // If not found, try to match the URL without the upload directory path
2013 $upload_dir = wp_upload_dir();
2014 $url_relative = str_replace( $upload_dir['baseurl'] . '/', '', $url );
2015
2016 $attachment = $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attached_file' AND meta_value LIKE %s;", '%' . $wpdb->esc_like( $url_relative ) ) );
2017
2018 // If found, return the first attachment ID
2019 if ( !empty( $attachment ) ) {
2020 return ( int )$attachment[0];
2021 }
2022
2023 // If still not found, return 0
2024 return 0;
2025 }
2026
2027 // From a fullpath to the shortened and cleaned path (for example '2013/02/file.png')
2028 // Original version by Jordy
2029 // function clean_uploaded_filename( $fullpath ) {
2030 // $basedir = $this->upload_path;
2031 // $file = str_replace( $basedir, '', $fullpath );
2032 // $file = str_replace( "./", "", $file );
2033 // $file = trim( $file, "/" );
2034 // return $file;
2035 // }
2036
2037 // From a fullpath to the shortened and cleaned path (for example '2013/02/file.png')
2038 // Faster version, more difficult to read, by Mike Meinz
2039 function clean_uploaded_filename( $fullpath ) {
2040 $dirIndex = strpos( $fullpath, $this->upload_url );
2041 if ( $dirIndex == false ) {
2042 $file = $fullpath;
2043 }
2044 else {
2045 // Remove first part of the path leaving yyyy/mm/filename.ext
2046 $file = substr( $fullpath, 1 + strlen( $this->upload_url ) + $dirIndex );
2047 }
2048 if ( substr( $file, 0, 2 ) == './' ) {
2049 $file = substr( $file, 2 );
2050 }
2051 if ( substr( $file, 0, 1 ) == '/' ) {
2052 $file = substr( $file, 1 );
2053 }
2054 return $file;
2055 }
2056
2057 /**
2058 * Check if the file or the Media ID is used in the install.
2059 * That file or ID will be checked against the database of references created by the plugin
2060 * by the parsers.
2061 */
2062 function reference_exists( $file, $mediaId ) {
2063 global $wpdb;
2064
2065 $table = $wpdb->prefix . "mclean_refs";
2066
2067 $row = null;
2068 if ( !empty( $mediaId ) ) {
2069 $row = $wpdb->get_row( $wpdb->prepare( "SELECT originType FROM $table WHERE mediaId = %d", $mediaId ) );
2070 if ( !empty( $row ) ) {
2071 $origin = $row->originType === 'MEDIA LIBRARY' ? 'Media Library' : 'content';
2072 $this->log( "�
2073 Media #{$mediaId} used by {$origin}" );
2074 return $row->originType;
2075 }
2076 }
2077 if ( !empty( $file ) ) {
2078 $row = $wpdb->get_row( $wpdb->prepare( "SELECT originType FROM $table WHERE mediaUrl = %s", $file ) );
2079 if ( !empty( $row ) ) {
2080 $origin = $row->originType === 'MEDIA LIBRARY' ? 'Media Library' : 'content';
2081 $this->log( "�
2082 File {$file} used by {$origin}" );
2083 return $row->originType;
2084 }
2085 }
2086 return false;
2087 }
2088
2089 function get_full_upload_path( $relative_path ) {
2090 $wp_upload_dir = wp_upload_dir();
2091 $full_path = trailingslashit( $wp_upload_dir['basedir'] ) . $relative_path;
2092 return $full_path;
2093 }
2094
2095 function get_paths_from_attachment( $attachmentId ) {
2096 $paths = array();
2097 $fullpath = get_attached_file( $attachmentId );
2098 if ( empty( $fullpath ) ) {
2099 $this->log( 'Could not find attached file for Media ID ' . $attachmentId );
2100 return array();
2101 }
2102 $mainfile = $this->clean_uploaded_filename( $fullpath );
2103 array_push( $paths, $mainfile );
2104 $baseUp = pathinfo( $mainfile );
2105 $filespath = trailingslashit( $this->upload_path ) . trailingslashit( $baseUp['dirname'] );
2106 $meta = wp_get_attachment_metadata( $attachmentId );
2107 if ( isset( $meta['original_image'] ) ) {
2108 $original_image = $this->clean_uploaded_filename( $filespath . $meta['original_image'] );
2109 array_push( $paths, $original_image );
2110 }
2111 $isImage = isset( $meta, $meta['width'], $meta['height'] );
2112 $sizes = $this->get_image_sizes();
2113 if ( $isImage && isset( $meta['sizes'] ) ) {
2114 foreach ( $meta['sizes'] as $name => $attr ) {
2115 if ( isset( $attr['file'] ) ) {
2116 $file = $this->clean_uploaded_filename( $filespath . $attr['file'] );
2117 array_push( $paths, $file );
2118 }
2119 }
2120 }
2121 return $paths;
2122 }
2123
2124 function is_media_ignored( $attachmentId ) {
2125 global $wpdb;
2126 $table_name = $wpdb->prefix . "mclean_scan";
2127 $issue = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE postId = %d", $attachmentId ), OBJECT );
2128 //error_log( $attachmentId );
2129 //error_log( print_r( $issue, 1 ) );
2130 if ( $issue && $issue->ignored )
2131 return true;
2132 return false;
2133 }
2134
2135 function check_media( $attachmentId, $checkOnly = false ) {
2136
2137 // Is Media ID ignored, consider as used.
2138 if ( $this->is_media_ignored( $attachmentId ) ) {
2139 return true;
2140 }
2141
2142 // Remove everything related to this media from the database.
2143 if ( !$checkOnly ) {
2144 $this->delete_attachment_related_data( $attachmentId );
2145 }
2146
2147 $size = 0;
2148 $countfiles = 0;
2149 $check_broken_media = !$this->check_content;
2150 $fullpath = get_attached_file( $attachmentId );
2151 $is_broken = apply_filters( 'wpmc_is_file_broken', !file_exists( $fullpath ), $attachmentId );
2152
2153 // It's a broken-only scan
2154 if ( $check_broken_media && !$is_broken ) {
2155 $is_considered_used = apply_filters( 'wpmc_check_media', true, $attachmentId, false );
2156 return $is_considered_used;
2157 }
2158
2159 // Let's analyze the usage of each path (thumbnails included) for this Media ID.
2160 $issue = 'NO_CONTENT';
2161 $paths = $this->get_paths_from_attachment( $attachmentId );
2162 foreach ( $paths as $path ) {
2163
2164 // If it's found in the content, we stop the scan right away
2165 if ( $this->check_content && $this->reference_exists( $path, $attachmentId ) ) {
2166 $is_considered_used = apply_filters( 'wpmc_check_media', true, $attachmentId, false );
2167 if ( $is_considered_used ) {
2168 return true;
2169 }
2170 }
2171
2172 // Let's count the size of the files for later, in case it's unused
2173 $filepath = trailingslashit( $this->upload_path ) . $path;
2174 if ( file_exists( $filepath ) )
2175 $size += filesize( $filepath );
2176 $countfiles++;
2177 }
2178
2179 // This Media ID seems not in used (or broken)
2180 // Let's double-check through the filter (overridable by users)
2181 $is_considered_used = apply_filters( 'wpmc_check_media', false, $attachmentId, $is_broken );
2182 if ( !$is_considered_used ) {
2183 if ( $is_broken ) {
2184 $this->log( "🚫 File {$fullpath} does not exist." );
2185 $issue = 'ORPHAN_MEDIA';
2186 }
2187 if ( !$checkOnly ) {
2188 global $wpdb;
2189 $table_name = $wpdb->prefix . "mclean_scan";
2190 $mainfile = $this->clean_uploaded_filename( $fullpath );
2191 $wpdb->insert( $table_name,
2192 array(
2193 'time' => current_time('mysql'),
2194 'type' => 1,
2195 'size' => $size,
2196 'path' => $mainfile . ( $countfiles > 0 ? ( " (+ " . $countfiles . " thumbnails)" ) : "" ),
2197 'postId' => $attachmentId,
2198 'issue' => $issue
2199 )
2200 );
2201 }
2202 }
2203 return $is_considered_used;
2204 }
2205
2206 // Delete all issues
2207 function reset_issues( $includingIgnored = false ) {
2208 global $wpdb;
2209 $table_name = $wpdb->prefix . "mclean_scan";
2210 if ( $includingIgnored ) {
2211 $wpdb->query( "DELETE FROM $table_name WHERE deleted = 0" );
2212 }
2213 else {
2214 $wpdb->query( "DELETE FROM $table_name WHERE ignored = 0 AND deleted = 0" );
2215 }
2216 if ( file_exists( WPMC_PATH . '/logs/media-cleaner.log' ) ) {
2217 file_put_contents( WPMC_PATH . '/logs/media-cleaner.log', '' );
2218 }
2219 }
2220
2221 function is_image_extension( $ext ) {
2222 $ext = strtolower( $ext );
2223 $valid = apply_filters( 'wpmc_valid_image_extensions', array( 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'ico', 'webp', 'avif' ) );
2224
2225 return in_array( $ext, $valid );
2226
2227 }
2228
2229
2230 function reset_references() {
2231 global $wpdb;
2232 $table_name = $wpdb->prefix . "mclean_refs";
2233 $wpdb->query("TRUNCATE $table_name");
2234 }
2235
2236 function get_issue_for_postId( $postId ) {
2237 global $wpdb;
2238 $table_name = $wpdb->prefix . "mclean_scan";
2239 $issue = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE postId = %d", $postId ), OBJECT );
2240 return $issue;
2241 }
2242
2243 function echo_issue( $issue ) {
2244 if ( $issue == 'NO_CONTENT' ) {
2245 _e( "Not found in content", 'media-cleaner' );
2246 }
2247 else if ( $issue == 'ORPHAN_FILE' ) {
2248 _e( "Not in Library", 'media-cleaner' );
2249 }
2250 else if ( $issue == 'ORPHAN_RETINA' ) {
2251 _e( "Orphan Retina", 'media-cleaner' );
2252 }
2253 else if ( $issue == 'ORPHAN_WEBP' ) {
2254 _e( "Orphan WebP", 'media-cleaner' );
2255 }
2256 else if ( $issue == 'ORPHAN_MEDIA' ) {
2257 _e( "No attached file", 'media-cleaner' );
2258 }
2259 else {
2260 echo $issue;
2261 }
2262 }
2263
2264 function get_uploads_directory_hierarchy() {
2265 $uploads_dir = wp_upload_dir();
2266 $base_dir = wp_normalize_path( $uploads_dir['basedir'] );
2267 $root = '/' . wp_basename( $base_dir );
2268 $directories = array();
2269
2270 // Get all subdirectories of the base directory
2271 $dir_iterator = new RecursiveDirectoryIterator( $base_dir, FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS );
2272 $iterator = new RecursiveIteratorIterator( $dir_iterator, RecursiveIteratorIterator::SELF_FIRST );
2273
2274 foreach ( $iterator as $file ) {
2275 if ( $file->isDir() ) {
2276 // Normalize path for consistency
2277 $file_path = wp_normalize_path( $file->getPathname() );
2278 // Remove base_dir from path
2279 $directory = str_replace( $base_dir, '', $file_path );
2280 if ( $directory ) {
2281 $directories[] = $root . $directory;
2282 }
2283 }
2284 }
2285
2286 // Return the hierarchy as a JSON file
2287 return json_encode( $directories );
2288 }
2289
2290 /**
2291 *
2292 * Roles & Access Rights
2293 *
2294 */
2295 public function can_access_settings() {
2296 return apply_filters( 'wpmc_allow_setup', current_user_can( 'manage_options' ) );
2297 }
2298
2299 public function can_access_features() {
2300 return apply_filters( 'wpmc_allow_usage', current_user_can( 'administrator' ) );
2301 }
2302
2303 #region Options
2304
2305 function list_options() {
2306 return array(
2307 'method' => 'media',
2308 'content' => true,
2309 'filesystem_content' => true,
2310 'media_library' => false,
2311 'live_content' => false,
2312 'debuglogs' => false,
2313 'images_only' => false,
2314 'attach_is_use' => false,
2315 'thumbnails_only' => false,
2316 'dirs_filter' => '',
2317 'files_filter' => '',
2318 'hide_thumbnails' => false,
2319 'hide_warning' => false,
2320 'skip_trash' => false,
2321 'medias_buffer' => 100,
2322 'posts_buffer' => 5,
2323 'analysis_buffer' => 100,
2324 'file_op_buffer' => 20,
2325 'uploads_file_buffer' => 5000,
2326 'delay' => 100,
2327 'refs_buffer' => 500,
2328 'shortcodes_disabled' => false,
2329
2330 'output_buffer_cleaning_disabled' => false,
2331 'php_error_logs' => false,
2332 'posts_per_page' => 10,
2333 'clean_uninstall' => false,
2334 'repair_mode' => false,
2335 'expert_mode' => false,
2336 'logs_path' => null,
2337 'thumbnail_force_issues' => [],
2338 );
2339 }
2340
2341 function reset_options() {
2342 delete_option( $this->option_name );
2343 }
2344
2345 function get_option( $option ) {
2346 $options = $this->get_all_options();
2347 return $options[$option];
2348 }
2349
2350 function get_all_options() {
2351 $options = get_option( $this->option_name, null );
2352 $options = $this->check_options( $options );
2353 return $options;
2354 }
2355
2356 // Let's work on this function if we need it.
2357 // Right now, it looks like the options are all updated at the same time.
2358
2359 // function update_option( $option, $value ) {
2360 // if ( !array_key_exists( $name, $options ) ) {
2361 // return new WP_REST_Response([ 'success' => false, 'message' => 'This option does not exist.' ], 200 );
2362 // }
2363 // $value = is_bool( $params['value'] ) ? ( $params['value'] ? '1' : '' ) : $params['value'];
2364 // }
2365
2366 function update_options( $options ) {
2367 if ( !update_option( $this->option_name, $options, false ) ) {
2368 return false;
2369 }
2370 $options = $this->sanitize_options();
2371 return $options;
2372 }
2373
2374 // Upgrade from the old way of storing options to the new way.
2375 function check_options( $options = [] ) {
2376 $plugin_options = $this->list_options();
2377 $options = empty( $options ) ? [] : $options;
2378 $hasChanges = false;
2379 foreach ( $plugin_options as $option => $default ) {
2380 // The option already exists
2381 if ( isset( $options[$option] ) ) {
2382 continue;
2383 }
2384 // The option does not exist, so we need to add it.
2385 // Let's use the old value if any, or the default value.
2386 $options[$option] = get_option( 'wpmc_' . $option, $default );
2387 delete_option( 'wpmc_' . $option );
2388 $hasChanges = true;
2389 }
2390 if ( $hasChanges ) {
2391 update_option( $this->option_name , $options );
2392 }
2393
2394 // Dynamically added options
2395 //TODO: we should have a rest route to fetch this instead of using the options directly. This is temporary.
2396 $options['scan_progress'] = get_transient( $this->progress_key );
2397 $options['thumbnail_sizes'] = $this->get_thumbnail_sizes();
2398
2399 return $options;
2400 }
2401
2402 // Validate and keep the options clean and logical.
2403 function sanitize_options() {
2404 $options = $this->get_all_options();
2405 $medias = $options['medias_buffer'];
2406 $posts = $options['posts_buffer'];
2407 $analysis = $options['analysis_buffer'];
2408 $fileOp = $options['file_op_buffer'];
2409 $delay = $options['delay'];
2410 $hasChanges = false;
2411 if ( $medias === '' ) {
2412 $options['medias_buffer'] = 100;
2413 $hasChanges = true;
2414 }
2415 if ( $posts === '' ) {
2416 $options['posts_buffer'] = 5;
2417 $hasChanges = true;
2418 }
2419 if ( $analysis === '' ) {
2420 $options['analysis_buffer'] = 100;
2421 $hasChanges = true;
2422 }
2423 if ( $fileOp === '' ) {
2424 $options['file_op_buffer'] = 20;
2425 $hasChanges = true;
2426 }
2427 if ( $delay === '' ) {
2428 $options['delay'] = 100;
2429 $hasChanges = true;
2430 }
2431 $refs = $options['refs_buffer'];
2432 if ( $refs === '' ) {
2433 $options['refs_buffer'] = 500;
2434 $hasChanges = true;
2435 }
2436 if ( $hasChanges ) {
2437 update_option( $this->option_name, $options, false );
2438 }
2439 return $options;
2440 }
2441
2442 #endregion
2443 }
2444
2445 // Check the DB. If does not exist, let's create it.
2446 function wpmc_check_database() {
2447 wpmc_create_database();
2448 }
2449
2450 function wpmc_create_database() {
2451 global $wpdb;
2452 $table_name = $wpdb->prefix . "mclean_scan";
2453 $charset_collate = $wpdb->get_charset_collate();
2454 $sql = "CREATE TABLE $table_name (
2455 id BIGINT(20) NOT NULL AUTO_INCREMENT,
2456 time DATETIME DEFAULT '0000-00-00 00:00:00' NOT NULL,
2457 type TINYINT(1) NOT NULL,
2458 postId BIGINT(20) NULL,
2459 path TINYTEXT NULL,
2460 size INT(9) NULL,
2461 ignored TINYINT(1) NOT NULL DEFAULT 0,
2462 deleted TINYINT(1) NOT NULL DEFAULT 0,
2463 issue TINYTEXT NOT NULL,
2464 parentId BIGINT(20) NULL,
2465 PRIMARY KEY (id),
2466 KEY PostIdIndex (postId),
2467 KEY IgnoredIndex (ignored)
2468 ) " . $charset_collate . ";" ;
2469 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
2470 dbDelta( $sql );
2471
2472 $table_name = $wpdb->prefix . "mclean_refs";
2473 $charset_collate = $wpdb->get_charset_collate();
2474 // This key doesn't work on too many installs because of the 'Specified key was too long' issue
2475 // KEY mediaLookUp (mediaId, mediaUrl)
2476 $sql = "CREATE TABLE $table_name (
2477 id BIGINT(20) NOT NULL AUTO_INCREMENT,
2478 mediaId BIGINT(20) NULL,
2479 mediaUrl TINYTEXT NULL,
2480 originType TINYTEXT NOT NULL,
2481 origin TINYTEXT NULL,
2482 parentId BIGINT(20) NULL,
2483 ref_hash VARCHAR(32) NULL,
2484 PRIMARY KEY (id),
2485 KEY mediaId_index (mediaId),
2486 UNIQUE KEY ref_hash_unique (ref_hash)
2487 ) " . $charset_collate . ";";
2488 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
2489 dbDelta( $sql );
2490 }
2491
2492 function wpmc_remove_database() {
2493 global $wpdb;
2494 $table_name1 = $wpdb->prefix . "mclean_scan";
2495 $table_name2 = $wpdb->prefix . "mclean_refs";
2496 $table_name3 = $wpdb->prefix . "wpmcleaner";
2497 $sql = "DROP TABLE IF EXISTS $table_name1, $table_name2, $table_name3;";
2498 $wpdb->query( $sql );
2499 }
2500
2501 #region Install / Uninstall
2502
2503 /*
2504 INSTALL / UNINSTALL
2505 */
2506
2507 function wpmc_init( $mainfile ) {
2508 //register_activation_hook( $mainfile, 'wpmc_install' );
2509 //register_deactivation_hook( $mainfile, 'wpmc_uninstall' );
2510 register_uninstall_hook( $mainfile, 'wpmc_uninstall' );
2511 }
2512
2513 function wpmc_install() {
2514 wpmc_create_database();
2515 }
2516
2517 function wpmc_reset () {
2518 wpmc_remove_database();
2519 wpmc_create_database();
2520 }
2521
2522 function wpmc_remove_options() {
2523 global $wpdb;
2524 $options = $wpdb->get_results( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE 'wpmc_%'" );
2525 foreach( $options as $option ) {
2526 delete_option( $option->option_name );
2527 }
2528 }
2529
2530 function wpmc_uninstall () {
2531 $options = get_option( 'wpmc_options', [] );
2532 $cleanUninstall = $options['clean_uninstall'];
2533 if ($cleanUninstall) {
2534 wpmc_remove_options();
2535 wpmc_remove_database();
2536 }
2537 }
2538
2539 #endregion