Changeset 3381342
- Timestamp:
- 10/20/2025 01:48:45 PM (4 months ago)
- Location:
- folder-auditor
- Files:
-
- 74 added
- 9 edited
-
tags/4.7 (added)
-
tags/4.7/assets (added)
-
tags/4.7/assets/admin.js (added)
-
tags/4.7/assets/brand-banner.webp (added)
-
tags/4.7/assets/dark-icon.png (added)
-
tags/4.7/assets/email.jpg (added)
-
tags/4.7/assets/icon.png (added)
-
tags/4.7/assets/style.css (added)
-
tags/4.7/folder-auditor.php (added)
-
tags/4.7/includes (added)
-
tags/4.7/includes/bridge (added)
-
tags/4.7/includes/bridge/class-wpfa-mainwp-bridge.php (added)
-
tags/4.7/includes/bridge/unlock-relock.php (added)
-
tags/4.7/includes/class-wp-folder-auditor.php (added)
-
tags/4.7/includes/handlers (added)
-
tags/4.7/includes/handlers/handler-actions.php (added)
-
tags/4.7/includes/handlers/handler-content.php (added)
-
tags/4.7/includes/handlers/handler-htaccess.php (added)
-
tags/4.7/includes/handlers/handler-plugins.php (added)
-
tags/4.7/includes/handlers/handler-root.php (added)
-
tags/4.7/includes/handlers/handler-scanner.php (added)
-
tags/4.7/includes/handlers/handler-settings.php (added)
-
tags/4.7/includes/handlers/handler-themes.php (added)
-
tags/4.7/includes/handlers/handler-uploads.php (added)
-
tags/4.7/includes/helpers (added)
-
tags/4.7/includes/helpers/admin.php (added)
-
tags/4.7/includes/helpers/health-score (added)
-
tags/4.7/includes/helpers/health-score/health-score-display.php (added)
-
tags/4.7/includes/helpers/health-score/health-score-functions.php (added)
-
tags/4.7/includes/helpers/html-export.php (added)
-
tags/4.7/includes/helpers/lock-system (added)
-
tags/4.7/includes/helpers/lock-system/folder-locker.php (added)
-
tags/4.7/includes/helpers/lock-system/traits (added)
-
tags/4.7/includes/helpers/lock-system/traits/WPFA_Folder_Locker_Trait_Actions.php (added)
-
tags/4.7/includes/helpers/lock-system/traits/WPFA_Folder_Locker_Trait_Assets.php (added)
-
tags/4.7/includes/helpers/lock-system/traits/WPFA_Folder_Locker_Trait_Cache.php (added)
-
tags/4.7/includes/helpers/lock-system/traits/WPFA_Folder_Locker_Trait_FS.php (added)
-
tags/4.7/includes/helpers/lock-system/traits/WPFA_Folder_Locker_Trait_FSModal.php (added)
-
tags/4.7/includes/helpers/lock-system/traits/WPFA_Folder_Locker_Trait_NoticesBar.php (added)
-
tags/4.7/includes/helpers/lock-system/traits/WPFA_Folder_Locker_Trait_Request.php (added)
-
tags/4.7/includes/helpers/lock-system/traits/WPFA_Folder_Locker_Trait_Status.php (added)
-
tags/4.7/includes/helpers/lock-system/traits/WPFA_Folder_Locker_Trait_Targets.php (added)
-
tags/4.7/includes/helpers/reports (added)
-
tags/4.7/includes/helpers/reports/index.html (added)
-
tags/4.7/includes/helpers/safe-paths.php (added)
-
tags/4.7/includes/helpers/scanner (added)
-
tags/4.7/includes/helpers/scanner/patterns.php (added)
-
tags/4.7/includes/helpers/scanner/scanner.php (added)
-
tags/4.7/includes/helpers/security-headers.php (added)
-
tags/4.7/includes/helpers/user-security.php (added)
-
tags/4.7/includes/summaries (added)
-
tags/4.7/includes/summaries/summary-content.php (added)
-
tags/4.7/includes/summaries/summary-htaccess.php (added)
-
tags/4.7/includes/summaries/summary-plugins.php (added)
-
tags/4.7/includes/summaries/summary-root.php (added)
-
tags/4.7/includes/summaries/summary-themes.php (added)
-
tags/4.7/includes/summaries/summary-totals.php (added)
-
tags/4.7/includes/summaries/summary-uploads.php (added)
-
tags/4.7/includes/views (added)
-
tags/4.7/includes/views/view-audit.php (added)
-
tags/4.7/includes/views/view-content.php (added)
-
tags/4.7/includes/views/view-dashboard.php (added)
-
tags/4.7/includes/views/view-header.php (added)
-
tags/4.7/includes/views/view-htaccess-files.php (added)
-
tags/4.7/includes/views/view-html-export.php (added)
-
tags/4.7/includes/views/view-plugins.php (added)
-
tags/4.7/includes/views/view-root.php (added)
-
tags/4.7/includes/views/view-scanner.php (added)
-
tags/4.7/includes/views/view-security.php (added)
-
tags/4.7/includes/views/view-settings.php (added)
-
tags/4.7/includes/views/view-themes.php (added)
-
tags/4.7/includes/views/view-uploads.php (added)
-
tags/4.7/readme.txt (added)
-
trunk/folder-auditor.php (modified) (1 diff)
-
trunk/includes/handlers/handler-htaccess.php (modified) (1 diff)
-
trunk/includes/helpers/scanner/patterns.php (added)
-
trunk/includes/helpers/scanner/scanner.php (modified) (1 diff)
-
trunk/includes/summaries/summary-htaccess.php (modified) (1 diff)
-
trunk/includes/summaries/summary-plugins.php (modified) (1 diff)
-
trunk/includes/summaries/summary-uploads.php (modified) (1 diff)
-
trunk/includes/views/view-htaccess-files.php (modified) (1 diff)
-
trunk/includes/views/view-plugins.php (modified) (1 diff)
-
trunk/includes/views/view-uploads.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
folder-auditor/trunk/folder-auditor.php
r3380575 r3381342 3 3 * Plugin Name: Guard Dog Security & Site Lock 4 4 * Description: Helps WordPress administrators take full control of their site. It scans critical areas including the root directory, wp-content, plugins, themes, uploads, and .htaccess files to detect anything suspicious such as orphaned folders, leftover files, or hidden PHP in uploads. From the WordPress dashboard, you can safely review, download, or remove items that don’t belong, with built-in protection to ensure required resources remain untouched. In addition, Guard Dog Security lets you lock all files and folders as read-only, preventing unauthorized changes, additions, or deletions to your WordPress installation. 5 * Version: 4. 65 * Version: 4.7 6 6 * Author: WP Fix It 7 7 * Author URI: https://www.wpfixit.com -
folder-auditor/trunk/includes/handlers/handler-htaccess.php
r3374418 r3381342 1 1 <?php 2 2 3 4 3 /** 5 4 6 7 5 * .htaccess Handlers for Folder Auditor 8 6 9 10 7 * 11 8 12 13 9 * Manages ignore/include, download, and delete operations for .htaccess files. 14 10 15 16 11 * Includes bulk operations with nonce & capability checks, plus safe-path resolution. 17 12 18 19 13 * 20 14 21 22 15 * Note: This trait expects the class to provide fa_require_filesystem() 23 16 24 25 17 * (e.g., via WPFA_content_handler_functions). 26 18 27 28 19 */ 29 20 30 31 21 if ( ! defined( 'ABSPATH' ) ) { exit; } // No direct access 👮 32 22 33 34 23 trait WPFA_htaccess_handler_functions { 35 24 36 37 25 // AJAX: view a single .htaccess file's contents in a modal. 38 26 39 40 27 // Action: wp_ajax_folder_auditor_htaccess_view 41 28 42 43 29 public function handle_htaccess_view_ajax() { 44 30 45 46 31 if ( ! $this->can_manage() ) { 47 32 48 49 33 wp_send_json_error( array( 'message' => __( 'You do not have permission to do this.', 'folder-auditor' ) ), 403 ); 50 34 51 52 35 } 53 36 54 55 37 $rel = (string) ( filter_input( INPUT_POST, 'rel', FILTER_UNSAFE_RAW ) ?? '' ); 56 38 57 58 39 $rel = sanitize_text_field( $rel ); 59 40 60 61 41 // Row-specific nonce: 'fa_htaccess_view_' . md5($rel) 62 42 63 64 43 if ( ! check_ajax_referer( 'fa_htaccess_view_' . md5( $rel ), '_wpnonce', false ) ) { 65 44 66 67 45 wp_send_json_error( array( 'message' => __( 'Invalid or expired nonce.', 'folder-auditor' ) ), 400 ); 68 46 69 70 47 } 71 48 72 73 49 $abs = $this->get_safe_htaccess_file( $rel ); 74 50 75 76 51 if ( ! $abs ) { 77 52 78 79 53 wp_send_json_error( array( 'message' => __( 'Invalid file.', 'folder-auditor' ) ), 400 ); 80 54 81 82 55 } 83 56 84 85 57 // Read via WP_Filesystem if available 86 58 87 88 59 $wp_filesystem = $this->fa_require_filesystem(); 89 60 90 91 61 $contents = false; 92 62 93 94 63 if ( $wp_filesystem && method_exists( $wp_filesystem, 'get_contents' ) ) { 95 64 96 97 65 $contents = $wp_filesystem->get_contents( $abs ); 98 66 99 100 67 } else { 101 68 102 103 69 // Fallback 104 70 105 106 71 $contents = @file_get_contents( $abs ); 107 72 108 109 73 } 110 74 111 112 75 if ( false === $contents ) { 113 76 114 115 77 wp_send_json_error( array( 'message' => __( 'Unable to read file (permissions?).', 'folder-auditor' ) ), 500 ); 116 78 117 118 79 } 119 80 120 121 81 // Safety limits: truncate large files (e.g., 200KB) 122 82 123 124 83 $max_bytes = 200 * 1024; 125 84 126 127 85 $truncated = false; 128 86 129 130 87 if ( strlen( $contents ) > $max_bytes ) { 131 88 132 133 89 $contents = substr( $contents, 0, $max_bytes ); 134 90 135 136 91 $truncated = true; 137 92 138 139 93 } 140 94 141 142 95 // Return plain text (do not escape here; consumer will display in <pre>) 143 96 144 145 97 wp_send_json_success( array( 146 98 147 148 99 'rel' => $rel, 149 100 150 151 101 'size' => (int) @filesize( $abs ), 152 102 153 154 103 'mtime' => (int) @filemtime( $abs ), 155 104 156 157 105 'truncated' => $truncated, 158 106 159 160 107 'content' => $contents, 161 108 162 163 109 ) ); 164 110 165 166 111 } 167 112 168 169 113 170 114 171 172 115 /** 173 116 174 175 117 * Try to delete a single .htaccess file robustly: 176 118 177 178 119 * 1) Attempt delete 179 120 180 181 121 * 2) If fail: chmod file, try again 182 122 183 184 123 * 3) If still fail: chmod parent dir, try again 185 124 186 187 125 * 188 126 189 190 127 * @param string $abs_file Normalized absolute path to .htaccess 191 128 192 193 129 * @param WP_Filesystem_Base $fs Filesystem API (Direct) 194 130 195 196 131 * @return bool 197 132 198 199 133 */ 200 134 201 202 135 private function try_delete_htaccess( string $abs_file, $fs ) : bool { 203 136 204 205 137 $ok = true; 206 138 207 208 139 // 1) Straight delete 209 140 210 211 141 if ( method_exists( $fs, 'delete' ) ) { 212 142 213 214 143 $ok = $fs->delete( $abs_file, false, 'f' ); 215 144 216 217 145 } else { 218 146 219 220 147 $ok = (bool) wp_delete_file( $abs_file ); 221 148 222 223 149 } 224 150 225 226 151 if ( $ok ) { return true; } 227 152 228 229 153 // 2) Make file writable and retry 230 154 231 232 155 if ( method_exists( $fs, 'chmod' ) ) { @ $fs->chmod( $abs_file, 0644 ); } 233 156 234 235 157 if ( method_exists( $fs, 'delete' ) ) { 236 158 237 238 159 $ok = $fs->delete( $abs_file, false, 'f' ); 239 160 240 241 161 } else { 242 162 243 244 163 $ok = (bool) wp_delete_file( $abs_file ); 245 164 246 247 165 } 248 166 249 250 167 if ( $ok ) { return true; } 251 168 252 253 169 // 3) Loosen parent dir perms (read/execute for owner at least), retry 254 170 255 256 171 $parent = wp_normalize_path( dirname( $abs_file ) ); 257 172 258 259 173 if ( $parent && is_dir( $parent ) && method_exists( $fs, 'chmod' ) ) { 260 174 261 262 175 @ $fs->chmod( $parent, 0755 ); 263 176 264 265 177 } 266 178 267 268 179 if ( method_exists( $fs, 'delete' ) ) { 269 180 270 271 181 $ok = $fs->delete( $abs_file, false, 'f' ); 272 182 273 274 183 } else { 275 184 276 277 185 $ok = (bool) wp_delete_file( $abs_file ); 278 186 279 280 187 } 281 188 282 283 189 return (bool) $ok; 284 190 285 286 191 } 287 192 288 289 193 /** 290 194 291 292 195 * Ignore all discovered .htaccess files (adds every relative path to the ignore set). 293 196 294 295 197 * 296 198 297 298 199 * Nonce: 'fa_htaccess_ignore_all' 299 200 300 301 201 */ 302 202 303 304 203 public function handle_htaccess_ignore_all() { 305 204 306 307 205 if ( ! $this->can_manage() ) { 308 206 309 310 207 wp_die( esc_html__( 'You do not have permission to do this.', 'folder-auditor' ) ); 311 208 312 313 } 314 209 } 315 210 316 211 check_admin_referer( 'fa_htaccess_ignore_all' ); 317 212 318 319 213 $base = realpath( ABSPATH ); 320 214 321 322 215 if ( ! $base || ! is_dir( $base ) ) { 323 216 324 325 217 wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&tab=htaccess' ) ); 326 218 327 328 219 exit; 329 220 330 331 } 332 221 } 333 222 334 223 // 1) Scan absolute paths 335 224 336 337 225 $all_abs = $this->scan_htaccess_for_bulk( $base ); 338 226 339 340 227 // 2) Convert to view-friendly relative paths 341 228 342 343 229 $added = 0; 344 230 345 346 231 $total = 0; 347 232 348 349 233 $sets = $this->get_ignored(); // Full ignore sets 350 234 351 352 235 if ( ! isset( $sets['htaccess'] ) || ! is_array( $sets['htaccess'] ) ) { 353 236 354 355 237 $sets['htaccess'] = []; 356 238 357 358 } 359 239 } 360 240 361 241 foreach ( (array) $all_abs as $abs ) { 362 242 363 364 243 $total++; 365 244 366 367 245 // Ensure normalized/contained, then compute rel 368 246 369 370 247 $abs_norm = wp_normalize_path( realpath( $abs ) ?: $abs ); 371 248 372 373 249 if ( strpos( $abs_norm, wp_normalize_path( $base ) ) !== 0 ) { continue; } 374 250 375 376 251 $rel = ltrim( str_replace( wp_normalize_path( $base ), '', $abs_norm ), '/\\' ); 377 252 378 379 253 if ( $rel === '' ) { continue; } 380 254 381 382 255 if ( empty( $sets['htaccess'][ $rel ] ) ) { 383 256 384 385 257 $sets['htaccess'][ $rel ] = true; 386 258 387 388 259 $added++; 389 260 390 391 261 } 392 262 393 394 263 } 395 264 396 397 265 // 3) Persist 398 266 399 400 267 $this->save_ignored( $sets ); 401 268 402 403 269 // 4) Back to the tab with a summary 404 270 405 406 271 $redirect = add_query_arg( 407 272 408 409 273 [ 410 274 411 412 275 'page' => self::MENU_SLUG, 413 276 414 415 277 'tab' => 'htaccess', 416 278 417 418 279 'fa_htaccess_ignored_all' => (int) $added, 419 280 420 421 281 'fa_htaccess_ignored_tot' => (int) $total, 422 282 423 424 283 ], 425 284 426 427 285 admin_url( 'admin.php' ) 428 286 429 430 287 ); 431 288 432 433 289 wp_safe_redirect( $redirect ); 434 290 435 436 291 exit; 437 292 438 439 293 } 440 294 441 442 295 /** 443 296 444 445 297 * Bulk action handler for .htaccess rows (delete | ignore | include). 446 298 447 448 299 * 449 300 450 451 301 * Nonce: 'fa_htaccess_bulk' 452 302 453 454 303 * POST: bulk[row_key]={delete|ignore|include}, rel[row_key]={relative path} 455 304 456 457 305 */ 458 306 459 460 307 public function handle_htaccess_bulk() { 461 308 462 463 309 if ( ! $this->can_manage() ) { 464 310 465 466 311 wp_die( esc_html__( 'You do not have permission to do this.', 'folder-auditor' ) ); 467 312 468 469 } 470 313 } 471 314 472 315 // CSRF (do not touch $_POST['_wpnonce'] directly) 473 316 474 475 317 check_admin_referer( 'fa_htaccess_bulk' ); 476 318 477 478 319 // Fetch arrays from POST without touching superglobals (avoids WPCS warnings). 479 320 480 481 321 $bulk_raw = (array) ( filter_input( INPUT_POST, 'bulk', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ) ?: array() ); 482 322 483 484 323 $rels_raw = (array) ( filter_input( INPUT_POST, 'rel', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ) ?: array() ); 485 324 486 487 325 // Sanitize into new arrays (both keys and values). 488 326 489 490 327 $bulk = array(); 491 328 492 493 329 foreach ( $bulk_raw as $k => $v ) { 494 330 495 496 331 $bulk[ sanitize_text_field( (string) $k ) ] = sanitize_text_field( (string) $v ); 497 332 498 499 333 } 500 334 501 502 335 $rels = array(); 503 336 504 505 337 foreach ( $rels_raw as $k => $v ) { 506 338 507 508 339 $rels[ sanitize_text_field( (string) $k ) ] = sanitize_text_field( (string) $v ); 509 340 510 511 341 } 512 342 513 514 343 $deleted = 0; 515 344 516 517 345 $ignored_added = 0; 518 346 519 520 347 $ignored_removed = 0; 521 348 522 523 349 $wp_filesystem = $this->fa_require_filesystem(); 524 350 525 526 351 foreach ( $rels as $row_key => $rel ) { 527 352 528 529 353 $choice = isset( $bulk[ $row_key ] ) ? $bulk[ $row_key ] : ''; 530 354 531 532 355 if ( ! in_array( $choice, array( 'delete', 'ignore', 'include' ), true ) ) { 533 356 534 535 357 continue; // skip unknown/no-op 536 358 537 538 359 } 539 360 540 541 361 if ( 'delete' === $choice ) { 542 362 543 544 363 $abs = $this->get_safe_htaccess_file( $rel ); 545 364 546 547 365 if ( $abs && is_file( $abs ) && basename( $abs ) === '.htaccess' ) { 548 366 549 550 367 // Prefer Filesystem API; fallback to wp_delete_file (no raw unlink/chmod) 551 368 552 553 369 $ok = true; 554 370 555 556 371 if ( method_exists( $wp_filesystem, 'delete' ) ) { 557 372 558 559 373 if ( method_exists( $wp_filesystem, 'chmod' ) ) { 560 374 561 562 375 @$wp_filesystem->chmod( $abs, FS_CHMOD_FILE ); 563 376 564 565 377 } 566 378 567 568 379 $ok = $wp_filesystem->delete( $abs, false, 'f' ); 569 380 570 571 381 } else { 572 382 573 574 383 $ok = (bool) wp_delete_file( $abs ); 575 384 576 577 385 } 578 386 579 580 387 if ( $ok ) { 581 388 582 583 389 $deleted++; 584 390 585 586 391 // Remove from ignore set if it was tracked 587 392 588 589 393 $this->ignore_remove( 'htaccess', $rel ); 590 394 591 592 395 } 593 396 594 595 397 } 596 398 597 598 399 } elseif ( 'ignore' === $choice ) { 599 400 600 601 401 $this->ignore_add( 'htaccess', $rel ); 602 402 603 604 403 $ignored_added++; 605 404 606 607 405 } elseif ( 'include' === $choice ) { 608 406 609 610 407 $this->ignore_remove( 'htaccess', $rel ); 611 408 612 613 409 $ignored_removed++; 614 410 615 616 411 } 617 412 618 619 } 620 413 } 621 414 622 415 // UX: bounce back with a summary; preserve pagination if present 623 416 624 625 417 $args = array( 626 418 627 628 419 'page' => self::MENU_SLUG, 629 420 630 631 421 'tab' => 'htaccess', 632 422 633 634 423 'fa_bulk_done' => 1, 635 424 636 637 425 'fa_deleted' => $deleted, 638 426 639 640 427 'fa_ignored' => $ignored_added, 641 428 642 643 429 'fa_included' => $ignored_removed, 644 430 645 646 431 ); 647 432 648 649 433 if ( isset( $_GET['fa_paged'] ) ) { 650 434 651 652 435 $args['fa_paged'] = (int) $_GET['fa_paged']; 653 436 654 655 } 656 437 } 657 438 658 439 wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) ); 659 440 660 661 441 exit; 662 442 663 664 443 } 665 444 666 667 445 /** 668 446 669 670 447 * Count .htaccess files under ABSPATH (skips common heavy/vendor dirs). 671 448 672 673 449 */ 674 450 675 676 451 private function count_htaccess_files() : int { 677 452 678 679 453 $base = realpath( ABSPATH ); 680 454 681 682 455 if ( ! $base || ! is_dir( $base ) ) { return 0; } 683 456 684 685 457 $skip = array( '.git', '.svn', 'node_modules', 'vendor', 'cache', 'backups', 'backup' ); 686 458 687 688 459 $count = 0; 689 460 690 691 461 try { 692 462 693 694 463 $it = new RecursiveIteratorIterator( 695 464 696 697 465 new RecursiveDirectoryIterator( $base, FilesystemIterator::SKIP_DOTS ), 698 466 699 700 467 RecursiveIteratorIterator::SELF_FIRST 701 468 702 703 469 ); 704 470 705 706 471 foreach ( $it as $fi ) { 707 472 708 709 473 if ( $fi->isDir() ) { 710 474 711 712 475 if ( in_array( $fi->getBasename(), $skip, true ) ) { $it->next(); continue; } 713 476 714 715 477 } elseif ( $fi->isFile() && $fi->getBasename() === '.htaccess' ) { 716 478 717 718 479 $real = realpath( $fi->getPathname() ); 719 480 720 721 481 if ( $real && strpos( $real, $base ) === 0 ) { $count++; } 722 482 723 724 483 } 725 484 726 727 485 } 728 486 729 730 487 } catch ( \Exception $e ) { /* ignore */ } 731 488 732 733 489 return $count; 734 490 735 736 491 } 737 738 739 492 /** 740 493 741 742 * Build a summary for the .htaccess metric card (raw vs effective, honoring "ignored"). 743 494 * Delete ALL .htaccess files under ABSPATH (2-pass attempt for robustness). 744 495 745 496 * 746 497 747 748 * @return array { 749 750 751 * @type string key 752 753 754 * @type string label 755 756 757 * @type string tab 758 759 760 * @type int count Raw total 761 762 763 * @type int count_effective Total minus ignored 764 765 766 * } 767 498 * Nonce: 'fa_htaccess_delete_all' 768 499 769 500 */ 770 501 771 772 private function summary_htaccess() : array { 773 774 775 $base = realpath( ABSPATH ); 776 777 778 $all_abs = []; 779 780 781 $all_rel = []; 782 783 784 if ( $base && is_dir( $base ) && is_readable( $base ) ) { 785 786 787 try { 788 789 790 $it = new RecursiveIteratorIterator( 791 792 793 new RecursiveDirectoryIterator( $base, FilesystemIterator::SKIP_DOTS ), 794 795 796 RecursiveIteratorIterator::SELF_FIRST 797 798 799 ); 800 801 802 $skip = [ '.git', '.svn', 'node_modules', 'vendor', 'cache', 'backups', 'backup' ]; 803 804 805 foreach ( $it as $fi ) { 806 807 808 if ( $fi->isDir() ) { 809 810 811 if ( in_array( $fi->getBasename(), $skip, true ) ) { $it->next(); } 812 813 814 continue; 815 816 817 } 818 819 820 if ( ! $fi->isFile() ) { continue; } 821 822 823 if ( $fi->getBasename() !== '.htaccess' ) { continue; } 824 825 826 $abs = realpath( $fi->getPathname() ); 827 828 829 if ( ! $abs ) { continue; } 830 831 832 if ( strpos( $abs, $base ) !== 0 ) { continue; } 833 834 835 $all_abs[] = $abs; 836 837 838 $all_rel[] = ltrim( str_replace( $base, '', $abs ), '/\\' ); // same rel format as view 839 502 public function handle_htaccess_delete_all() { 503 504 if ( ! $this->can_manage() ) { wp_die( esc_html__( 'You do not have permission to do this.', 'folder-auditor' ) ); } 505 506 check_admin_referer( 'fa_htaccess_delete_all' ); 507 508 if ( function_exists( 'ignore_user_abort' ) ) { ignore_user_abort( true ); } 509 510 if ( function_exists( 'set_time_limit' ) ) { @set_time_limit( 0 ); } // phpcs:ignore 511 512 if ( function_exists( 'ini_set' ) ) { @ini_set( 'max_execution_time', '0' ); } // phpcs:ignore 513 514 $abs_root = realpath( ABSPATH ); 515 516 if ( ! $abs_root || ! is_dir( $abs_root ) || ! is_readable( $abs_root ) ) { 517 518 wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&tab=htaccess' ) ); 519 520 exit; 521 522 } 523 524 $root_norm = wp_normalize_path( $abs_root ); 525 526 $wp_filesystem = $this->fa_require_filesystem(); 527 528 $scan = function() use ( $abs_root ) { return $this->scan_htaccess_for_bulk( $abs_root ); }; 529 530 $total_deleted = 0; 531 532 $total_failed = 0; 533 534 // ---- progress-driven loop with time budget ---- 535 536 $deadline_sec = 30; // adjust if you want 537 538 $deadline_at = microtime( true ) + $deadline_sec; 539 540 $stable_scans = 0; // need 2 consecutive stable scans to stop 541 542 $last_left = null; 543 544 while ( microtime( true ) < $deadline_at ) { 545 546 $files = $scan(); 547 548 $left = count( $files ); 549 550 // Already stable? 551 552 if ( $left === 0 ) { break; } 553 554 if ( $last_left !== null && $left === $last_left ) { 555 556 $stable_scans++; 557 558 if ( $stable_scans >= 2 ) { break; } // nothing is changing anymore 559 560 } else { 561 562 $stable_scans = 0; 563 564 $last_left = $left; 565 566 } 567 568 // Depth-first: longer paths first 569 570 usort( $files, static function( $a, $b ){ return strlen( $b ) <=> strlen( $a ); }); 571 572 $deleted_this_pass = 0; 573 574 foreach ( $files as $abs ) { 575 576 $real = realpath( $abs ); 577 578 if ( ! $real ) { $total_failed++; continue; } 579 580 $real_norm = wp_normalize_path( $real ); 581 582 if ( strpos( $real_norm, $root_norm ) !== 0 || basename( $real_norm ) !== '.htaccess' || ! is_file( $real_norm ) ) { 583 584 $total_failed++; continue; 585 586 } 587 588 $ok = $this->try_delete_htaccess( $real_norm, $wp_filesystem ); 589 590 if ( $ok ) { 591 592 $deleted_this_pass++; $total_deleted++; 593 594 if ( method_exists( $this, 'ignore_remove' ) ) { 595 596 $rel = ltrim( str_replace( $root_norm, '', $real_norm ), '/\\' ); 597 598 $this->ignore_remove( 'htaccess', $rel ); 840 599 841 600 } 842 601 843 844 } catch ( \Exception $e ) { /* ignore */ } 845 846 847 } 848 849 850 $raw_count = count( $all_rel ); 851 852 853 // Apply ignore set (bucket: 'htaccess'; keys are REL paths like "wp-content/uploads/.htaccess") 854 855 856 $ignored = method_exists( $this, 'get_ignored' ) ? (array) $this->get_ignored() : []; 857 858 859 $ignored_keys = array_keys( (array) ( $ignored['htaccess'] ?? [] ) ); 860 861 862 $ignored_in_set = count( array_intersect( $all_rel, $ignored_keys ) ); 863 864 865 $count_effective = max( 0, $raw_count - $ignored_in_set ); 866 867 868 return [ 869 870 871 'key' => 'htaccess', 872 873 874 'label' => __( '.htaccess Files', 'folder-auditor' ), 875 876 877 'tab' => 'htaccess', 878 879 880 'count' => (int) $raw_count, // raw total for display 881 882 883 'count_effective' => (int) $count_effective, // excludes ignored (for health score) 884 885 886 ]; 887 602 } else { 603 604 $total_failed++; 605 606 } 607 608 } 609 610 // Make sure PHP/FS caches are flushed and give the FS a breath 611 612 if ( function_exists( 'clearstatcache' ) ) { clearstatcache( true ); } 613 614 usleep( 150000 ); // 150ms; helps on NFS/slow FS 615 616 // If nothing deleted in this pass, mark stability (second check happens at loop head) 617 618 if ( $deleted_this_pass === 0 ) { /* no-op - stability logic above will catch this */ } 888 619 889 620 } 890 621 622 $remaining = count( $scan() ); 623 624 $args = array( 625 626 'page' => self::MENU_SLUG, 627 628 'tab' => 'htaccess', 629 630 'fa_htaccess_bulk' => (int) $total_deleted, 631 632 'fa_htaccess_fail' => (int) $total_failed, 633 634 'fa_htaccess_left' => (int) $remaining, 635 636 ); 637 638 wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) ); 639 640 exit; 641 642 } 891 643 892 644 /** 893 645 894 895 * Delete ALL .htaccess files under ABSPATH (2-pass attempt for robustness). 896 646 * Utility: scan for .htaccess files under a root (absolute paths). 897 647 898 648 * 899 649 900 901 * Nonce: 'fa_htaccess_delete_all' 902 650 * @param string $root 651 652 * @return string[] absolute paths 903 653 904 654 */ 905 655 906 907 public function handle_htaccess_delete_all() { 908 909 910 if ( ! $this->can_manage() ) { wp_die( esc_html__( 'You do not have permission to do this.', 'folder-auditor' ) ); } 911 912 913 check_admin_referer( 'fa_htaccess_delete_all' ); 914 915 916 if ( function_exists( 'ignore_user_abort' ) ) { ignore_user_abort( true ); } 917 918 919 if ( function_exists( 'set_time_limit' ) ) { @set_time_limit( 0 ); } // phpcs:ignore 920 921 922 if ( function_exists( 'ini_set' ) ) { @ini_set( 'max_execution_time', '0' ); } // phpcs:ignore 923 924 925 $abs_root = realpath( ABSPATH ); 926 927 928 if ( ! $abs_root || ! is_dir( $abs_root ) || ! is_readable( $abs_root ) ) { 929 930 931 wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&tab=htaccess' ) ); 932 656 private function scan_htaccess_for_bulk( $root ) { 657 658 $found = []; 659 660 if ( ! $root || ! is_dir( $root ) ) { return $found; } 661 662 $root_real = realpath( $root ); 663 664 if ( ! $root_real ) { return $found; } 665 666 $root_norm = wp_normalize_path( $root_real ); 667 668 // Keep this list in sync with other scanners 669 670 $skip = array( '.git', '.svn', 'node_modules', 'vendor', 'cache', 'backups', 'backup' ); 671 672 try { 673 674 $dirFlags = FilesystemIterator::SKIP_DOTS; 675 676 // Prevent UnexpectedValueException from aborting traversal on unreadable dirs 677 678 if ( defined('RecursiveDirectoryIterator::CATCH_GET_CHILD') ) { 679 680 $dirFlags |= RecursiveDirectoryIterator::CATCH_GET_CHILD; 681 682 } 683 684 $it = new RecursiveIteratorIterator( 685 686 new RecursiveDirectoryIterator( $root_real, $dirFlags ), 687 688 RecursiveIteratorIterator::SELF_FIRST 689 690 ); 691 692 foreach ( $it as $fi ) { 693 694 // Skip heavy/vendor dirs early 695 696 if ( $fi->isDir() ) { 697 698 if ( in_array( $fi->getBasename(), $skip, true ) ) { 699 700 $it->next(); 701 702 } 703 704 continue; 705 706 } 707 708 if ( ! $fi->isFile() ) { continue; } 709 710 if ( $fi->getBasename() !== '.htaccess' ) { continue; } 711 712 // Normalize & safety-check (avoid symlink escapes) 713 714 $real = realpath( $fi->getPathname() ); 715 716 if ( ! $real ) { continue; } 717 718 $real_norm = wp_normalize_path( $real ); 719 720 if ( strpos( $real_norm, $root_norm ) !== 0 ) { continue; } 721 722 $found[] = $real_norm; 723 724 } 725 726 } catch ( \Exception $e ) { 727 728 // Swallow to keep bulk op resilient 729 730 } 731 732 return $found; 733 734 } 735 736 /** 737 738 * Resolve a safe absolute path for a relative path under ABSPATH, 739 740 * restricted strictly to files literally named ".htaccess". 741 742 * 743 744 * @param string $rel Relative path (e.g., 'wp-content/uploads/.htaccess') 745 746 * @return string|false Absolute path if valid and safe, false otherwise 747 748 */ 749 750 private function get_safe_htaccess_file( string $rel ) { 751 752 $rel = wp_normalize_path( sanitize_text_field( $rel ) ); 753 754 // Deny traversal / absolute paths 755 756 if ( $rel === '' || strpos( $rel, '..' ) !== false || preg_match( '#^[\\/]|[a-zA-Z]:[\\\\/]#', $rel ) ) { 757 758 return false; 759 760 } 761 762 $base = wp_normalize_path( realpath( ABSPATH ) ); 763 764 if ( ! $base ) { return false; } 765 766 $abs = wp_normalize_path( $base . '/' . ltrim( $rel, '/\\' ) ); 767 768 $real = realpath( $abs ); 769 770 if ( ! $real ) { return false; } 771 772 $real = wp_normalize_path( $real ); 773 774 // Must live under ABSPATH and be literally ".htaccess" 775 776 if ( strpos( $real, $base ) !== 0 || basename( $real ) !== '.htaccess' || ! is_file( $real ) ) { 777 778 return false; 779 780 } 781 782 return $real; 783 784 } 785 786 /** 787 788 * Download a single .htaccess file as a tiny ZIP (named "htaccess.zip"). 789 790 * 791 792 * Nonce: 'fa_htaccess_download_' . md5($rel) 793 794 * POST: rel 795 796 */ 797 798 public function handle_htaccess_download() { 799 800 if ( ! $this->can_manage() ) { 801 802 wp_die( esc_html__( 'You do not have permission to do this.', 'folder-auditor' ) ); 803 804 } 805 806 $rel = (string) ( filter_input( INPUT_POST, 'rel', FILTER_UNSAFE_RAW ) ?? '' ); 807 808 $rel = sanitize_text_field( $rel ); 809 810 check_admin_referer( 'fa_htaccess_download_' . md5( $rel ) ); 811 812 $abs = $this->get_safe_htaccess_file( $rel ); 813 814 if ( ! $abs ) { 815 816 wp_die( esc_html__( 'Invalid file.', 'folder-auditor' ) ); 817 818 } 819 820 if ( ! class_exists( 'ZipArchive' ) ) { 821 822 wp_die( esc_html__( 'ZipArchive is not available on this server.', 'folder-auditor' ) ); 823 824 } 825 826 // Build one-file ZIP (keeps filename exactly ".htaccess" inside) 827 828 $zip_file = wp_tempnam( 'fa-htaccess-' ) . '.zip'; 829 830 $zip = new ZipArchive(); 831 832 if ( true !== $zip->open( $zip_file, ZipArchive::CREATE | ZipArchive::OVERWRITE ) ) { 833 834 // Safe cleanup via WP helper 835 836 wp_delete_file( $zip_file ); 837 838 wp_die( esc_html__( 'Failed to build ZIP.', 'folder-auditor' ) ); 839 840 } 841 842 $zip->addFile( $abs, '.htaccess' ); 843 844 $zip->close(); 845 846 if ( function_exists( 'ob_get_level' ) ) { 847 848 while ( ob_get_level() ) { ob_end_clean(); } 849 850 } 851 852 // Use Filesystem API to read + echo; avoid readfile() 853 854 $wp_filesystem = $this->fa_require_filesystem(); 855 856 $size = is_file( $zip_file ) ? (int) filesize( $zip_file ) : 0; 857 858 $contents = $wp_filesystem->get_contents( $zip_file ); 859 860 nocache_headers(); 861 862 header( 'Content-Type: application/zip' ); 863 864 if ( $size > 0 ) { 865 866 header( 'Content-Length: ' . $size ); 867 868 } 869 870 header( 'Content-Disposition: attachment; filename="htaccess.zip"; filename*=UTF-8\'\'htaccess.zip' ); 871 872 if ( false !== $contents ) { 873 874 echo $contents; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- sending raw file bytes 875 876 } 877 878 // Remove temp file via WP helper (no unlink()). 879 880 wp_delete_file( $zip_file ); 933 881 934 882 exit; 935 883 936 937 884 } 938 885 939 940 $root_norm = wp_normalize_path( $abs_root ); 941 942 943 $wp_filesystem = $this->fa_require_filesystem(); 944 945 946 $scan = function() use ( $abs_root ) { return $this->scan_htaccess_for_bulk( $abs_root ); }; 947 948 949 $total_deleted = 0; 950 951 952 $total_failed = 0; 953 954 955 // ---- progress-driven loop with time budget ---- 956 957 958 $deadline_sec = 30; // adjust if you want 959 960 961 $deadline_at = microtime( true ) + $deadline_sec; 962 963 964 $stable_scans = 0; // need 2 consecutive stable scans to stop 965 966 967 $last_left = null; 968 969 970 while ( microtime( true ) < $deadline_at ) { 971 972 973 $files = $scan(); 974 975 976 $left = count( $files ); 977 978 979 // Already stable? 980 981 982 if ( $left === 0 ) { break; } 983 984 985 if ( $last_left !== null && $left === $last_left ) { 986 987 988 $stable_scans++; 989 990 991 if ( $stable_scans >= 2 ) { break; } // nothing is changing anymore 992 886 /** 887 888 * Delete a single .htaccess file. 889 890 * 891 892 * Nonce: 'fa_htaccess_delete_' . md5($rel) 893 894 * POST: rel 895 896 */ 897 898 public function handle_htaccess_delete() { 899 900 if ( ! $this->can_manage() ) { 901 902 wp_die( esc_html__( 'You do not have permission to do this.', 'folder-auditor' ) ); 903 904 } 905 906 $rel = (string) ( filter_input( INPUT_POST, 'rel', FILTER_UNSAFE_RAW ) ?? '' ); 907 908 $rel = sanitize_text_field( $rel ); 909 910 check_admin_referer( 'fa_htaccess_delete_' . md5( $rel ) ); 911 912 $abs = $this->get_safe_htaccess_file( $rel ); 913 914 if ( ! $abs ) { 915 916 wp_die( esc_html__( 'Invalid file.', 'folder-auditor' ) ); 917 918 } 919 920 $wp_filesystem = $this->fa_require_filesystem(); 921 922 // Prefer FS API; fallback to wp_delete_file 923 924 $ok = true; 925 926 if ( method_exists( $wp_filesystem, 'delete' ) ) { 927 928 $ok = $wp_filesystem->delete( $abs, false, 'f' ); 993 929 994 930 } else { 995 931 996 997 $stable_scans = 0; 998 999 1000 $last_left = $left; 1001 1002 1003 } 1004 1005 1006 // Depth-first: longer paths first 1007 1008 1009 usort( $files, static function( $a, $b ){ return strlen( $b ) <=> strlen( $a ); }); 1010 1011 1012 $deleted_this_pass = 0; 1013 1014 1015 foreach ( $files as $abs ) { 1016 1017 1018 $real = realpath( $abs ); 1019 1020 1021 if ( ! $real ) { $total_failed++; continue; } 1022 1023 1024 $real_norm = wp_normalize_path( $real ); 1025 1026 1027 if ( strpos( $real_norm, $root_norm ) !== 0 || basename( $real_norm ) !== '.htaccess' || ! is_file( $real_norm ) ) { 1028 1029 1030 $total_failed++; continue; 1031 1032 1033 } 1034 1035 1036 $ok = $this->try_delete_htaccess( $real_norm, $wp_filesystem ); 1037 1038 1039 if ( $ok ) { 1040 1041 1042 $deleted_this_pass++; $total_deleted++; 1043 1044 1045 if ( method_exists( $this, 'ignore_remove' ) ) { 1046 1047 1048 $rel = ltrim( str_replace( $root_norm, '', $real_norm ), '/\\' ); 1049 1050 1051 $this->ignore_remove( 'htaccess', $rel ); 1052 1053 1054 } 1055 1056 1057 } else { 1058 1059 1060 $total_failed++; 1061 1062 1063 } 1064 1065 1066 } 1067 1068 1069 // Make sure PHP/FS caches are flushed and give the FS a breath 1070 1071 1072 if ( function_exists( 'clearstatcache' ) ) { clearstatcache( true ); } 1073 1074 1075 usleep( 150000 ); // 150ms; helps on NFS/slow FS 1076 1077 1078 // If nothing deleted in this pass, mark stability (second check happens at loop head) 1079 1080 1081 if ( $deleted_this_pass === 0 ) { /* no-op - stability logic above will catch this */ } 1082 932 $ok = (bool) wp_delete_file( $abs ); 933 934 } 935 936 if ( ! $ok ) { 937 938 wp_die( esc_html__( 'Failed to delete the file (permissions?).', 'folder-auditor' ) ); 939 940 } 941 942 // Back to the tab with a success context 943 944 $redirect = add_query_arg( 945 946 array( 947 948 'page' => self::MENU_SLUG, 949 950 'tab' => 'htaccess', 951 952 'fa_htaccess_msg' => rawurlencode( $rel ), 953 954 ), 955 956 admin_url( 'admin.php' ) 957 958 ); 959 960 wp_safe_redirect( $redirect ); 961 962 exit; 1083 963 1084 964 } 1085 965 1086 1087 $remaining = count( $scan() );1088 1089 1090 $args = array(1091 1092 1093 'page' => self::MENU_SLUG,1094 1095 1096 'tab' => 'htaccess',1097 1098 1099 'fa_htaccess_bulk' => (int) $total_deleted,1100 1101 1102 'fa_htaccess_fail' => (int) $total_failed,1103 1104 1105 'fa_htaccess_left' => (int) $remaining,1106 1107 1108 );1109 1110 1111 wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) );1112 1113 1114 exit;1115 1116 1117 966 } 1118 1119 1120 /**1121 1122 1123 * Utility: scan for .htaccess files under a root (absolute paths).1124 1125 1126 *1127 1128 1129 * @param string $root1130 1131 1132 * @return string[] absolute paths1133 1134 1135 */1136 1137 1138 private function scan_htaccess_for_bulk( $root ) {1139 1140 1141 $found = [];1142 1143 1144 if ( ! $root || ! is_dir( $root ) ) { return $found; }1145 1146 1147 $root_real = realpath( $root );1148 1149 1150 if ( ! $root_real ) { return $found; }1151 1152 1153 $root_norm = wp_normalize_path( $root_real );1154 1155 1156 // Keep this list in sync with other scanners1157 1158 1159 $skip = array( '.git', '.svn', 'node_modules', 'vendor', 'cache', 'backups', 'backup' );1160 1161 1162 try {1163 1164 1165 $dirFlags = FilesystemIterator::SKIP_DOTS;1166 1167 1168 // Prevent UnexpectedValueException from aborting traversal on unreadable dirs1169 1170 1171 if ( defined('RecursiveDirectoryIterator::CATCH_GET_CHILD') ) {1172 1173 1174 $dirFlags |= RecursiveDirectoryIterator::CATCH_GET_CHILD;1175 1176 1177 }1178 1179 1180 $it = new RecursiveIteratorIterator(1181 1182 1183 new RecursiveDirectoryIterator( $root_real, $dirFlags ),1184 1185 1186 RecursiveIteratorIterator::SELF_FIRST1187 1188 1189 );1190 1191 1192 foreach ( $it as $fi ) {1193 1194 1195 // Skip heavy/vendor dirs early1196 1197 1198 if ( $fi->isDir() ) {1199 1200 1201 if ( in_array( $fi->getBasename(), $skip, true ) ) {1202 1203 1204 $it->next();1205 1206 1207 }1208 1209 1210 continue;1211 1212 1213 }1214 1215 1216 if ( ! $fi->isFile() ) { continue; }1217 1218 1219 if ( $fi->getBasename() !== '.htaccess' ) { continue; }1220 1221 1222 // Normalize & safety-check (avoid symlink escapes)1223 1224 1225 $real = realpath( $fi->getPathname() );1226 1227 1228 if ( ! $real ) { continue; }1229 1230 1231 $real_norm = wp_normalize_path( $real );1232 1233 1234 if ( strpos( $real_norm, $root_norm ) !== 0 ) { continue; }1235 1236 1237 $found[] = $real_norm;1238 1239 1240 }1241 1242 1243 } catch ( \Exception $e ) {1244 1245 1246 // Swallow to keep bulk op resilient1247 1248 1249 }1250 1251 1252 return $found;1253 1254 1255 }1256 1257 1258 /**1259 1260 1261 * Resolve a safe absolute path for a relative path under ABSPATH,1262 1263 1264 * restricted strictly to files literally named ".htaccess".1265 1266 1267 *1268 1269 1270 * @param string $rel Relative path (e.g., 'wp-content/uploads/.htaccess')1271 1272 1273 * @return string|false Absolute path if valid and safe, false otherwise1274 1275 1276 */1277 1278 1279 private function get_safe_htaccess_file( string $rel ) {1280 1281 1282 $rel = wp_normalize_path( sanitize_text_field( $rel ) );1283 1284 1285 // Deny traversal / absolute paths1286 1287 1288 if ( $rel === '' || strpos( $rel, '..' ) !== false || preg_match( '#^[\\/]|[a-zA-Z]:[\\\\/]#', $rel ) ) {1289 1290 1291 return false;1292 1293 1294 }1295 1296 1297 $base = wp_normalize_path( realpath( ABSPATH ) );1298 1299 1300 if ( ! $base ) { return false; }1301 1302 1303 $abs = wp_normalize_path( $base . '/' . ltrim( $rel, '/\\' ) );1304 1305 1306 $real = realpath( $abs );1307 1308 1309 if ( ! $real ) { return false; }1310 1311 1312 $real = wp_normalize_path( $real );1313 1314 1315 // Must live under ABSPATH and be literally ".htaccess"1316 1317 1318 if ( strpos( $real, $base ) !== 0 || basename( $real ) !== '.htaccess' || ! is_file( $real ) ) {1319 1320 1321 return false;1322 1323 1324 }1325 1326 1327 return $real;1328 1329 1330 }1331 1332 1333 /**1334 1335 1336 * Download a single .htaccess file as a tiny ZIP (named "htaccess.zip").1337 1338 1339 *1340 1341 1342 * Nonce: 'fa_htaccess_download_' . md5($rel)1343 1344 1345 * POST: rel1346 1347 1348 */1349 1350 1351 public function handle_htaccess_download() {1352 1353 1354 if ( ! $this->can_manage() ) {1355 1356 1357 wp_die( esc_html__( 'You do not have permission to do this.', 'folder-auditor' ) );1358 1359 1360 }1361 1362 1363 $rel = (string) ( filter_input( INPUT_POST, 'rel', FILTER_UNSAFE_RAW ) ?? '' );1364 1365 1366 $rel = sanitize_text_field( $rel );1367 1368 1369 check_admin_referer( 'fa_htaccess_download_' . md5( $rel ) );1370 1371 1372 $abs = $this->get_safe_htaccess_file( $rel );1373 1374 1375 if ( ! $abs ) {1376 1377 1378 wp_die( esc_html__( 'Invalid file.', 'folder-auditor' ) );1379 1380 1381 }1382 1383 1384 if ( ! class_exists( 'ZipArchive' ) ) {1385 1386 1387 wp_die( esc_html__( 'ZipArchive is not available on this server.', 'folder-auditor' ) );1388 1389 1390 }1391 1392 1393 // Build one-file ZIP (keeps filename exactly ".htaccess" inside)1394 1395 1396 $zip_file = wp_tempnam( 'fa-htaccess-' ) . '.zip';1397 1398 1399 $zip = new ZipArchive();1400 1401 1402 if ( true !== $zip->open( $zip_file, ZipArchive::CREATE | ZipArchive::OVERWRITE ) ) {1403 1404 1405 // Safe cleanup via WP helper1406 1407 1408 wp_delete_file( $zip_file );1409 1410 1411 wp_die( esc_html__( 'Failed to build ZIP.', 'folder-auditor' ) );1412 1413 1414 }1415 1416 1417 $zip->addFile( $abs, '.htaccess' );1418 1419 1420 $zip->close();1421 1422 1423 if ( function_exists( 'ob_get_level' ) ) {1424 1425 1426 while ( ob_get_level() ) { ob_end_clean(); }1427 1428 1429 }1430 1431 1432 // Use Filesystem API to read + echo; avoid readfile()1433 1434 1435 $wp_filesystem = $this->fa_require_filesystem();1436 1437 1438 $size = is_file( $zip_file ) ? (int) filesize( $zip_file ) : 0;1439 1440 1441 $contents = $wp_filesystem->get_contents( $zip_file );1442 1443 1444 nocache_headers();1445 1446 1447 header( 'Content-Type: application/zip' );1448 1449 1450 if ( $size > 0 ) {1451 1452 1453 header( 'Content-Length: ' . $size );1454 1455 1456 }1457 1458 1459 header( 'Content-Disposition: attachment; filename="htaccess.zip"; filename*=UTF-8\'\'htaccess.zip' );1460 1461 1462 if ( false !== $contents ) {1463 1464 1465 echo $contents; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- sending raw file bytes1466 1467 1468 }1469 1470 1471 // Remove temp file via WP helper (no unlink()).1472 1473 1474 wp_delete_file( $zip_file );1475 1476 1477 exit;1478 1479 1480 }1481 1482 1483 /**1484 1485 1486 * Delete a single .htaccess file.1487 1488 1489 *1490 1491 1492 * Nonce: 'fa_htaccess_delete_' . md5($rel)1493 1494 1495 * POST: rel1496 1497 1498 */1499 1500 1501 public function handle_htaccess_delete() {1502 1503 1504 if ( ! $this->can_manage() ) {1505 1506 1507 wp_die( esc_html__( 'You do not have permission to do this.', 'folder-auditor' ) );1508 1509 1510 }1511 1512 1513 $rel = (string) ( filter_input( INPUT_POST, 'rel', FILTER_UNSAFE_RAW ) ?? '' );1514 1515 1516 $rel = sanitize_text_field( $rel );1517 1518 1519 check_admin_referer( 'fa_htaccess_delete_' . md5( $rel ) );1520 1521 1522 $abs = $this->get_safe_htaccess_file( $rel );1523 1524 1525 if ( ! $abs ) {1526 1527 1528 wp_die( esc_html__( 'Invalid file.', 'folder-auditor' ) );1529 1530 1531 }1532 1533 1534 $wp_filesystem = $this->fa_require_filesystem();1535 1536 1537 // Prefer FS API; fallback to wp_delete_file1538 1539 1540 $ok = true;1541 1542 1543 if ( method_exists( $wp_filesystem, 'delete' ) ) {1544 1545 1546 $ok = $wp_filesystem->delete( $abs, false, 'f' );1547 1548 1549 } else {1550 1551 1552 $ok = (bool) wp_delete_file( $abs );1553 1554 1555 }1556 1557 1558 if ( ! $ok ) {1559 1560 1561 wp_die( esc_html__( 'Failed to delete the file (permissions?).', 'folder-auditor' ) );1562 1563 1564 }1565 1566 1567 // Back to the tab with a success context1568 1569 1570 $redirect = add_query_arg(1571 1572 1573 array(1574 1575 1576 'page' => self::MENU_SLUG,1577 1578 1579 'tab' => 'htaccess',1580 1581 1582 'fa_htaccess_msg' => rawurlencode( $rel ),1583 1584 1585 ),1586 1587 1588 admin_url( 'admin.php' )1589 1590 1591 );1592 1593 1594 wp_safe_redirect( $redirect );1595 1596 1597 exit;1598 1599 1600 }1601 1602 1603 } -
folder-auditor/trunk/includes/helpers/scanner/scanner.php
r3380575 r3381342 854 854 return [ $raw, $norm ]; 855 855 } 856 protected function wpfa_get_raw_patterns() : array { 857 return [ 858 'socketapiupdates\\.com', 859 'adolescent_deployment', 860 'decent[-_ ]follower', 861 'Farrell\\s*-\\s*Herzog', 862 'decent_follower\\.php|class\\/pick\\.php', 863 'sneaky_arrange_calculating\\.php', 864 'fatally_unsightly_quirkily\\s*\\(|verbally_concrete\\s*\\(|indelible_ethyl\\s*\\(|eyebrow_gracefully_whitewash\\s*\\(|harp_passionate\\s*\\(', 865 '^\x7fELF', 866 '^MZ', 867 '^\xCF\xFA\xED', 868 '\b(?:require|require_once|include|include_once)\s*\(?\s*(?:@?\s*)?(?:base64_decode|str_rot13|gzinflate|gzuncompress)\s*\(', 869 'error_reporting\\s*\\(\\s*0\\s*\\)\\s*;\\s*\\$LlCam\\s*=\\s*array\\(\\s*\"\\\\x5f\\\\107\\\\x45\\\\x54\"\\s*\\)\\s*;', 870 '\\$\\{\\s*\\$LlCam\\s*\\[\\s*0\\s*\\]\\s*\\}', 871 '@?require_once\\s*[\'"](?:\\\\x7a\\\\x69\\\\x70)[^\'"]*[\'"]', 872 '\\\\x65\\\\x64\\\\x31\\\\x31\\\\x30\\\\x62\\\\x65\\\\x62\\\\x63\\\\x65\\\\x39\\\\x2e\\\\x74\\\\x6d\\\\x70', 873 'ycycsUnT3uBLqyUrzfdIDg23r', 874 '\beval\s*\(\s*base64_decode\s*\(', 875 '\bpreg_replace\s*\(\s*([\'"])\s*([^\w\s\\])(?:\\.|(?!\2).)*\2(?=[A-Za-z]*e[A-Za-z]*\1)', 876 '\b(?:include|require|include_once|require_once)\s*(?:\(\s*)?[\'"]https?:\/\/', 877 '\b(?:exec|shell_exec|system|passthru|proc_open|popen|pcntl_exec)\s*\([^)]*\$_(?:GET|POST|REQUEST|COOKIE|SERVER)\b', 878 'shell_exec\s*\([^)]*\$_REQUEST', 879 '\b(?:file_put_contents|fopen|fwrite|fputs)\s*\(\s*\$[A-Za-z_]\w*\s*,\s*base64_decode\s*\(', 880 '[\'"]\s*e\s*[\'"]\s*\.\s*[\'"]\s*val\s*[\'"]', 881 '[\'"]\s*base\s*[\'"]\s*\.\s*[\'"]\s*64\s*[\'"]\s*\.\s*[\'"]\s*decode\s*[\'"]', 882 '\$\w+\s*=\s*[\'"][a-z]{1,3}[\'"]\s*;\s*\$\w+\s*=\s*[\'"][a-z]{1,3}[\'"]\s*;\s*\$\w+\s*=\s*\$\w+\s*\.\s*\$\w+\s*\.\s*[\'"](tem|val|sert)[\'"]\s*;', 883 'ob_implicit_flush\s*\(\s*true\s*\)\s*;[\s\S]{0,160}\bob_end_flush\s*\(', 884 '(?s)(?:OPENSSL_RAW_DATA.*substr\s*\(\s*hash\s*\(\s*[\'"]sha256[\'"]|substr\s*\(\s*hash\s*\(\s*[\'"]sha256[\'"].*OPENSSL_RAW_DATA|[\'"]<\s*\/?\s*scr?\s*[\'"]\s*\.\s*[\'"]r?ipt\s*>[\'"])', 885 '(?s)\$[A-Za-z_]\w*\s*=\s*\$_(?:POST|REQUEST)\s*;.*?isset\s*\(\s*\$[A-Za-z_]\w*\s*\[[\'"][a-z0-9_]{3,}[\'"]\]\s*\).*?\$\w+\s*\(\s*\.\.\.\$\w+\s*\)', 886 '(?s)readfile\s*\(\s*base64_decode\s*\(\s*["\'][^"\']{8,}["\']\s*\)\s*\)\s*;.*?eval\s*\(\s*.*?ob_get_clean\s*\(\s*\)\s*\)\s*;', 887 'function\s*uPqmvR\s*\(', 888 'function\s*yh1\s*\(', 889 '\bUpVwwHRQ\s*\(', 890 'array_map\(\s*[\'"]md5[\'"]\s*,\s*\$_COOKIE', 891 '\$gi6\[\d+\]\s*\(\s*\$_(?:COOKIE|POST|REQUEST)', 892 'include\s*\(\s*base64_decode\s*\(\s*\$[A-Za-z_]\w*\s*\)\s*(?:\.\s*)?\)\s*;', 893 'call_user_func\s*\(\s*new\s+LiteSpeedMetaDataStore' 894 ]; 856 857 protected function wpfa_get_raw_patterns(): array { 858 $patterns = include __DIR__ . '/patterns.php'; 859 return $patterns['raw']; 860 } 861 862 protected function wpfa_get_patterns(): array { 863 $patterns = include __DIR__ . '/patterns.php'; 864 return $patterns['scan']; 865 } 895 866 } 896 897 protected function wpfa_get_patterns() : array {898 return [899 'socketapiupdates\\.com',900 'adolescent_deployment',901 'decent[-_ ]follower',902 'Farrell\\s*-\\s*Herzog',903 'decent_follower\\.php|class\\/pick\\.php',904 'sneaky_arrange_calculating\\.php',905 'fatally_unsightly_quirkily\\s*\\(|verbally_concrete\\s*\\(|indelible_ethyl\\s*\\(|eyebrow_gracefully_whitewash\\s*\\(|harp_passionate\\s*\\(',906 '^\x7fELF',907 '^MZ',908 '^\xCF\xFA\xED',909 '\b(?:require|require_once|include|include_once)\s*\(?\s*(?:@?\s*)?(?:base64_decode|str_rot13|gzinflate|gzuncompress)\s*\(',910 'error_reporting\\s*\\(\\s*0\\s*\\)\\s*;\\s*\\$LlCam\\s*=\\s*array\\s*\\(',911 '\\$\\{\\s*\\$LlCam\\s*\\[\\s*0\\s*\\]\\s*\\}',912 '@?require_once\\s*[\\\'\\"][^\\\'\\\"]{3,256}[\\\'\\"]',913 'ed110bebce9\\.tmp',914 'ycycsUnT3uBLqyUrzfdIDg23r',915 '\b(?:fopen|fwrite|fputs|file_put_contents|file_get_contents|fclose|chmod|unlink)\s*\([^)]*base64_decode\s*\(',916 'basename\s*\(\s*__FILE__\s*,\s*base64_decode\s*\(',917 '\b(?:exec|shell_exec|system|passthru|proc_open|popen|pcntl_exec)\s*\([^)]*base64_decode\s*\(',918 '\b(?:exec|shell_exec|system|passthru|proc_open|popen|pcntl_exec)\s*\(\s*\$_(?:GET|POST|REQUEST|COOKIE|SERVER)\b',919 '>\s*\/dev\/null\s*2>\s*\/dev\/null\s*&',920 '(?:base64_encode\s*\(\s*){2,}[^)]*\)',921 '(?:base64_decode\s*\(\s*){2,}[^)]*\)',922 'openssl_(?:en|de)crypt\s*\([^,]+,\s*[\'"]?\s*aes\s*[-_ ]?(?:128|192|256)\s*[-_ ]?cbc[\'"]?\s*,[^)]*hash\s*\(\s*[\'"]sha256[\'"]\s*,\s*[\'"][^\'"]{8,}[\'"]\s*,\s*true\s*\)[^)]*\)',923 'substr\s*\(\s*hash\s*\(\s*[\'"]sha256[\'"]\s*,\s*[\'"][^\'"]{8,}[\'"]\s*,\s*true\s*\)\s*,\s*0\s*,\s*16\s*\)',924 '\b(?:include|require|include_once|require_once)\s*\(\s*base64_decode\s*\(',925 '\b(?:include|require|include_once|require_once|file_get_contents|fopen)\s*\([^)]*(?:php:\/\/input|php:\/\/filter|data:\/\/)',926 'md5\s*\(\s*uniqid\s*\([^)]*\)\s*\)\s*\.\s*[\'"]\.(?:php|phtml)\b',927 'curl_init\s*\([^)]*\)\s*;[^;]*CURLOPT_(?:HTTPHEADER|COOKIE)[^;]*(?:Authorization|Cookie)[^;]*token\s*=\s*',928 'wp_remote_request\s*\([^)]*\bheaders\b[^)]*(?:Authorization|Cookie)[\'"]\s*=>\s*[\'"][^\'"]*token=',929 '`[^`]{1,200}`',930 '[\'"]<\s*sc[\'"]\s*\.\s*[\'"]ript\s*>[\'"]',931 '[\'"]<\/\s*scr[\'"]\s*\.\s*[\'"]ipt\s*>[\'"]',932 'array\s*\(\s*(?:\s*[\'"][a-z0-9][\'"]\s*,){3,}\s*[\'"][a-z0-9][\'"]\s*\)',933 '\$[A-Za-z_]\w*\s*=\s*\$\w+\s*\.\s*\$\w+\s*\.\s*[\'"](tem|xec)[\'"]',934 '(?s)readfile\s*\(\s*base64_decode\s*\(.*?\)\s*\).*?eval\s*\(\s*.*?ob_get_clean\s*\(\s*\).*?\)',935 '\$[A-Za-z_]\w*\s*=\s*\$[A-Za-z_]\w*\s*\[[\'"][a-z0-9_]{3,}[\'"]\]\s*;.*\$\w+\s*\(\s*\.\.\.\$\w+\s*\)',936 'function\s*uPqmvR\s*\(',937 'function\s*yh1\s*\(',938 '\bUpVwwHRQ\s*\(',939 'array_map\(\s*[\'"]md5[\'"]\s*,\s*\$_COOKIE',940 '\$gi6\[\d+\]\s*\(\s*\$_(?:COOKIE|POST|REQUEST)',941 'include\s*\(\s*base64_decode\s*\(\s*\$[A-Za-z_]\w*\s*\)\s*(?:\.\s*)?\)\s*;',942 'call_user_func\s*\(\s*new\s+LiteSpeedMetaDataStore'943 ];944 }945 } -
folder-auditor/trunk/includes/summaries/summary-htaccess.php
r3368707 r3381342 3 3 trait WPFA_htaccess_summary_functions { 4 4 5 6 /** 7 8 * Build a summary for the .htaccess metric card (raw vs effective, honoring "ignored"). 9 10 * 11 12 * @return array { 13 14 * @type string key 15 16 * @type string label 17 18 * @type string tab 19 20 * @type int count Raw total 21 22 * @type int count_effective Total minus ignored 23 24 * } 25 26 */ 27 28 private function summary_htaccess() : array { 29 30 $base = realpath( ABSPATH ); 31 $all_abs = []; 32 $all_rel = []; 33 34 if ( $base && is_dir( $base ) && is_readable( $base ) ) { 35 36 try { 37 $it = new RecursiveIteratorIterator( 38 new RecursiveDirectoryIterator( $base, FilesystemIterator::SKIP_DOTS ), 39 RecursiveIteratorIterator::SELF_FIRST 40 ); 41 42 $skip = [ '.git', '.svn', 'node_modules', 'vendor', 'cache', 'backups', 'backup' ]; 43 44 foreach ( $it as $fi ) { 45 if ( $fi->isDir() ) { 46 if ( in_array( $fi->getBasename(), $skip, true ) ) { $it->next(); } 47 continue; 48 } 49 50 if ( ! $fi->isFile() ) { continue; } 51 if ( $fi->getBasename() !== '.htaccess' ) { continue; } 52 53 $abs = realpath( $fi->getPathname() ); 54 if ( ! $abs ) { continue; } 55 if ( strpos( $abs, $base ) !== 0 ) { continue; } 56 57 // ✅ Exclude specific file: wp-content/plugins/mainwp-child/.htaccess 58 $rel = str_replace('\\', '/', ltrim( str_replace( $base, '', $abs ), '/\\' )); 59 if ( strtolower( $rel ) === 'wp-content/plugins/mainwp-child/.htaccess' ) { 60 continue; 61 } 62 63 $all_abs[] = $abs; 64 $all_rel[] = $rel; // same rel format as view 65 } 66 67 } catch ( \Exception $e ) { /* ignore */ } 68 } 69 70 $raw_count = count( $all_rel ); 71 72 // Apply ignore set (bucket: 'htaccess'; keys are REL paths like "wp-content/uploads/.htaccess") 73 $ignored = method_exists( $this, 'get_ignored' ) ? (array) $this->get_ignored() : []; 74 $ignored_keys = array_keys( (array) ( $ignored['htaccess'] ?? [] ) ); 75 $ignored_in_set = count( array_intersect( $all_rel, $ignored_keys ) ); 76 $count_effective = max( 0, $raw_count - $ignored_in_set ); 77 78 return [ 79 'key' => 'htaccess', 80 'label' => __( '.htaccess Files', 'folder-auditor' ), 81 'tab' => 'htaccess', 82 'count' => (int) $raw_count, // raw total for display 83 'count_effective' => (int) $count_effective, // excludes ignored (for health score) 84 ]; 85 } 5 86 6 87 } -
folder-auditor/trunk/includes/summaries/summary-plugins.php
r3367997 r3381342 15 15 // Skip specific plugin folders entirely (e.g., "support-wpfi") 16 16 17 $skip_slugs = apply_filters( 'folder_auditor_plugin_skip_slugs', array( 'support-wpfi' ) );17 $skip_slugs = apply_filters( 'folder_auditor_plugin_skip_slugs', array( 'support-wpfi', 'mainwp-child', 'mainwp-child-reports' ) ); 18 18 19 19 $skip_lc = array(); -
folder-auditor/trunk/includes/summaries/summary-uploads.php
r3367997 r3381342 49 49 } 50 50 51 // ── scan uploads recursively for any *.php (for health score / card) 51 // ── scan uploads recursively for any *.php (for health score / card) 52 if ( is_dir( $base ) && is_readable( $base ) ) { 53 try { 54 $rii = new RecursiveIteratorIterator( 55 new RecursiveDirectoryIterator( $base, FilesystemIterator::SKIP_DOTS ) 56 ); 52 57 53 if ( is_dir( $base ) && is_readable( $base ) ) { 58 foreach ( $rii as $fi ) { 59 if ( $fi->isFile() && preg_match( '/\.php$/i', $fi->getFilename() ) ) { 54 60 55 try { 61 // relative path like "2024/08/shell.php" or "file.php" 62 $full = $fi->getPathname(); 63 $rel = ltrim( str_replace('\\','/', substr( $full, strlen( $base ) ) ), '/' ); 56 64 57 $rii = new RecursiveIteratorIterator( 58 59 new RecursiveDirectoryIterator( $base, FilesystemIterator::SKIP_DOTS ) 60 61 ); 62 63 foreach ( $rii as $fi ) { 64 65 if ( $fi->isFile() && preg_match( '/\.php$/i', $fi->getFilename() ) ) { 66 67 // relative path like "2024/08/shell.php" or "file.php" 68 69 $full = $fi->getPathname(); 70 71 $rel = ltrim( str_replace('\\','/', substr( $full, strlen( $base ) ) ), '/' ); 72 73 $php_any_rel[] = $rel; 74 65 // ✅ Exclude "mainwp/index.php" 66 if ( strtolower( $rel ) === 'mainwp/index.php' ) { 67 continue; 75 68 } 76 69 70 $php_any_rel[] = $rel; 77 71 } 72 } 78 73 79 } catch ( \Exception $e ) { /* ignore */ } 80 81 } 74 } catch ( \Exception $e ) { /* ignore */ } 75 } 82 76 83 77 // ── apply “Ignore” (if user has marked items to exclude) -
folder-auditor/trunk/includes/views/view-htaccess-files.php
r3374418 r3381342 101 101 if ( ! $fi->isFile() ) { continue; } 102 102 103 if ( $fi->getBasename() !== '.htaccess' ) { continue; } 104 105 $real = realpath( $fi->getPathname() ); 106 107 if ( ! $real ) { continue; } 103 if ( $fi->getBasename() !== '.htaccess' ) { continue; } 104 105 $real = realpath( $fi->getPathname() ); 106 107 if ( ! $real ) { continue; } 108 109 // ✅ Exclude the specific file wp-content/plugins/mainwp-child/.htaccess 110 $rel = str_replace('\\', '/', ltrim( str_replace( realpath( ABSPATH ), '', $real ), '/' )); 111 if ( strtolower( $rel ) === 'wp-content/plugins/mainwp-child/.htaccess' ) { 112 continue; 113 } 108 114 109 115 if ( strpos( $real, $base ) !== 0 ) { continue; } -
folder-auditor/trunk/includes/views/view-plugins.php
r3374418 r3381342 5 5 <?php 6 6 7 // Ignore 'support-wpfi' everywhere (counts + tables) 8 9 $fa_ignore_slug = 'support-wpfi'; 10 11 $fa_eq = static function($a,$b){ return strtolower((string)$a) === strtolower((string)$b); }; 7 // Ignore these slugs everywhere (counts + tables) 8 $fa_ignore_slugs = array_map('strtolower', [ 9 'support-wpfi', 10 'mainwp-child', 11 'mainwp-child-reports', 12 ]); 13 14 $fa_is_ignored = static function( $s ) use ( $fa_ignore_slugs ) { 15 $s = strtolower( (string) $s ); 16 return in_array( $s, $fa_ignore_slugs, true ); 17 }; 12 18 13 19 // Remove from the disk map used for lookups/existence checks 14 15 if (isset($folders_map) && is_array($folders_map)) { 16 17 foreach (array_keys($folders_map) as $k) { 18 19 if ($fa_eq($k, $fa_ignore_slug)) { unset($folders_map[$k]); } 20 20 if ( isset( $folders_map ) && is_array( $folders_map ) ) { 21 foreach ( array_keys( $folders_map ) as $k ) { 22 if ( $fa_is_ignored( $k ) ) { unset( $folders_map[ $k ] ); } 21 23 } 22 23 24 } 24 25 25 26 // Remove from the simple list of disk slugs (used in cards/counts) 26 27 if (isset($disk_slugs) && is_array($disk_slugs)) { 28 29 $disk_slugs = array_values(array_filter($disk_slugs, function($s) use ($fa_eq,$fa_ignore_slug){ 30 31 return ! $fa_eq($s, $fa_ignore_slug); 32 33 })); 34 27 if ( isset( $disk_slugs ) && is_array( $disk_slugs ) ) { 28 $disk_slugs = array_values( array_filter( $disk_slugs, function( $s ) use ( $fa_is_ignored ) { 29 return ! $fa_is_ignored( $s ); 30 } ) ); 35 31 } 36 32 37 33 // Remove from orphan list (used in the “not showing as installed” table/card) 38 39 if (isset($orphan_folders) && is_array($orphan_folders)) { 40 41 $orphan_folders = array_values(array_filter($orphan_folders, function($s) use ($fa_eq,$fa_ignore_slug){ 42 43 return ! $fa_eq($s, $fa_ignore_slug); 44 45 })); 46 34 if ( isset( $orphan_folders ) && is_array( $orphan_folders ) ) { 35 $orphan_folders = array_values( array_filter( $orphan_folders, function( $s ) use ( $fa_is_ignored ) { 36 return ! $fa_is_ignored( $s ); 37 } ) ); 47 38 } 48 39 49 40 // Remove from installed-plugins table rows and update count 50 51 if (isset($plugin_rows) && is_array($plugin_rows)) { 52 53 $plugin_rows = array_values(array_filter($plugin_rows, function($row) use ($fa_eq,$fa_ignore_slug){ 54 55 return empty($row['folder_slug']) || ! $fa_eq($row['folder_slug'], $fa_ignore_slug); 56 57 })); 58 59 $total_plugins = count($plugin_rows); 60 41 if ( isset( $plugin_rows ) && is_array( $plugin_rows ) ) { 42 $plugin_rows = array_values( array_filter( $plugin_rows, function( $row ) use ( $fa_is_ignored ) { 43 return empty( $row['folder_slug'] ) || ! $fa_is_ignored( $row['folder_slug'] ); 44 } ) ); 45 $total_plugins = count( $plugin_rows ); 61 46 } 62 47 63 48 // Put active plugins at the top, then sort A→Z by name (then folder slug) 64 65 49 if ( ! empty( $plugin_rows ) && is_array( $plugin_rows ) ) { 66 50 67 51 // Build a fast lookup of active plugins (site + network) 68 69 52 $active_site = (array) get_option( 'active_plugins', array() ); 70 71 53 $network_active = is_multisite() ? array_keys( (array) get_site_option( 'active_sitewide_plugins', array() ) ) : array(); 72 73 54 $active_lookup = array_fill_keys( array_merge( $active_site, $network_active ), true ); 74 55 75 56 usort( $plugin_rows, function( $a, $b ) use ( $active_lookup ) { 76 77 $a_key = isset( $a['plugin_file'] ) ? (string) $a['plugin_file'] : ''; 78 79 $b_key = isset( $b['plugin_file'] ) ? (string) $b['plugin_file'] : ''; 80 57 $a_key = isset( $a['plugin_file'] ) ? (string) $a['plugin_file'] : ''; 58 $b_key = isset( $b['plugin_file'] ) ? (string) $b['plugin_file'] : ''; 81 59 $a_active = isset( $active_lookup[ $a_key ] ); 82 83 60 $b_active = isset( $active_lookup[ $b_key ] ); 84 61 85 62 // Active first 86 87 63 if ( $a_active !== $b_active ) { 88 89 64 return $a_active ? -1 : 1; 90 91 65 } 92 66 93 67 // Then by display name (A→Z) 94 95 $an = isset( $a['name'] ) ? (string) $a['name'] : ''; 96 97 $bn = isset( $b['name'] ) ? (string) $b['name'] : ''; 98 68 $an = isset( $a['name'] ) ? (string) $a['name'] : ''; 69 $bn = isset( $b['name'] ) ? (string) $b['name'] : ''; 99 70 $cmp = strcasecmp( $an, $bn ); 100 101 71 if ( 0 !== $cmp ) { 102 103 72 return $cmp; 104 105 73 } 106 74 107 75 // Stable fallback by folder slug 108 109 76 $aslug = isset( $a['folder_slug'] ) ? (string) $a['folder_slug'] : ''; 110 111 77 $bslug = isset( $b['folder_slug'] ) ? (string) $b['folder_slug'] : ''; 112 113 78 return strcasecmp( $aslug, $bslug ); 114 115 }); 116 79 } ); 117 80 } 118 81 119 82 // Build list of files directly in plugins root that are NOT registered as plugins 120 121 83 if ( ! isset( $plugins_root_files ) ) { 122 123 84 $plugins_root_files = []; 124 125 85 try { 126 127 86 if ( is_dir( WP_PLUGIN_DIR ) && is_readable( WP_PLUGIN_DIR ) ) { 128 129 87 $it = new DirectoryIterator( WP_PLUGIN_DIR ); 130 131 88 foreach ( $it as $fi ) { 132 133 89 if ( $fi->isFile() ) { // root-only files 134 135 90 $plugins_root_files[] = $fi->getFilename(); 136 137 91 } 138 139 92 } 140 141 93 } 142 143 94 } catch ( Exception $e ) {} 144 95 145 96 // Remove any single-file plugins WP already knows about 146 147 97 if ( ! empty( $plugin_rows ) ) { 148 149 98 foreach ( $plugin_rows as $row ) { 150 151 99 if ( isset( $row['folder_slug'] ) && $row['folder_slug'] === '.' ) { 152 153 100 $plugins_root_files = array_values( 154 155 101 array_diff( $plugins_root_files, [ basename( $row['plugin_file'] ) ] ) 156 157 102 ); 158 159 103 } 160 161 104 } 162 163 105 } 164 165 106 } 166 107 167 108 // Count for the card 168 169 109 $plugins_root_files_count = is_array( $plugins_root_files ) ? count( $plugins_root_files ) : 0; 170 110 -
folder-auditor/trunk/includes/views/view-uploads.php
r3374418 r3381342 74 74 75 75 } 76 77 // Filter out a specific file so it never shows up anywhere downstream 78 $uploads_php_hits = array_values(array_filter( 79 (array) $uploads_php_hits, 80 function( $hit ) { 81 // Normalize path like "mainwp/index.php" 82 $rel = isset($hit['path']) ? ltrim(str_replace('\\','/',$hit['path']), '/'): ''; 83 // Only skip this exact file 84 return strtolower($rel) !== 'mainwp/index.php'; 85 } 86 )); 76 87 77 88 }
Note: See TracChangeset
for help on using the changeset viewer.