class-redux-filesystem.php (29210B)
1 <?php 2 /** 3 * Redux Filesystem Class 4 * 5 * @class Redux_Filesystem 6 * @version 4.0.0 7 * @package Redux Framework/Classes 8 */ 9 10 defined( 'ABSPATH' ) || exit; 11 12 if ( ! class_exists( 'Redux_Filesystem', false ) ) { 13 14 /** 15 * Class Redux_Filesystem 16 */ 17 class Redux_Filesystem { 18 19 /** 20 * Instance of this class. 21 * 22 * @since 1.0.0 23 * @var object 24 */ 25 protected static $instance = null; 26 27 /** 28 * WP Filesystem object. 29 * 30 * @var object 31 */ 32 protected static $direct = null; 33 34 /** 35 * File system credentials. 36 * 37 * @var array 38 */ 39 private $creds = array(); 40 41 /** 42 * ReduxFramework object pointer. 43 * 44 * @var object 45 */ 46 public $parent = null; 47 48 /** 49 * Instance of WP_Filesystem 50 * 51 * @var WP_Filesystem|null 52 */ 53 private $wp_filesystem; 54 55 /** 56 * If DBI_Filesystem should attempt to use the WP_Filesystem class. 57 * 58 * @var bool 59 */ 60 private $use_filesystem = false; 61 62 /** 63 * Default chmod octal value for directories. 64 * 65 * @var int 66 */ 67 private $chmod_dir; 68 69 /** 70 * Default chmod octal value for files. 71 * 72 * @var int 73 */ 74 private $chmod_file; 75 76 /** 77 * Default cache folder. 78 * 79 * @var string 80 */ 81 public $cache_folder; 82 83 /** 84 * Pass `true` when instantiating to skip using WP_Filesystem. 85 * 86 * @param bool $force_no_fs Force no use of the filesystem. 87 */ 88 public function __construct( bool $force_no_fs = false ) { 89 90 // This little number fixes some issues with certain filesystem setups. 91 92 if ( ! function_exists( 'request_filesystem_credentials' ) ) { 93 require_once ABSPATH . '/wp-admin/includes/template.php'; 94 require_once ABSPATH . '/wp-includes/pluggable.php'; 95 require_once ABSPATH . '/wp-admin/includes/file.php'; 96 } 97 98 if ( ! $force_no_fs && function_exists( 'request_filesystem_credentials' ) ) { 99 if ( ( defined( 'WPMDB_WP_FILESYSTEM' ) && WPMDB_WP_FILESYSTEM ) || ! defined( 'WPMDB_WP_FILESYSTEM' ) ) { 100 $this->maybe_init_wp_filesystem(); 101 } 102 } 103 104 $uploads_dir = wp_upload_dir(); 105 $this->cache_folder = trailingslashit( $uploads_dir['basedir'] ) . 'redux/'; 106 if ( ! $this->file_exists( $this->cache_folder ) ) { 107 $this->mkdir( $this->cache_folder ); 108 } 109 } 110 111 /** 112 * Return an instance of this class. 113 * 114 * @param object $parent ReduxFramework pointer. 115 * 116 * @since 1.0.0 117 * @return object A single instance of this class. 118 */ 119 public static function get_instance( $parent = null ) { 120 121 // If the single instance hasn't been set, set it now. 122 if ( null === self::$instance ) { 123 self::$instance = new self(); 124 } 125 126 if ( null !== $parent ) { 127 self::$instance->parent = $parent; 128 } 129 130 return self::$instance; 131 } 132 133 /** 134 * Build FTP form. 135 */ 136 public function ftp_form() { 137 if ( isset( $this->parent->ftp_form ) && ! empty( $this->parent->ftp_form ) ) { 138 echo '<div class="wrap">'; 139 echo '<div class="error">'; 140 echo '<p>'; 141 // translators: %1$s: Upload URL. %2$s: Codex URL. 142 echo '<strong>' . esc_html__( 'File Permission Issues', 'redux-framework' ) . '</strong><br/>' . sprintf( esc_html__( 'We were unable to modify required files. Please ensure that %1$s has the proper read-write permissions, or modify your wp-config.php file to contain your FTP login credentials as %2$s.', 'redux-framework' ), '<code>' . esc_url( Redux_Functions_Ex::wp_normalize_path( trailingslashit( WP_CONTENT_DIR ) ) . '/uploads/' ) . '</code>', ' <a href="https://codex.wordpress.org/Editing_wp-config.php#WordPress_Upgrade_Constants" target="_blank">' . esc_html__( 'outlined here', 'redux-framework' ) . '</a>' ); 143 echo '</p>'; 144 echo '</div>'; 145 echo '<h2></h2>'; 146 echo '</div>'; 147 } 148 } 149 150 /** 151 * Attempt to initiate WP_Filesystem 152 * If this fails, $use_filesystem is set to false and all methods in this class should use native php fallbacks 153 * Thwarts `request_filesystem_credentials()` attempt to display a form for obtaining creds from users 154 * TODO: provide notice and input in wp-admin for users when this fails 155 */ 156 public function maybe_init_wp_filesystem() { 157 // Set up the filesystem with creds. 158 require_once ABSPATH . '/wp-admin/includes/template.php'; 159 require_once ABSPATH . '/wp-includes/pluggable.php'; 160 require_once ABSPATH . '/wp-admin/includes/file.php'; 161 ob_start(); 162 $credentials = request_filesystem_credentials( '', '', false, false ); 163 $ob_contents = ob_get_contents(); 164 ob_end_clean(); 165 if ( @wp_filesystem( $credentials ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors 166 global $wp_filesystem; 167 $this->wp_filesystem = $wp_filesystem; 168 $this->use_filesystem = true; 169 $this->generate_default_files(); 170 } 171 } 172 173 /** 174 * Init WO Filesystem. 175 * 176 * @param string $form_url Form URL. 177 * @param string $method Connect method. 178 * @param bool $context Context. 179 * @param null $fields Fields. 180 * 181 * @return bool 182 */ 183 public function advanced_filesystem_init( string $form_url, string $method = '', bool $context = false, $fields = null ): bool { 184 if ( ! empty( $this->wp_filesystem ) && $this->use_filesystem ) { 185 return true; 186 } 187 188 if ( ! empty( $this->creds ) ) { 189 return true; 190 } 191 192 ob_start(); 193 194 $this->creds = request_filesystem_credentials( $form_url, $method, false, $context ); 195 196 /* first attempt to get credentials */ 197 if ( false === $this->creds ) { 198 $this->creds = array(); 199 $this->parent->ftp_form = ob_get_contents(); 200 ob_end_clean(); 201 202 /** 203 * If we come here - we don't have credentials 204 * so the request for them is displaying 205 * no need for further processing 206 * */ 207 return false; 208 } 209 210 /* now we got some credentials - try to use them */ 211 if ( ! @wp_filesystem( $this->creds ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors 212 $this->creds = array(); 213 /* incorrect connection data - ask for credentials again, now with error message */ 214 request_filesystem_credentials( $form_url, '', true, $context ); 215 $this->parent->ftp_form = ob_get_contents(); 216 ob_end_clean(); 217 218 return false; 219 } 220 221 global $wp_filesystem; 222 $this->wp_filesystem = $wp_filesystem; 223 $this->use_filesystem = true; 224 $this->generate_default_files(); 225 226 return true; 227 } 228 229 /** 230 * Load WP fiesystem directly. 231 */ 232 public static function load_direct() { 233 if ( null === self::$direct ) { 234 require_once ABSPATH . '/wp-admin/includes/class-wp-filesystem-base.php'; 235 require_once ABSPATH . '/wp-admin/includes/class-wp-filesystem-direct.php'; 236 237 self::$direct = new WP_Filesystem_Direct( array() ); 238 } 239 } 240 241 /** 242 * Execute filesystem request. 243 * 244 * @param string $action Action to perform. 245 * @param string $file File to perform upon. 246 * @param array $params Argument for action. 247 * 248 * @return bool|void 249 */ 250 public function execute( string $action, string $file = '', array $params = array() ) { 251 if ( empty( $this->parent->args ) ) { 252 return; 253 } 254 255 if ( ! empty( $params ) ) { 256 // phpcs:ignore WordPress.PHP.DontExtract 257 extract( $params ); 258 } 259 260 if ( empty( $this->wp_filesystem ) ) { 261 if ( 'submenu' === $this->parent->args['menu_type'] ) { 262 $page_parent = $this->parent->args['page_parent']; 263 $base = $page_parent . '?page=' . $this->parent->args['page_slug']; 264 } else { 265 $base = 'admin.php?page=' . $this->parent->args['page_slug']; 266 } 267 268 $url = wp_nonce_url( $base, 'redux-options' ); 269 $this->advanced_filesystem_init( $url, 'direct', dirname( $file ) ); 270 } 271 272 return $this->do_action( $action, $file, $params ); 273 } 274 275 276 /** 277 * Generates the default Redux cache folder. 278 * 279 * @return void 280 */ 281 private function generate_default_files() { 282 283 // Set default permissions. 284 if ( defined( 'FS_CHMOD_DIR' ) ) { 285 $this->chmod_dir = FS_CHMOD_DIR; 286 } else { 287 $this->chmod_dir = ( fileperms( ABSPATH ) & 0777 | 0755 ); 288 } 289 290 if ( defined( 'FS_CHMOD_FILE' ) ) { 291 $this->chmod_file = FS_CHMOD_FILE; 292 } else { 293 $this->chmod_file = ( fileperms( ABSPATH . 'index.php' ) & 0777 | 0644 ); 294 } 295 296 if ( ! $this->is_dir( Redux_Core::$upload_dir ) ) { 297 $this->mkdir( Redux_Core::$upload_dir ); 298 } 299 300 $hash_path = trailingslashit( Redux_Core::$upload_dir ) . 'hash'; 301 if ( ! $this->file_exists( $hash_path ) ) { 302 $this->put_contents( $hash_path, Redux_Helpers::get_hash() ); 303 } 304 305 $version_path = trailingslashit( Redux_Core::$upload_dir ) . 'version'; 306 if ( ! $this->file_exists( $version_path ) ) { 307 $this->put_contents( $version_path, Redux_Core::$version ); 308 } else { 309 $version_compare = $this->get_contents( $version_path ); 310 if ( (string) Redux_Core::$version !== $version_compare ) { 311 $this->put_contents( $version_path, Redux_Core::$version ); 312 } 313 } 314 } 315 316 /** 317 * Do request filesystem action. 318 * 319 * @param string $action Requested action. 320 * @param string $file File to perform action upon. 321 * @param array $params Action arguments. 322 * 323 * @return bool|void 324 */ 325 public function do_action( string $action, string $file = '', array $params = array() ) { 326 if ( ! empty( $params ) ) { 327 328 // phpcs:ignore WordPress.PHP.DontExtract 329 extract( $params ); 330 } 331 332 global $wp_filesystem; 333 334 if ( defined( 'FS_CHMOD_FILE' ) ) { 335 $chmod = FS_CHMOD_FILE; 336 } else { 337 $chmod = 0644; 338 } 339 340 if ( isset( $params['chmod'] ) && ! empty( $params['chmod'] ) ) { 341 $chmod = $params['chmod']; 342 } 343 $res = false; 344 if ( ! isset( $recursive ) ) { 345 $recursive = false; 346 } 347 348 // Do unique stuff. 349 if ( 'mkdir' === $action ) { 350 $chmod = null; 351 if ( isset( $params['chmod'] ) && ! empty( $params['chmod'] ) ) { 352 $chmod = $params['chmod']; 353 } 354 $res = $this->mkdir( $file, $chmod ); 355 } elseif ( 'rmdir' === $action ) { 356 $res = $this->rmdir( $file, $recursive ); 357 } elseif ( 'copy' === $action && ! isset( $this->wp_filesystem->killswitch ) ) { 358 $res = $this->copy( $file, $destination, $overwrite, $chmod ); 359 } elseif ( 'move' === $action && ! isset( $this->wp_filesystem->killswitch ) ) { 360 $res = $this->move( $file, $destination, $overwrite ); 361 } elseif ( 'delete' === $action ) { 362 if ( $this->is_dir( $file ) ) { 363 $res = $this->rmdir( $file, $recursive ); 364 } else { 365 $res = $this->unlink( $file ); 366 } 367 } elseif ( 'rmdir' === $action ) { 368 $res = $this->rmdir( $file, $recursive ); 369 } elseif ( 'dirlist' === $action ) { 370 if ( ! isset( $include_hidden ) ) { 371 $include_hidden = true; 372 } 373 $res = $this->scandir( $file, $include_hidden, $recursive ); 374 } elseif ( 'put_contents' === $action && ! isset( $this->wp_filesystem->killswitch ) ) { 375 // Write a string to a file. 376 if ( isset( $this->parent->ftp_form ) && ! empty( $this->parent->ftp_form ) ) { 377 self::load_direct(); 378 $res = self::$direct->put_contents( $file, $content, $chmod ); 379 } else { 380 $res = $this->put_contents( $file, $content, $chmod ); 381 } 382 } elseif ( 'chown' === $action ) { 383 // Changes file owner. 384 if ( isset( $owner ) && ! empty( $owner ) ) { 385 $res = $wp_filesystem->chmod( $file, $chmod, $recursive ); 386 } 387 } elseif ( 'owner' === $action ) { 388 // Gets file owner. 389 $res = $this->wp_filesystem->owner( $file ); 390 } elseif ( 'chmod' === $action ) { 391 if ( ! isset( $params['chmod'] ) || ( isset( $params['chmod'] ) && empty( $params['chmod'] ) ) ) { 392 $chmod = false; 393 } 394 395 $res = $this->chmod( $file, $chmod, $recursive ); 396 } elseif ( 'get_contents' === $action ) { 397 // Reads entire file into a string. 398 if ( isset( $this->parent->ftp_form ) && ! empty( $this->parent->ftp_form ) ) { 399 self::load_direct(); 400 $res = self::$direct->get_contents( $file ); 401 } else { 402 $res = $this->get_contents( $file ); 403 } 404 } elseif ( 'get_contents_array' === $action ) { 405 // Reads entire file into an array. 406 $res = $this->wp_filesystem->get_contents_array( $file ); 407 } elseif ( 'object' === $action ) { 408 $res = $this->wp_filesystem; 409 } elseif ( 'unzip' === $action ) { 410 $unzipfile = unzip_file( $file, $destination ); 411 if ( $unzipfile ) { 412 $res = true; 413 } 414 } 415 416 if ( ! $res ) { 417 if ( 'dirlist' === $action ) { 418 if ( empty( $res ) || '' === $res ) { 419 return; 420 } 421 422 if ( ! is_array( $res ) ) { 423 if ( count( glob( "$file*" ) ) === 0 ) { 424 return; 425 } 426 } 427 } 428 429 $this->killswitch = true; 430 431 // translators: %1$s: Upload URL. %2$s: Codex URL. 432 $msg = '<strong>' . esc_html__( 'File Permission Issues', 'redux-framework' ) . '</strong><br/>' . sprintf( esc_html__( 'We were unable to modify required files. Please ensure that %1$s has the proper read-write permissions, or modify your wp-config.php file to contain your FTP login credentials as %2$s.', 'redux-framework' ), '<code>' . esc_url( Redux_Functions_Ex::wp_normalize_path( trailingslashit( WP_CONTENT_DIR ) ) ) . '/uploads/</code>', '<a href="https://codex.wordpress.org/Editing_wp-config.php#WordPress_Upgrade_Constants" target="_blank">' . esc_html__( 'outlined here', 'redux-framework' ) . '</a>' ); 433 434 $data = array( 435 'parent' => self::$instance->parent, 436 'type' => 'error', 437 'msg' => $msg, 438 'id' => 'redux-wp-login', 439 'dismiss' => false, 440 ); 441 442 Redux_Admin_Notices::set_notice( $data ); 443 } 444 445 return $res; 446 } 447 448 449 /** 450 * Getter for the instantiated WP_Filesystem. This should be used carefully since $wp_filesystem won't always have a value. 451 * 452 * @return WP_Filesystem|false 453 */ 454 public function get_wp_filesystem() { 455 if ( $this->use_filesystem ) { 456 return $this->wp_filesystem; 457 } else { 458 return false; 459 } 460 } 461 462 /** 463 * Check if WP_Filesystem being used. 464 * 465 * @return bool 466 */ 467 public function using_wp_filesystem(): bool { 468 return $this->use_filesystem; 469 } 470 471 /** 472 * Attempts to use the correct path for the FS method being used. 473 * 474 * @param string $abs_path Absolute path. 475 * 476 * @return string 477 */ 478 public function get_sanitized_path( string $abs_path ): string { 479 if ( $this->using_wp_filesystem() ) { 480 return str_replace( ABSPATH, $this->wp_filesystem->abspath(), $abs_path ); 481 } 482 483 return $abs_path; 484 } 485 486 /** 487 * Create file if not exists then set mtime and atime on file 488 * 489 * @param string $abs_path Absolute path. 490 * @param int $time Time. 491 * @param int $atime Altered time. 492 * 493 * @return bool 494 */ 495 public function touch( string $abs_path, int $time = 0, int $atime = 0 ): bool { 496 if ( 0 === $time ) { 497 $time = time(); 498 } 499 500 if ( 0 === $atime ) { 501 $atime = time(); 502 } 503 504 // phpcs:ignore WordPress.PHP.NoSilencedErrors 505 $return = @touch( $abs_path, $time, $atime ); 506 507 if ( ! $return && $this->use_filesystem ) { 508 $abs_path = $this->get_sanitized_path( $abs_path ); 509 $return = $this->wp_filesystem->touch( $abs_path, $time, $atime ); 510 } 511 512 return $return; 513 } 514 515 /** 516 * Calls file_put_contents with chmod. 517 * 518 * @param string $abs_path Absolute path. 519 * @param string $contents Content to write to the file. 520 * @param string|null $perms Default permissions value. 521 * 522 * @return bool 523 */ 524 public function put_contents( string $abs_path, string $contents, string $perms = null ): bool { 525 526 if ( ! $this->is_dir( dirname( $abs_path ) ) ) { 527 $this->mkdir( dirname( $abs_path ) ); 528 } 529 530 // phpcs:ignore WordPress.PHP.NoSilencedErrors 531 // @codingStandardsIgnoreStart 532 $return = @file_put_contents( $abs_path, $contents ); 533 // @codingStandardsIgnoreEnd 534 $this->chmod( $abs_path ); 535 536 if ( null === $perms ) { 537 $perms = $this->chmod_file; 538 } 539 540 if ( ! $return && $this->use_filesystem ) { 541 $abs_path = $this->get_sanitized_path( $abs_path ); 542 $return = $this->wp_filesystem->put_contents( $abs_path, $contents, $perms ); 543 } 544 545 return (bool) $return; 546 } 547 548 /** 549 * Calls file_put_contents with chmod. 550 * 551 * @param string $path Get full cache path. 552 * 553 * @return string 554 */ 555 public function get_cache_path( string $path ): string { 556 return $this->folder . $path; 557 } 558 559 /** 560 * Calls file_put_contents with chmod in cache directory. 561 * 562 * @param string $abs_path Absolute path. 563 * @param string $contents Contents to put in the cache. 564 * 565 * @return bool 566 */ 567 public function put_contents_cache( string $abs_path, string $contents ): bool { 568 return $this->put_contents( $this->get_cache_path( $abs_path ), $contents ); 569 } 570 571 /** 572 * Does the specified file or dir exist. 573 * 574 * @param string $abs_path Absolute path. 575 * @return bool 576 */ 577 public function file_exists( string $abs_path ): bool { 578 $return = file_exists( $abs_path ); 579 580 if ( ! $return && $this->use_filesystem ) { 581 $abs_path = $this->get_sanitized_path( $abs_path ); 582 $return = $this->wp_filesystem->exists( $abs_path ); 583 } 584 585 return (bool) $return; 586 } 587 588 /** 589 * Get a file's size. 590 * 591 * @param string $abs_path Absolute path. 592 * 593 * @return int 594 */ 595 public function filesize( string $abs_path ): int { 596 $return = filesize( $abs_path ); 597 598 if ( ! $return && $this->use_filesystem ) { 599 $abs_path = $this->get_sanitized_path( $abs_path ); 600 $return = $this->wp_filesystem->size( $abs_path ); 601 } 602 603 return $return; 604 } 605 606 /** 607 * Get the contents of a file as a string. 608 * 609 * @param string $abs_path Absolute path. 610 * 611 * @return string 612 */ 613 public function get_local_file_contents( string $abs_path ): string { 614 615 try { 616 ob_start(); 617 if ( $this->file_exists( $abs_path ) && is_file( $abs_path ) ) { 618 require_once $abs_path; 619 } 620 $contents = ob_get_clean(); 621 } catch ( Exception $e ) { 622 // This means that ob_start has been disabled on the system. Lets fallback to good old file_get_contents. 623 $contents = file_get_contents( $abs_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents 624 } 625 626 return $contents; 627 } 628 629 /** 630 * Get the contents of a file as a string. 631 * 632 * @param string $abs_path Absolute path. 633 * 634 * @return string 635 */ 636 public function get_contents( string $abs_path ): string { 637 $abs_path = $this->get_sanitized_path( $abs_path ); 638 $return = ''; 639 if ( $this->use_filesystem ) { 640 $return = $this->wp_filesystem->get_contents( $abs_path ); 641 } 642 if ( empty( $return ) ) { 643 $return = $this->get_local_file_contents( $abs_path ); 644 } 645 646 return $return; 647 } 648 649 /** 650 * Delete a file. 651 * 652 * @param string $abs_path Absolute path. 653 * 654 * @return bool 655 */ 656 public function unlink( string $abs_path ): bool { 657 // phpcs:ignore WordPress.PHP.NoSilencedErrors 658 $return = @unlink( $abs_path ); 659 660 if ( ! $return && $this->use_filesystem ) { 661 $abs_path = $this->get_sanitized_path( $abs_path ); 662 $return = $this->wp_filesystem->delete( $abs_path, false, false ); 663 } 664 665 return $return; 666 } 667 668 /** 669 * Chmod a file. 670 * 671 * @param string $abs_path Absolute path. 672 * @param int|null $perms Permission value, if not provided, defaults to WP standards. 673 * 674 * @return bool 675 */ 676 public function chmod( string $abs_path, int $perms = null ): bool { 677 if ( ! $this->file_exists( $abs_path ) ) { 678 return false; 679 } 680 if ( is_null( $perms ) ) { 681 $perms = $this->is_file( $abs_path ) ? $this->chmod_file : $this->chmod_dir; 682 } 683 // phpcs:ignore WordPress.PHP.NoSilencedErrors 684 $return = @chmod( $abs_path, $perms ); 685 686 if ( ! $return && $this->use_filesystem ) { 687 $abs_path = $this->get_sanitized_path( $abs_path ); 688 $return = $this->wp_filesystem->chmod( $abs_path, $perms, false ); 689 } 690 691 return $return; 692 } 693 694 /** 695 * Check if this path is a directory. 696 * 697 * @param string $abs_path Absolute path. 698 * 699 * @return bool 700 */ 701 public function is_dir( string $abs_path ): bool { 702 $return = is_dir( $abs_path ); 703 704 if ( ! $return && $this->use_filesystem ) { 705 $abs_path = $this->get_sanitized_path( $abs_path ); 706 $return = $this->wp_filesystem->is_dir( $abs_path ); 707 } 708 709 return $return; 710 } 711 712 /** 713 * Check if the specified path is a file. 714 * 715 * @param string $abs_path Absolute path. 716 * 717 * @return bool 718 */ 719 public function is_file( string $abs_path ): bool { 720 $return = is_file( $abs_path ); 721 722 if ( ! $return && $this->use_filesystem ) { 723 $abs_path = $this->get_sanitized_path( $abs_path ); 724 $return = $this->wp_filesystem->is_file( $abs_path ); 725 } 726 727 return $return; 728 } 729 730 /** 731 * Is the specified path readable. 732 * 733 * @param string $abs_path Absolute path. 734 * 735 * @return bool 736 */ 737 public function is_readable( string $abs_path ): bool { 738 $return = is_readable( $abs_path ); 739 740 if ( ! $return && $this->use_filesystem ) { 741 $abs_path = $this->get_sanitized_path( $abs_path ); 742 $return = $this->wp_filesystem->is_readable( $abs_path ); 743 } 744 745 return $return; 746 } 747 748 /** 749 * Is the specified path writable. 750 * 751 * @param string $abs_path Absolute path. 752 * 753 * @return bool 754 */ 755 public function is_writable( string $abs_path ): bool { 756 $return = is_writable( $abs_path ); 757 758 if ( ! $return && $this->use_filesystem ) { 759 $abs_path = $this->get_sanitized_path( $abs_path ); 760 $return = $this->wp_filesystem->is_writable( $abs_path ); 761 } 762 763 return $return; 764 } 765 766 /** 767 * Create an index file at the given path. 768 * 769 * @param string $path Directory to add the index to. 770 */ 771 private function create_index( string $path ) { 772 $index_path = trailingslashit( $path ) . 'index.php'; 773 if ( ! $this->file_exists( $index_path ) ) { 774 $this->put_contents( $index_path, "<?php\n//Silence is golden" ); 775 } 776 } 777 778 /** 779 * Recursive mkdir. 780 * 781 * @param string $abs_path Absolute path. 782 * @param int|null $perms Permissions, if default not required. 783 * 784 * @return bool 785 */ 786 public function mkdir( string $abs_path, int $perms = null ): bool { 787 if ( is_null( $perms ) ) { 788 $perms = $this->chmod_dir; 789 } 790 791 if ( $this->is_dir( $abs_path ) ) { 792 $this->chmod( $abs_path, $perms ); 793 $this->create_index( $abs_path ); 794 795 return true; 796 } 797 798 try { 799 $mkdirp = wp_mkdir_p( $abs_path ); 800 } catch ( Exception $e ) { 801 $mkdirp = false; 802 } 803 804 if ( $mkdirp ) { 805 $this->chmod( $abs_path, $perms ); 806 $this->create_index( $abs_path ); 807 808 return true; 809 } 810 // phpcs:ignore WordPress.PHP.NoSilencedErrors 811 $return = @mkdir( $abs_path, $perms, true ); 812 813 if ( ! $return && $this->use_filesystem ) { 814 $abs_path = $this->get_sanitized_path( $abs_path ); 815 816 if ( $this->is_dir( $abs_path ) ) { 817 $this->create_index( $abs_path ); 818 819 return true; 820 } 821 822 // WP_Filesystem doesn't offer a recursive mkdir(). 823 $abs_path = str_replace( '//', '/', $abs_path ); 824 $abs_path = rtrim( $abs_path, '/' ); 825 if ( empty( $abs_path ) ) { 826 $abs_path = '/'; 827 } 828 829 $dirs = explode( '/', ltrim( $abs_path, '/' ) ); 830 $current_dir = ''; 831 832 foreach ( $dirs as $dir ) { 833 $current_dir .= '/' . $dir; 834 if ( ! $this->is_dir( $current_dir ) ) { 835 $this->wp_filesystem->mkdir( $current_dir, $perms ); 836 } 837 } 838 839 $return = $this->is_dir( $abs_path ); 840 } 841 842 return $return; 843 } 844 845 /** 846 * Delete a directory. 847 * 848 * @param string $abs_path Absolute path. 849 * @param bool $recursive Set to recursive create. 850 * 851 * @return bool 852 */ 853 public function rmdir( string $abs_path, bool $recursive = false ): bool { 854 if ( ! $this->is_dir( $abs_path ) ) { 855 return false; 856 } 857 858 // Taken from WP_Filesystem_Direct. 859 if ( ! $recursive ) { 860 // phpcs:ignore WordPress.PHP.NoSilencedErrors 861 $return = @rmdir( $abs_path ); 862 } else { 863 864 // At this point it's a folder, and we're in recursive mode. 865 $abs_path = trailingslashit( $abs_path ); 866 $filelist = $this->scandir( $abs_path ); 867 868 $return = true; 869 if ( is_array( $filelist ) ) { 870 foreach ( $filelist as $filename => $fileinfo ) { 871 872 if ( 'd' === $fileinfo['type'] ) { 873 $return = $this->rmdir( $abs_path . $filename, $recursive ); 874 } else { 875 $return = $this->unlink( $abs_path . $filename ); 876 } 877 } 878 } 879 // phpcs:ignore WordPress.PHP.NoSilencedErrors 880 if ( file_exists( $abs_path ) && ! @rmdir( $abs_path ) ) { 881 $return = false; 882 } 883 } 884 885 if ( ! $return && $this->use_filesystem ) { 886 $abs_path = $this->get_sanitized_path( $abs_path ); 887 888 return $this->wp_filesystem->rmdir( $abs_path, $recursive ); 889 } 890 891 return $return; 892 893 } 894 895 /** 896 * Get a list of files/folders under specified directory. 897 * 898 * @param string $abs_path Absolute path. 899 * @param bool $include_hidden Include hidden files, defaults to true. 900 * @param bool $recursive Recursive search, defaults to false. 901 * 902 * @return array|bool 903 */ 904 public function scandir( string $abs_path, bool $include_hidden = true, bool $recursive = false ) { 905 // phpcs:ignore WordPress.PHP.NoSilencedErrors 906 $dirlist = @scandir( $abs_path ); 907 if ( false === $dirlist ) { 908 if ( $this->use_filesystem ) { 909 $abs_path = $this->get_sanitized_path( $abs_path ); 910 911 return $this->wp_filesystem->dirlist( $abs_path, $include_hidden, $recursive ); 912 } 913 914 return false; 915 } 916 917 $return = array(); 918 919 // Normalize return to look somewhat like the return value for WP_Filesystem::dirlist. 920 foreach ( $dirlist as $entry ) { 921 if ( '.' === $entry || '..' === $entry ) { 922 continue; 923 } 924 $return[ $entry ] = array( 925 'name' => $entry, 926 'type' => $this->is_dir( $abs_path . '/' . $entry ) ? 'd' : 'f', 927 ); 928 } 929 930 return $return; 931 932 } 933 934 /** 935 * Light wrapper for move_uploaded_file with chmod. 936 * 937 * @param string $file Source file. 938 * @param string $destination File destination. 939 * @param int|null $perms Permission value. 940 * 941 * @return bool 942 */ 943 public function move_uploaded_file( string $file, string $destination, int $perms = null ): bool { 944 // TODO: look into replicating more functionality from wp_handle_upload(). 945 // phpcs:ignore WordPress.PHP.NoSilencedErrors 946 $return = @move_uploaded_file( $file, $destination ); 947 948 if ( $return ) { 949 $this->chmod( $destination, $perms ); 950 } 951 952 return $return; 953 } 954 955 /** 956 * Copy a file. 957 * 958 * @param string $source_abs_path Source path. 959 * @param string $destination_abs_path Destination path. 960 * @param bool $overwrite Overwrite file. 961 * @param mixed $perms Permission value. 962 * @return bool 963 * Taken from WP_Filesystem_Direct 964 */ 965 public function copy( string $source_abs_path, string $destination_abs_path, bool $overwrite = true, $perms = false ): bool { 966 967 // Error if source file doesn't exist. 968 if ( ! $this->file_exists( $source_abs_path ) ) { 969 return false; 970 } 971 972 if ( ! $overwrite && $this->file_exists( $destination_abs_path ) ) { 973 return false; 974 } 975 if ( ! $this->is_dir( dirname( $destination_abs_path ) ) ) { 976 $this->mkdir( dirname( $destination_abs_path ) ); 977 } 978 979 // phpcs:ignore WordPress.PHP.NoSilencedErrors 980 $return = @copy( $source_abs_path, $destination_abs_path ); 981 if ( $perms && $return ) { 982 $this->chmod( $destination_abs_path, $perms ); 983 } 984 985 if ( ! $return && $this->use_filesystem ) { 986 $source_abs_path = $this->get_sanitized_path( $source_abs_path ); 987 $destination_abs_path = $this->get_sanitized_path( $destination_abs_path ); 988 $return = $this->wp_filesystem->copy( 989 $source_abs_path, 990 $destination_abs_path, 991 $overwrite, 992 $perms 993 ); 994 } 995 996 return $return; 997 } 998 999 /** 1000 * Move a file. 1001 * 1002 * @param string $source_abs_path Source absolute path. 1003 * @param string $destination_abs_path Destination absolute path. 1004 * @param bool $overwrite Overwrite if file exists. 1005 * @return bool 1006 */ 1007 public function move( string $source_abs_path, string $destination_abs_path, bool $overwrite = true ): bool { 1008 1009 // Error if source file doesn't exist. 1010 if ( ! $this->file_exists( $source_abs_path ) ) { 1011 return false; 1012 } 1013 1014 // Try using rename first. if that fails (for example, source is read only) try copy. 1015 // Taken in part from WP_Filesystem_Direct. 1016 if ( ! $overwrite && $this->file_exists( $destination_abs_path ) ) { 1017 return false; 1018 } elseif ( @rename( $source_abs_path, $destination_abs_path ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors 1019 return true; 1020 } else { 1021 if ( $this->copy( $source_abs_path, $destination_abs_path, $overwrite ) && $this->file_exists( 1022 $destination_abs_path 1023 ) ) { 1024 $this->unlink( $source_abs_path ); 1025 1026 return true; 1027 } else { 1028 $return = false; 1029 } 1030 } 1031 1032 if ( $this->use_filesystem ) { 1033 $source_abs_path = $this->get_sanitized_path( $source_abs_path ); 1034 $destination_abs_path = $this->get_sanitized_path( $destination_abs_path ); 1035 1036 $return = $this->wp_filesystem->move( $source_abs_path, $destination_abs_path, $overwrite ); 1037 } 1038 1039 return $return; 1040 } 1041 1042 /** 1043 * Shim: get_template. 1044 * 1045 * @param string $file Template name. 1046 * 1047 * @return void Path to template file. 1048 */ 1049 public function get_template( string $file ) { 1050 $panel = new Redux_Panel( $this ); 1051 $panel->get_template( $file ); 1052 } 1053 } 1054 1055 Redux_Filesystem::get_instance(); 1056 }