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