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