PluginProbe ʕ •ᴥ•ʔ
Media Cleaner: Clean your WordPress! / 5.5.4
Media Cleaner: Clean your WordPress! v5.5.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 / core.php
media-cleaner Last commit date
common 6 years ago parsers 6 years ago scripts 6 years ago views 6 years ago admin.php 6 years ago api.php 7 years ago core.php 6 years ago engine.php 6 years ago media-cleaner.php 6 years ago parsers.php 6 years ago readme.txt 6 years ago ui.php 6 years ago
core.php
1019 lines
1 <?php
2
3 class Meow_WPMC_Core {
4
5 public $admin = null;
6 public $last_analysis = null; //TODO: Is it actually used?
7 public $engine = null;
8 public $catch_timeout = true; // This will halt the plugin before reaching the PHP timeout.
9 private $regex_file = '/[A-Za-z0-9-_,.\(\)\s]+[.]{1}(MIMETYPES)/';
10 public $current_method = 'media';
11 private $refcache = array();
12 public $servername = null;
13 public $upload_folder = null;
14 public $contentDir = null; // becomes 'wp-content/uploads'
15 private $check_content = null;
16 private $check_postmeta = null;
17 private $check_posts = null;
18 private $check_widgets = null;
19 private $debug_logs = null;
20 public $site_url = null;
21
22 public function __construct( $admin ) {
23 $this->admin = $admin;
24 $this->site_url = get_site_url();
25 $this->current_method = get_option( 'wpmc_method', 'media' );
26 $types = "jpg|jpeg|jpe|gif|png|tiff|bmp|csv|pdf|xls|xlsx|doc|docx|odt|wpd|rtf|tiff|mp3|mp4|wav|lua";
27 $this->regex_file = str_replace( "MIMETYPES", $types, $this->regex_file );
28 $this->servername = str_replace( 'http://', '', str_replace( 'https://', '', $this->site_url ) );
29 $this->upload_folder = wp_upload_dir();
30 $this->contentDir = substr( $this->upload_folder['baseurl'], 1 + strlen( $this->site_url ) );
31
32 $this->check_content = get_option( 'wpmc_content', true );
33 $this->check_postmeta = get_option( 'wpmc_postmeta', false );
34 $this->check_posts = get_option( 'wpmc_posts', false );
35 $this->check_widgets = get_option( 'wpmc_widgets', false );
36
37 if ( $this->check_postmeta || $this->check_posts || $this->check_widgets ) {
38 delete_option( 'wpmc_postmeta' );
39 delete_option( 'wpmc_posts' );
40 delete_option( 'wpmc_widgets' );
41 }
42
43 $this->debug_logs = get_option( 'wpmc_debuglogs', false );
44 add_action( 'wpmc_initialize_parsers', array( $this, 'initialize_parsers' ), 10, 0 );
45
46 require __DIR__ . '/engine.php';
47 require __DIR__ . '/ui.php';
48 require __DIR__ . '/api.php';
49 $this->engine = new Meow_WPMC_Engine( $this, $admin );
50 new Meow_WPMC_UI( $this, $admin );
51 new Meow_WPMC_API( $this, $admin, $this->engine );
52 }
53
54 function initialize_parsers() {
55 include_once( 'parsers.php' );
56 new MeowApps_WPMC_Parsers();
57 }
58
59 function deepsleep( $seconds ) {
60 $start_time = time();
61 while( true ) {
62 if ( ( time() - $start_time ) > $seconds ) {
63 return false;
64 }
65 get_post( array( 'posts_per_page' => 50 ) );
66 }
67 }
68
69 private $start_time;
70 private $time_elapsed = 0;
71 private $item_scan_avg_time = 0;
72 private $wordpress_init_time = 0.5;
73 private $max_execution_time;
74 private $items_checked = 0;
75 private $items_count = 0;
76
77 function get_max_execution_time() {
78 if ( isset( $this->max_execution_time ) )
79 return $this->max_execution_time;
80
81 $this->max_execution_time = ini_get( "max_execution_time" );
82 if ( empty( $this->max_execution_time ) || $this->max_execution_time < 5 )
83 $this->max_execution_time = 30;
84
85 return $this->max_execution_time;
86 }
87
88 function timeout_check_start( $count ) {
89 $this->start_time = time();
90 $this->items_count = $count;
91 $this->get_max_execution_time();
92 }
93
94 function timeout_check() {
95 $this->time_elapsed = time() - $this->start_time;
96 $this->time_remaining = $this->max_execution_time - $this->wordpress_init_time - $this->time_elapsed;
97 if ( $this->catch_timeout ) {
98 if ( $this->time_remaining - $this->item_scan_avg_time < 0 ) {
99 error_log("Media Cleaner Timeout! Check the Media Cleaner logs for more info.");
100 $this->log( "Timeout! Some info for debug:" );
101 $this->log( "Elapsed time: $this->time_elapsed" );
102 $this->log( "WP init time: $this->wordpress_init_time" );
103 $this->log( "Remaining time: $this->time_remaining" );
104 $this->log( "Scan time per item: $this->item_scan_avg_time" );
105 $this->log( "PHP max_execution_time: $this->max_execution_time" );
106 header("HTTP/1.0 408 Request Timeout");
107 exit;
108 }
109 }
110 }
111
112 function timeout_check_additem() {
113 $this->items_checked++;
114 $this->time_elapsed = time() - $this->start_time;
115 $this->item_scan_avg_time = ceil( ( $this->time_elapsed / $this->items_checked ) * 10 ) / 10;
116 }
117
118 function array_to_ids_or_urls( &$meta, &$ids, &$urls ) {
119 foreach ( $meta as $k => $m ) {
120 if ( is_numeric( $m ) ) {
121 // Probably a Media ID
122 if ( $m > 0 )
123 array_push( $ids, $m );
124 }
125 else if ( is_array( $m ) ) {
126 // If it's an array with a width, probably that the index is the Media ID
127 if ( isset( $m['width'] ) && is_numeric( $k ) ) {
128 if ( $k > 0 )
129 array_push( $ids, $k );
130 }
131 }
132 else if ( !empty( $m ) ) {
133 // If it's a string, maybe it's a file (with an extension)
134 if ( preg_match( $this->regex_file, $m ) )
135 array_push( $urls, $m );
136 }
137 }
138 }
139
140 function get_favicon() {
141 // Yoast SEO plugin
142 $vals = get_option( 'wpseo_titles' );
143 if ( !empty( $vals ) ) {
144 $url = $vals['company_logo'];
145 if ( $this->is_url( $url ) )
146 return $this->clean_url( $url );
147 }
148 }
149
150 function get_urls_from_html( $html ) {
151 if ( empty( $html ) )
152 return array();
153
154 // Proposal/fix by @copytrans
155 // Discussion: https://wordpress.org/support/topic/bug-in-core-php/#post-11647775
156 $html = mb_convert_encoding( $html, 'HTML-ENTITIES', 'UTF-8' );
157
158 // Resolve src-set and shortcodes
159 if ( !get_option( 'wpmc_shortcodes_disabled', false ) )
160 $html = do_shortcode( $html );
161 $html = wp_make_content_images_responsive( $html );
162
163 // Create the DOM Document
164 $dom = new DOMDocument();
165 @$dom->loadHTML( $html );
166 $results = array();
167
168 // <meta> tags in <head> area
169 $metas = $dom->getElementsByTagName( 'meta' );
170 foreach ( $metas as $meta ) {
171 $property = $meta->getAttribute( 'property' );
172 if ( $property == 'og:image' || $property == 'og:image:secure_url' || $property == 'twitter:image' ) {
173 $url = $meta->getAttribute( 'content' );
174 if ( $this->is_url( $url ) ) {
175 $src = $this->clean_url( $url );
176 if ( !empty( $src ) ) {
177 array_push( $results, $src );
178 }
179 }
180 }
181 }
182
183 // IFrames (by Mike Meinz)
184 $iframes = $dom->getElementsByTagName( 'iframe' );
185 foreach( $iframes as $iframe ) {
186 $iframe_src = $iframe->getAttribute( 'src' );
187 // Ignore if the iframe src is not on this server
188 if ( ( strpos( $iframe_src, $this->servername ) !== false) || ( substr( $iframe_src, 0, 1 ) == "/" ) ) {
189 // Create a new DOM Document to hold iframe
190 $iframe_doc = new DOMDocument();
191 // Load the url's contents into the DOM
192 libxml_use_internal_errors( true ); // ignore html formatting problems
193 $rslt = $iframe_doc->loadHTMLFile( $iframe_src );
194 libxml_clear_errors();
195 libxml_use_internal_errors( false );
196 if ( $rslt ) {
197 // Get the resulting html
198 $iframe_html = $iframe_doc->saveHTML();
199 if ( $iframe_html !== false ) {
200 // Scan for links in the iframe
201 $iframe_urls = $this->get_urls_from_html( $iframe_html ); // Recursion
202 if ( !empty( $iframe_urls ) ) {
203 $results = array_merge( $results, $iframe_urls );
204 }
205 }
206 }
207 else {
208 $err = 'ERROR: Failed to load iframe: ' . $iframe_src;
209 //error_log( $err );
210 $this->log( $err );
211 }
212 }
213 }
214
215
216 // Images, src, srcset
217 $imgs = $dom->getElementsByTagName( 'img' );
218 foreach ( $imgs as $img ) {
219 //error_log($img->getAttribute('src'));
220 $src = $this->clean_url( $img->getAttribute('src') );
221 array_push( $results, $src );
222 $srcset = $img->getAttribute('srcset');
223 if ( !empty( $srcset ) ) {
224 $setImgs = explode( ',', trim( $srcset ) );
225 foreach ( $setImgs as $setImg ) {
226 $finalSetImg = explode( ' ', trim( $setImg ) );
227 if ( is_array( $finalSetImg ) ) {
228 array_push( $results, $this->clean_url( $finalSetImg[0] ) );
229 }
230 }
231 }
232 }
233
234 // Links, href
235 $urls = $dom->getElementsByTagName( 'a' );
236 foreach ( $urls as $url ) {
237 $url_href = $url->getAttribute('href'); // mm change
238 if ( $this->is_url( $url_href ) ) { // mm change
239 $src = $this->clean_url( $url_href ); // mm change
240 if ( !empty( $src ) )
241 array_push( $results, $src );
242 }
243 }
244
245 // <link> tags in <head> area
246 $urls = $dom->getElementsByTagName( 'link' );
247 foreach ( $urls as $url ) {
248 $url_href = $url->getAttribute( 'href' );
249 if ( $this->is_url( $url_href ) ) {
250 $src = $this->clean_url( $url_href );
251 if ( !empty( $src ) ) {
252 array_push( $results, $src );
253 }
254 }
255 }
256
257 // PDF
258 preg_match_all( "/((https?:\/\/)?[^\\&\#\[\] \"\?]+\.pdf)/", $html, $res );
259 if ( !empty( $res ) && isset( $res[1] ) && count( $res[1] ) > 0 ) {
260 foreach ( $res[1] as $url ) {
261 if ( $this->is_url($url) )
262 array_push( $results, $this->clean_url( $url ) );
263 }
264 }
265
266 // Background images
267 preg_match_all( "/url\(\'?\"?((https?:\/\/)?[^\\&\#\[\] \"\?]+\.(jpe?g|gif|png))\'?\"?/", $html, $res );
268 if ( !empty( $res ) && isset( $res[1] ) && count( $res[1] ) > 0 ) {
269 foreach ( $res[1] as $url ) {
270 if ( $this->is_url($url) )
271 array_push( $results, $this->clean_url( $url ) );
272 }
273 }
274
275 return $results;
276 }
277
278 // Parse a meta, visit all the arrays, look for the attributes, fill $ids and $urls arrays
279 function get_from_meta( $meta, $lookFor, &$ids, &$urls ) {
280 foreach ( $meta as $key => $value ) {
281 if ( is_object( $value ) || is_array( $value ) )
282 $this->get_from_meta( $value, $lookFor, $ids, $urls );
283 else if ( in_array( $key, $lookFor ) ) {
284 if ( empty( $value ) )
285 continue;
286 else if ( is_numeric( $value ) ) {
287 // It this an ID?
288 array_push( $ids, $value );
289 }
290 else {
291 if ( $this->is_url( $value ) ) {
292 // Is this an URL?
293 array_push( $urls, $this->clean_url( $value ) );
294 }
295 else {
296 // Is this an array of IDs, encoded as a string? (like "20,13")
297 $pieces = explode( ',', $value );
298 foreach ( $pieces as $pval ) {
299 if ( is_numeric( $pval ) ) {
300 array_push( $ids, $pval );
301 }
302 }
303 }
304 }
305 }
306 }
307 }
308
309 function get_images_from_themes( &$ids, &$urls ) {
310 // USE CURRENT THEME AND WP API
311 $ch = get_custom_header();
312 if ( !empty( $ch ) && !empty( $ch->url ) ) {
313 array_push( $urls, $this->clean_url( $ch->url ) );
314 }
315 if ( $this->is_url( $ch->thumbnail_url ) ) {
316 array_push( $urls, $this->clean_url( $ch->thumbnail_url ) );
317 }
318 if ( !empty( $ch ) && !empty( $ch->attachment_id ) ) {
319 array_push( $ids, $ch->attachment_id );
320 }
321 $cl = get_custom_logo();
322 if ( $this->is_url( $cl ) ) {
323 $urls = array_merge( $this->get_urls_from_html( $cl ), $urls );
324 }
325 $si = get_site_icon_url();
326 if ( $this->is_url( $si ) ) {
327 array_push( $urls, $this->clean_url( $si ) );
328 }
329 $si_id = get_option( 'site_icon' );
330 if ( !empty( $si_id ) && is_numeric( $si_id ) ) {
331 array_push( $ids, (int)$si_id );
332 }
333 $cd = get_background_image();
334 if ( $this->is_url( $cd ) ) {
335 array_push( $urls, $this->clean_url( $cd ) );
336 }
337 $photography_hero_image = get_theme_mod( 'photography_hero_image' );
338 if ( !empty( $photography_hero_image ) ) {
339 array_push( $ids, $photography_hero_image );
340 }
341 $author_profile_picture = get_theme_mod( 'author_profile_picture' );
342 if ( !empty( $author_profile_picture ) ) {
343 array_push( $ids, $author_profile_picture );
344 }
345 if ( function_exists ( 'get_uploaded_header_images' ) ) {
346 $header_images = get_uploaded_header_images();
347 if ( !empty( $header_images ) ) {
348 foreach( $header_images as $hi ) {
349 if ( !empty ( $hi['attachment_id'] ) ) {
350 array_push( $ids, $hi['attachment_id'] );
351 }
352 }
353 }
354 }
355 }
356
357 function log( $data = null, $force = false ) {
358 if ( !$this->debug_logs && !$force )
359 return;
360 $fh = @fopen( trailingslashit( dirname(__FILE__) ) . '/media-cleaner.log', 'a' );
361 if ( !$fh )
362 return false;
363 $date = date( "Y-m-d H:i:s" );
364 if ( is_null( $data ) )
365 fwrite( $fh, "\n" );
366 else
367 fwrite( $fh, "$date: {$data}\n" );
368 fclose( $fh );
369 return true;
370 }
371
372 /**
373 *
374 * HELPERS
375 *
376 */
377
378 function get_trashdir() {
379 return trailingslashit( $this->upload_folder['basedir'] ) . 'wpmc-trash';
380 }
381
382 /**
383 *
384 * DELETE / SCANNING / RESET
385 *
386 */
387
388 function recover_file( $path ) {
389 $originalPath = trailingslashit( $this->upload_folder['basedir'] ) . $path;
390 $trashPath = trailingslashit( $this->get_trashdir() ) . $path;
391 $path_parts = pathinfo( $originalPath );
392 if ( !file_exists( $path_parts['dirname'] ) && !wp_mkdir_p( $path_parts['dirname'] ) ) {
393 die( 'Failed to create folder.' );
394 }
395 if ( !file_exists( $trashPath ) ) {
396 $this->log( "The file $originalPath actually does not exist in the trash." );
397 return true;
398 }
399 if ( !rename( $trashPath, $originalPath ) ) {
400 die( 'Failed to move the file.' );
401 }
402 return true;
403 }
404
405 function recover( $id ) {
406 global $wpdb;
407 $table_name = $wpdb->prefix . "mclean_scan";
408 $issue = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id ), OBJECT );
409 $issue->path = stripslashes( $issue->path );
410
411 // Files
412 if ( $issue->type == 0 ) {
413 $this->recover_file( $issue->path );
414 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET deleted = 0 WHERE id = %d", $id ) );
415 return true;
416 }
417 // Media
418 else if ( $issue->type == 1 ) {
419
420 // Copy the main file back
421 $fullpath = get_attached_file( $issue->postId );
422 if ( empty( $fullpath ) ) {
423 error_log( "Media {$issue->postId} does not have attached file anymore." );
424 return false;
425 }
426 $mainfile = $this->clean_uploaded_filename( $fullpath );
427 $baseUp = pathinfo( $mainfile );
428 $baseUp = $baseUp['dirname'];
429 $file = $this->clean_uploaded_filename( $fullpath );
430 if ( !$this->recover_file( $file ) ) {
431 $this->log( "Could not recover $file." );
432 error_log( "Media Cleaner: Could not recover $file." );
433 }
434
435 // If images, copy the other files as well
436 $meta = wp_get_attachment_metadata( $issue->postId );
437 $isImage = isset( $meta, $meta['width'], $meta['height'] );
438 $sizes = $this->get_image_sizes();
439 if ( $isImage && isset( $meta['sizes'] ) ) {
440 foreach ( $meta['sizes'] as $name => $attr ) {
441 if ( isset( $attr['file'] ) ) {
442 $filepath = $this->upload_folder['basedir'];
443 $filepath = trailingslashit( $filepath ) . trailingslashit( $baseUp ) . $attr['file'];
444 $file = $this->clean_uploaded_filename( $filepath );
445 if ( !$this->recover_file( $file ) ) {
446 $this->log( "Could not recover $file." );
447 error_log( "Media Cleaner: Could not recover $file." );
448 }
449 }
450 }
451 }
452 if ( !wp_untrash_post( $issue->postId ) ) {
453 error_log( "Cleaner: Failed to Untrash Post {$issue->postId} (but deleted it from Cleaner DB)." );
454 }
455 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET deleted = 0 WHERE id = %d", $id ) );
456 return true;
457 }
458 }
459
460 function trash_file( $fileIssuePath ) {
461 global $wpdb;
462 $originalPath = trailingslashit( $this->upload_folder['basedir'] ) . $fileIssuePath;
463 $trashPath = trailingslashit( $this->get_trashdir() ) . $fileIssuePath;
464 $path_parts = pathinfo( $trashPath );
465
466 try {
467 if ( !file_exists( $path_parts['dirname'] ) && !wp_mkdir_p( $path_parts['dirname'] ) ) {
468 $this->log( "Could not create the trash directory for Media Cleaner." );
469 error_log( "Media Cleaner: Could not create the trash directory." );
470 return false;
471 }
472 // Rename the file (move). 'is_dir' is just there for security (no way we should move a whole directory)
473 if ( is_dir( $originalPath ) ) {
474 $this->log( "Attempted to delete a directory instead of a file ($originalPath). Can't do that." );
475 error_log( "Media Cleaner: Attempted to delete a directory instead of a file ($originalPath). Can't do that." );
476 return false;
477 }
478 if ( !file_exists( $originalPath ) ) {
479 $this->log( "The file $originalPath actually does not exist." );
480 return true;
481 }
482 if ( !@rename( $originalPath, $trashPath ) ) {
483 return false;
484 }
485 }
486 catch ( Exception $e ) {
487 return false;
488 }
489 $this->clean_dir( dirname( $originalPath ) );
490 return true;
491 }
492
493 function ignore( $id ) {
494 global $wpdb;
495 $table_name = $wpdb->prefix . "mclean_scan";
496 $issue = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id ), OBJECT );
497 if ( (int) $issue->ignored )
498 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET ignored = 0 WHERE id = %d", $id ) );
499 else {
500 if ( (int) $issue->deleted ) // If it is in trash, recover it
501 $this->recover( $id );
502 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET ignored = 1 WHERE id = %d", $id ) );
503 }
504 return true;
505 }
506
507 function endsWith( $haystack, $needle )
508 {
509 $length = strlen( $needle );
510 if ( $length == 0 )
511 return true;
512 return ( substr( $haystack, -$length ) === $needle );
513 }
514
515 function clean_dir( $dir ) {
516 if ( !file_exists( $dir ) )
517 return;
518 else if ( $this->endsWith( $dir, 'uploads' ) )
519 return;
520 $found = array_diff( scandir( $dir ), array( '.', '..' ) );
521 if ( count( $found ) < 1 ) {
522 if ( rmdir( $dir ) ) {
523 $this->clean_dir( dirname( $dir ) );
524 }
525 }
526 }
527
528 function delete( $id ) {
529 global $wpdb;
530 $table_name = $wpdb->prefix . "mclean_scan";
531 $issue = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id ), OBJECT );
532 $regex = "^(.*)(\\s\\(\\+.*)$";
533 $issue->path = preg_replace( '/' . $regex . '/i', '$1', $issue->path ); // remove " (+ 6 files)" from path
534
535 // Make sure there isn't a media DB entry
536 if ( $issue->type == 0 ) {
537 $attachmentid = $this->find_media_id_from_file( $issue->path, true );
538 if ( $attachmentid ) {
539 $this->log( "Issue listed as filesystem but Media {$attachmentid} exists." );
540 }
541 }
542
543 if ( $issue->type == 0 ) {
544
545 if ( $issue->deleted == 0 ) {
546 // Move file to the trash
547 if ( $this->trash_file( $issue->path ) )
548 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET deleted = 1, ignored = 0 WHERE id = %d", $id ) );
549 return true;
550 }
551 else {
552 // Delete file from the trash
553 $trashPath = trailingslashit( $this->get_trashdir() ) . $issue->path;
554 if ( !unlink( $trashPath ) ) {
555 $this->log( "Failed to delete the file." );
556 error_log( "Media Cleaner: Failed to delete the file." );
557 }
558 $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE id = %d", $id ) );
559 $this->clean_dir( dirname( $trashPath ) );
560 return true;
561 }
562 }
563
564 if ( $issue->type == 1 ) {
565 if ( $issue->deleted == 0 && MEDIA_TRASH ) {
566 // Move Media to trash
567 // Let's copy the images to the trash so that it can be recovered.
568 $fullpath = get_attached_file( $issue->postId );
569 $mainfile = $this->clean_uploaded_filename( $fullpath );
570 $baseUp = pathinfo( $mainfile );
571 $baseUp = $baseUp['dirname'];
572 $file = $this->clean_uploaded_filename( $fullpath );
573 if ( !$this->trash_file( $file ) ) {
574 $this->log( "Could not trash $file." );
575 error_log( "Media Cleaner: Could not trash $file." );
576 return false;
577 }
578
579 // If images, check the other files as well
580 $meta = wp_get_attachment_metadata( $issue->postId );
581 $isImage = isset( $meta, $meta['width'], $meta['height'] );
582 $sizes = $this->get_image_sizes();
583 if ( $isImage && isset( $meta['sizes'] ) ) {
584 foreach ( $meta['sizes'] as $name => $attr ) {
585 if ( isset( $attr['file'] ) ) {
586 $filepath = $this->upload_folder['basedir'];
587 $filepath = trailingslashit( $filepath ) . trailingslashit( $baseUp ) . $attr['file'];
588 $file = $this->clean_uploaded_filename( $filepath );
589 if ( !$this->trash_file( $file ) ) {
590 $this->log( "Could not trash $file." );
591 error_log( "Media Cleaner: Could not trash $file." );
592 }
593 }
594 }
595 }
596 wp_delete_attachment( $issue->postId, false );
597 $wpdb->query( $wpdb->prepare( "UPDATE $table_name SET deleted = 1, ignored = 0 WHERE id = %d", $id ) );
598 return true;
599 }
600 else {
601 // Trash Media definitely by recovering it (to be like a normal Media) and remove it through the
602 // standard WordPress workflow
603 if ( MEDIA_TRASH )
604 $this->recover( $id );
605 wp_delete_attachment( $issue->postId, true );
606 $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE id = %d", $id ) );
607 return true;
608 }
609 }
610 return false;
611 }
612
613 /**
614 *
615 * SCANNING / RESET
616 *
617 */
618
619 function add_reference_url( $urlOrUrls, $type, $origin = null, $extra = null ) {
620 $urlOrUrls = !is_array( $urlOrUrls ) ? array( $urlOrUrls ) : $urlOrUrls;
621 foreach ( $urlOrUrls as $url ) {
622 // With files, we need both filename without resolution and filename with resolution, it's important
623 // to make sure the original file is not deleted if a size exists for it.
624 // With media, all URLs should be without resolution to make sure it matches Media.
625 if ( $this->current_method == 'files' )
626 $this->add_reference( null, $url, $type, $origin );
627 $this->add_reference( 0, $this->clean_url_from_resolution( $url ), $type, $origin );
628 }
629 }
630
631 function add_reference_id( $idOrIds, $type, $origin = null, $extra = null ) {
632 $idOrIds = !is_array( $idOrIds ) ? array( $idOrIds ) : $idOrIds;
633 foreach ( $idOrIds as $id )
634 $this->add_reference( $id, "", $type, $origin );
635 }
636
637 private $cached_ids = array();
638 private $cached_urls = array();
639
640 private function add_reference( $id, $url, $type, $origin = null, $extra = null ) {
641 // The references are actually not being added directly in the DB, they are being pushed
642 // into a cache ($this->refcache).
643 if ( !empty( $id ) ) {
644 if ( !in_array( $id, $this->cached_ids ) )
645 array_push( $this->cached_ids, $id );
646 else
647 return;
648 }
649 if ( !empty( $url ) ) {
650 // The URL shouldn't contain http, https, javascript at the beginning (and there are probably many more cases)
651 // The URL must be cleaned before being passed as a reference.
652 if ( substr( $url, 0, 5 ) === "http:" )
653 return;
654 if ( substr( $url, 0, 6 ) === "https:" )
655 return;
656 if ( substr( $url, 0, 11 ) === "javascript:" )
657 return;
658 if ( !in_array( $url, $this->cached_urls ) )
659 array_push( $this->cached_urls, $url );
660 else
661 return;
662 }
663 //
664 array_push( $this->refcache, array( 'id' => $id, 'url' => $url, 'type' => $type, 'origin' => $origin ) );
665
666 // Without cache, the code would be this.
667 // $wpdb->insert( $table_name,
668 // array(
669 // 'time' => current_time('mysql'), 'mediaId' => $id, 'mediaUrl' => $url, 'origin' => $origin, 'originType' => $type )
670 // );
671 }
672
673 // The cache containing the references is wrote to the DB.
674 function write_references() {
675 global $wpdb;
676 $table = $wpdb->prefix . "mclean_refs";
677 $values = array();
678 $place_holders = array();
679 $query = "INSERT INTO $table (mediaId, mediaUrl, originType) VALUES ";
680 foreach( $this->refcache as $key => $value ) {
681 array_push( $values, $value['id'], $value['url'], $value['type'] );
682 if ( $this->debug_logs ) {
683 if ( !empty( $value['id'] ) )
684 $this->log( "* {$value['type']}: Media #{$value['id']}" );
685 if ( !empty( $value['url'] ) )
686 $this->log( "* {$value['type']}: {$value['url']}" );
687 }
688 $place_holders[] = "('%d','%s','%s')";
689 }
690 if ( !empty( $values ) ) {
691 $query .= implode( ', ', $place_holders );
692 $prepared = $wpdb->prepare( "$query ", $values );
693 $wpdb->query( $prepared );
694 }
695 $this->refcache = array();
696 }
697
698 function check_is_ignore( $file ) {
699 global $wpdb;
700 $table_name = $wpdb->prefix . "mclean_scan";
701 $count = $wpdb->get_var( "SELECT COUNT(*)
702 FROM $table_name
703 WHERE ignored = 1
704 AND path LIKE '%". esc_sql( $wpdb->esc_like( $file ) ) . "%'" );
705 if ( $count > 0 ) {
706 $this->log( "Could not trash $file." );
707 }
708 return ($count > 0);
709 }
710
711 function find_media_id_from_file( $file, $doLog ) {
712 global $wpdb;
713 $postmeta_table_name = $wpdb->prefix . 'postmeta';
714 $file = $this->clean_uploaded_filename( $file );
715 $sql = $wpdb->prepare( "SELECT post_id
716 FROM {$postmeta_table_name}
717 WHERE meta_key = '_wp_attached_file'
718 AND meta_value = %s", $file
719 );
720 $ret = $wpdb->get_var( $sql );
721 if ( $doLog ) {
722 if ( empty( $ret ) )
723 $this->log( "File $file not found as _wp_attached_file (Library)." );
724 else {
725 $this->log( "File $file found as Media $ret." );
726 }
727 }
728
729 return $ret;
730 }
731
732 function get_image_sizes() {
733 $sizes = array();
734 global $_wp_additional_image_sizes;
735 foreach ( get_intermediate_image_sizes() as $s ) {
736 $crop = false;
737 if ( isset( $_wp_additional_image_sizes[$s] ) ) {
738 $width = intval( $_wp_additional_image_sizes[$s]['width'] );
739 $height = intval( $_wp_additional_image_sizes[$s]['height'] );
740 $crop = $_wp_additional_image_sizes[$s]['crop'];
741 } else {
742 $width = get_option( $s.'_size_w' );
743 $height = get_option( $s.'_size_h' );
744 $crop = get_option( $s.'_crop' );
745 }
746 $sizes[$s] = array( 'width' => $width, 'height' => $height, 'crop' => $crop );
747 }
748 return $sizes;
749 }
750
751 function clean_url_from_resolution( $url ) {
752 $pattern = '/[_-]\d+x\d+(?=\.[a-z]{3,4}$)/';
753 $url = preg_replace( $pattern, '', $url );
754 return $url;
755 }
756
757 function is_url( $url ) {
758 return ( (
759 !empty( $url ) ) &&
760 is_string( $url ) &&
761 strlen( $url ) > 4 && (
762 strtolower( substr( $url, 0, 4) ) == 'http' || $url[0] == '/'
763 )
764 );
765 }
766
767 function clean_url_from_resolution_ref( &$url ) {
768 $url = $this->clean_url_from_resolution( $url );
769 }
770
771 // From a url to the shortened and cleaned url (for example '2013/02/file.png')
772 function clean_url( $url ) {
773 // if ( is_array( $url ) ) {
774 // error_log( print_r( $url, 1 ) );
775 // }
776 $dirIndex = strpos( $url, $this->contentDir );
777 if ( empty( $url ) || $dirIndex == false )
778 return null;
779 return urldecode( substr( $url, 1 + strlen( $this->contentDir ) + $dirIndex ) );
780 }
781
782 // From a fullpath to the shortened and cleaned path (for example '2013/02/file.png')
783 // Original version by Jordy
784 // function clean_uploaded_filename( $fullpath ) {
785 // $basedir = $this->upload_folder['basedir'];
786 // $file = str_replace( $basedir, '', $fullpath );
787 // $file = str_replace( "./", "", $file );
788 // $file = trim( $file, "/" );
789 // return $file;
790 // }
791
792 // From a fullpath to the shortened and cleaned path (for example '2013/02/file.png')
793 // Faster version, more difficult to read, by Mike Meinz
794 function clean_uploaded_filename( $fullpath ) {
795 $dirIndex = strpos( $fullpath, $this->contentDir );
796 if ( $dirIndex == false ) {
797 $file = $fullpath;
798 }
799 else {
800 // Remove first part of the path leaving yyyy/mm/filename.ext
801 $file = substr( $fullpath, 1 + strlen( $this->contentDir ) + $dirIndex );
802 }
803 if ( substr( $file, 0, 2 ) == './' ) {
804 $file = substr( $file, 2 );
805 }
806 if ( substr( $file, 0, 1 ) == '/' ) {
807 $file = substr( $file, 1 );
808 }
809 return $file;
810 }
811
812 /*
813 Check if the file or the Media ID is used in the install.
814 That file or ID will be checked against the database of references created by the plugin
815 by the parsers.
816 */
817 public function reference_exists( $file, $mediaId ) {
818 global $wpdb;
819 $table = $wpdb->prefix . "mclean_refs";
820 $row = null;
821 if ( !empty( $mediaId ) ) {
822 $row = $wpdb->get_row( $wpdb->prepare( "SELECT originType FROM $table WHERE mediaId = %d", $mediaId ) );
823 if ( !empty( $row ) ) {
824 $this->last_analysis = $row->originType;
825 $this->log( "OK! Media #{$mediaId} used by {$row->originType}" );
826 return true;
827 }
828 }
829 if ( !empty( $file ) ) {
830 $row = $wpdb->get_row( $wpdb->prepare( "SELECT originType FROM $table WHERE mediaUrl = %s", $file ) );
831 if ( !empty( $row ) ) {
832 $this->last_analysis = $row->originType;
833 $this->log( "OK! File {$file} used by {$row->originType}" );
834 return true;
835 }
836 }
837 return false;
838 }
839
840 function check_media( $attachmentId, $checkOnly = false ) {
841
842 $this->last_analysis = "N/A";
843
844 // Is it an image?
845 $meta = wp_get_attachment_metadata( $attachmentId );
846 $isImage = isset( $meta, $meta['width'], $meta['height'] );
847
848 // Get the main file
849 global $wpdb;
850 $fullpath = get_attached_file( $attachmentId );
851 $mainfile = $this->clean_uploaded_filename( $fullpath );
852 $baseUp = pathinfo( $mainfile );
853 $baseUp = $baseUp['dirname'];
854 $size = 0;
855 $countfiles = 0;
856 $issue = 'NO_CONTENT';
857 if ( file_exists( $fullpath ) ) {
858
859 // Special scan: Broken only!
860 if ( !$this->check_content && !$this->check_postmeta && !$this->check_posts && !$this->check_widgets )
861 return true;
862
863 $size = filesize( $fullpath );
864
865 // Analysis
866 $this->last_analysis = "NONE";
867 $this->log( "Checking Media #{$attachmentId}: {$mainfile}" );
868 if ( $this->check_is_ignore( $mainfile, $attachmentId ) ) {
869 $this->last_analysis = "IGNORED";
870 return true;
871 }
872 if ( $this->reference_exists( $mainfile, $attachmentId ) )
873 return true;
874
875 // If images, check the other files as well
876 $countfiles = 0;
877 $sizes = $this->get_image_sizes();
878 if ( $isImage && isset( $meta['sizes'] ) ) {
879 foreach ( $meta['sizes'] as $name => $attr ) {
880 if ( isset( $attr['file'] ) ) {
881 $filepath = $this->upload_folder['basedir'];
882 $filepath = trailingslashit( $filepath ) . trailingslashit( $baseUp ) . $attr['file'];
883 if ( file_exists( $filepath ) )
884 $size += filesize( $filepath );
885 $file = $this->clean_uploaded_filename( $filepath );
886 $countfiles++;
887 // Analysis
888 $this->log( "Checking Media #{$attachmentId}: {$file}" );
889 if ( $this->reference_exists( $mainfile, $attachmentId ) )
890 return true;
891 }
892 }
893 }
894 } else {
895 $this->log( "File {$fullpath} does not exist." );
896 $issue = 'ORPHAN_MEDIA';
897 }
898
899 if ( !$checkOnly ) {
900 $table_name = $wpdb->prefix . "mclean_scan";
901 $wpdb->insert( $table_name,
902 array(
903 'time' => current_time('mysql'),
904 'type' => 1,
905 'size' => $size,
906 'path' => $mainfile . ( $countfiles > 0 ? ( " (+ " . $countfiles . " files)" ) : "" ),
907 'postId' => $attachmentId,
908 'issue' => $issue
909 )
910 );
911 }
912 return false;
913 }
914
915 // Delete all issues
916 function reset_issues( $includingIgnored = false ) {
917 global $wpdb;
918 $table_name = $wpdb->prefix . "mclean_scan";
919 if ( $includingIgnored ) {
920 $wpdb->query( "DELETE FROM $table_name WHERE deleted = 0" );
921 }
922 else {
923 $wpdb->query( "DELETE FROM $table_name WHERE ignored = 0 AND deleted = 0" );
924 }
925 if ( file_exists( plugin_dir_path( __FILE__ ) . '/media-cleaner.log' ) ) {
926 file_put_contents( plugin_dir_path( __FILE__ ) . '/media-cleaner.log', '' );
927 }
928 $table_name = $wpdb->prefix . "mclean_refs";
929 $wpdb->query("TRUNCATE $table_name");
930 }
931
932 function echo_issue( $issue ) {
933 if ( $issue == 'NO_CONTENT' ) {
934 _e( "Seems not in use.", 'media-cleaner' );
935 }
936 else if ( $issue == 'NO_MEDIA' ) {
937 _e( "Not in Media Library.", 'media-cleaner' );
938 }
939 else if ( $issue == 'ORPHAN_RETINA' ) {
940 _e( "Orphan retina.", 'media-cleaner' );
941 }
942 else if ( $issue == 'ORPHAN_WEBP' ) {
943 _e( "Orphan WebP.", 'media-cleaner' );
944 }
945 else if ( $issue == 'ORPHAN_MEDIA' ) {
946 _e( "File not found.", 'media-cleaner' );
947 }
948 else {
949 echo $issue;
950 }
951 }
952 }
953
954
955 /*
956 INSTALL / UNINSTALL
957 */
958
959 function wpmc_init( $mainfile ) {
960 //register_activation_hook( $mainfile, 'wpmc_reset' );
961 //register_deactivation_hook( $mainfile, 'wpmc_uninstall' );
962 register_uninstall_hook( $mainfile, 'wpmc_uninstall' );
963 }
964
965 function wpmc_reset () {
966 wpmc_uninstall();
967 wpmc_install();
968 $upload_folder = wp_upload_dir();
969 $basedir = $upload_folder['basedir'];
970 if ( !is_writable( $basedir ) ) {
971 echo '<div class="error"><p>' . __( 'The directory for uploads is not writable. Media Cleaner will only be able to scan.', 'media-cleaner' ) . '</p></div>';
972 }
973
974 }
975
976 function wpmc_install() {
977 global $wpdb;
978 $table_name = $wpdb->prefix . "mclean_scan";
979 $charset_collate = $wpdb->get_charset_collate();
980 $sql = "CREATE TABLE $table_name (
981 id BIGINT(20) NOT NULL AUTO_INCREMENT,
982 time DATETIME DEFAULT '0000-00-00 00:00:00' NOT NULL,
983 type TINYINT(1) NOT NULL,
984 postId BIGINT(20) NULL,
985 path TINYTEXT NULL,
986 size INT(9) NULL,
987 ignored TINYINT(1) NOT NULL DEFAULT 0,
988 deleted TINYINT(1) NOT NULL DEFAULT 0,
989 issue TINYTEXT NOT NULL,
990 PRIMARY KEY (id)
991 ) " . $charset_collate . ";" ;
992 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
993 dbDelta( $sql );
994 $sql="ALTER TABLE $table_name ADD INDEX IgnoredIndex (ignored) USING BTREE;";
995 $wpdb->query($sql);
996 $table_name = $wpdb->prefix . "mclean_refs";
997 $charset_collate = $wpdb->get_charset_collate();
998 // This key doesn't work on too many installs because of the 'Specified key was too long' issue
999 // KEY mediaLookUp (mediaId, mediaUrl)
1000 $sql = "CREATE TABLE $table_name (
1001 id BIGINT(20) NOT NULL AUTO_INCREMENT,
1002 mediaId BIGINT(20) NULL,
1003 mediaUrl VARBINARY(256) NULL,
1004 originType VARBINARY(32) NOT NULL,
1005 PRIMARY KEY (id)
1006 ) " . $charset_collate . ";";
1007 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
1008 dbDelta( $sql );
1009 }
1010
1011 function wpmc_uninstall () {
1012 global $wpdb;
1013 $table_name1 = $wpdb->prefix . "mclean_scan";
1014 $table_name2 = $wpdb->prefix . "mclean_refs";
1015 $table_name3 = $wpdb->prefix . "wpmcleaner";
1016 $sql = "DROP TABLE IF EXISTS $table_name1, $table_name2, $table_name3;";
1017 $wpdb->query( $sql );
1018 }
1019