<?php
/**
 * iThemes Security directory library.
 *
 * Contains the ITSEC_Lib_Directory class.
 *
 * @package iThemes_Security
 */

if ( ! class_exists( 'ITSEC_Lib_Directory' ) ) {
	/**
	 * iThemes Security Directory Library class.
	 *
	 * Utility class for managing directories.
	 *
	 * @package iThemes_Security
	 * @since 1.15.0
	 */
	class ITSEC_Lib_Directory {
		/**
		 * Get a listing of files and subdirectories contained in the supplied directory.
		 *
		 * @since 1.15.0
		 *
		 * @return array|WP_Error An array of the files and directories on success or a WP_Error object if an error occurs.
		 */
		public static function read( $dir ) {
			$callable = array();

			if ( ITSEC_Lib_Utility::is_callable_function( 'opendir' ) && ITSEC_Lib_Utility::is_callable_function( 'readdir' ) && ITSEC_Lib_Utility::is_callable_function( 'closedir' ) ) {
				$callable[] = 'opendir';
			}
			if ( ITSEC_Lib_Utility::is_callable_function( 'glob' ) ) {
				$callable[] = 'glob';
			}

			if ( empty( $callable ) ) {
				return new WP_Error( 'itsec-lib-directory-read-no-callable-functions', sprintf( __( '%s could not be read. Both the opendir/readdir/closedir and glob functions are disabled on the server.', 'better-wp-security' ), $dir ) );
			}


			if ( in_array( 'opendir', $callable ) ) {
				if ( false !== ( $dh = opendir( $dir ) ) ) {
					$files = array();

					while ( false !== ( $file = readdir( $dh ) ) ) {
						if ( in_array( basename( $file ), array( '.', '..' ) ) ) {
							continue;
						}

						$files[] = "$dir/$file";
					}

					closedir( $dh );

					sort( $files );

					return $files;
				}
			}

			if ( ITSEC_Lib_Utility::is_callable_function( 'glob' ) ) {
				$visible = glob( "$dir/*" );
				$hidden = glob( "$dir/.*" );

				if ( false !== $visible || false !== $hidden ) {
					if ( false === $visible ) {
						$visible = array();
					}
					if ( false === $hidden ) {
						$hidden = array();
					}

					$files = array_merge( $visible, $hidden );

					foreach ( $files as $index => $file ) {
						if ( in_array( basename( $file ), array( '.', '..' ) ) ) {
							unset( $files[$index] );
						}
					}

					sort( $files );

					return $files;
				}
			}


			return new WP_Error( 'itsec-lib-directory-read-cannot-read', sprintf( __( '%s could not be read due to an unknown error.', 'better-wp-security' ), $dir ) );
		}

		/**
		 * Determine if the supplied directory exists.
		 *
		 * @since 1.15.0
		 *
		 * @param string $dir Full path to the directory to test.
		 * @return bool|WP_Error Boolean true if it exists, false if it does not
		 */
		public static function is_dir( $dir ) {
			$dir = rtrim( $dir, '/' );

			if ( empty( $dir ) ) {
				return true;
			}

			// phpcs:ignore -- Have Tide ignore the following line. We use arguments that don't exist in early versions, but these versions ignore the arguments.
			@clearstatcache( true, $dir );

			return @is_dir( $dir );
		}

		/**
		 * Create the supplied directory.
		 *
		 * @since 1.15.0
		 *
		 * @param string $dir Full path to the directory to create.
		 * @return bool|WP_Error Boolean true if it is created successfully, WP_Error object otherwise.
		 */
		public static function create( $dir ) {
			$dir = rtrim( $dir, '/' );

			if ( self::is_dir( $dir ) ) {
				self::add_file_listing_protection( $dir );

				return true;
			}

			if ( ITSEC_Lib_File::exists( $dir ) ) {
				return new WP_Error( 'itsec-lib-directory-create-file-exists', sprintf( __( 'The directory %s could not be created as a file with that name already exists.', 'better-wp-security' ), $dir ) );
			}

			if ( ! ITSEC_Lib_Utility::is_callable_function( 'mkdir' ) ) {
				return new WP_Error( 'itsec-lib-directory-create-mkdir-is-disabled', sprintf( __( 'The directory %s could not be created as the mkdir() function is disabled. This is a system configuration issue.', 'better-wp-security' ), $dir ) );
			}


			$parent = dirname( $dir );

			while ( ! empty( $parent ) && ! self::is_dir( $parent ) && dirname( $parent ) !== $parent ) {
				$parent = dirname( $parent );
			}

			if ( empty( $parent ) ) {
				return new WP_Error( 'itsec-lib-directory-create-unable-to-find-parent', sprintf( __( 'The directory %s could not be created as an existing parent directory could not be found.', 'better-wp-security' ), $dir ) );
			}


			$perms = self::get_permissions( $parent );

			if ( ! is_int( $perms ) ) {
				$perms = 0755;
			}

			$cached_umask = umask( 0 );
			$result = @mkdir( $dir, $perms, true );
			umask( $cached_umask );

			if ( $result ) {
				self::add_file_listing_protection( $dir );

				return true;
			}

			return new WP_Error( 'itsec-lib-directory-create-failed', sprintf( __( 'The directory %s could not be created due to an unknown error. This could be due to a permissions issue.', 'better-wp-security' ), $dir ) );
		}

		/**
		 * Recursively remove the supplied directory.
		 *
		 * @since 1.15.0
		 *
		 * @param string $dir
		 *
		 * @return bool|WP_Error Boolean true on success or a WP_Error object if an error occurs.
		 */
		public static function remove( $dir ) {
			$emptied = self::empty_directory( $dir );

			if ( is_wp_error( $emptied ) ) {
				return $emptied;
			}

			$result = rmdir( $dir );
			// phpcs:ignore -- Have Tide ignore the following line. We use arguments that don't exist in early versions, but these versions ignore the arguments.
			@clearstatcache( true, $dir );

			if ( $result ) {
				return true;
			}

			return new WP_Error( 'itsec-lib-directory-remove-unknown-error', sprintf( __( 'Unable to remove %s due to an unknown error.', 'better-wp-security' ), $dir ) );
		}

		/**
		 * Empty a directory of all it's contents, but don't delete the directory.
		 *
		 * @param string $dir
		 *
		 * @return bool|WP_Error
		 */
		public static function empty_directory( $dir ) {
			if ( ! ITSEC_Lib_File::exists( $dir ) ) {
				return true;
			}

			if ( ! ITSEC_Lib_Utility::is_callable_function( 'rmdir' ) ) {
				return new WP_Error( 'itsec-lib-directory-remove-rmdir-is-disabled', sprintf( __( 'The directory %s could not be removed as the rmdir() function is disabled. This is a system configuration issue.', 'better-wp-security' ), $dir ) );
			}

			$files = self::read( $dir );

			if ( is_wp_error( $files ) ) {
				return new WP_Error( 'itsec-lib-directory-remove-read-error', sprintf( __( 'Unable to remove %1$s due to the following error: %2$s', 'better-wp-security' ), $dir, $files->get_error_message() ) );
			}

			foreach ( $files as $file ) {
				if ( ITSEC_Lib_File::is_file( $file ) ) {
					ITSEC_Lib_File::remove( $file );
				} else if ( self::is_dir( $file ) ) {
					self::remove( $file );
				}
			}

			// phpcs:ignore -- Have Tide ignore the following line. We use arguments that don't exist in early versions, but these versions ignore the arguments.
			@clearstatcache( true, $dir );

			return true;
		}

		/**
		 * Creates a temporary directory.
		 *
		 * @return string|WP_Error
		 */
		public static function create_temp_directory() {
			$temp = get_temp_dir();

			do {
				$directory = $temp . wp_generate_password( 12, false );
			} while ( self::is_dir( $directory ) );

			$created = self::create( $directory );

			if ( is_wp_error( $created ) ) {
				return $created;
			}

			return trailingslashit( $directory );
		}

		/**
		 * Test to see if the requested directory is writable.
		 *
		 * @since 2.3.0
		 *
		 * @param string $dir Full path to the directory to test.
		 * @return bool True if the directory is writable, false otherwise.
		 */
		public static function is_writable( $dir ) {
			$dir = rtrim( $dir, '/' );

			if ( ! self::is_dir( $dir ) ) {
				return false;
			}

			$test_count = 0;

			do {
				$test_file = 'itsec-test-file-' . wp_generate_password( 10, false ) . '.txt';
			} while ( $test_count++ < 10 && ITSEC_Lib_File::exists( "$dir/$test_file" ) );

			if ( ITSEC_Lib_File::exists( "$dir/$test_file" ) ) {
				return false;
			}

			$result = ITSEC_Lib_File::write( "$dir/$test_file", __( 'This is a test file generated by iThemes Security. It can be removed.', 'better-wp-security' ) );
			ITSEC_Lib_File::remove( "$dir/$test_file" );

			if ( true === $result ) {
				return $result;
			}

			return false;
		}

		/**
		 * Add an index.php file to the directory to prevent file listing.
		 *
		 * @since 2.3.0
		 *
		 * @param string $dir Full path to the directory to protect.
		 * @return bool|WP_Error Boolean true if the file could be created or already exists, WP_Error object otherwise.
		 */
		public static function add_file_listing_protection( $dir ) {
			$dir = rtrim( $dir, '/' );

			if ( ! self::is_dir( $dir ) ) {
				return new WP_Error( 'itsec-lib-directory-add-file-listing-protection-directory-does-not-exist', sprintf( __( 'The directory %s could not be protected from file listing as the directory does not exist.', 'better-wp-security' ), $dir ) );
			}

			if ( ITSEC_Lib_File::exists( "$dir/index.php" ) ) {
				return true;
			}

			return ITSEC_Lib_File::write( "$dir/index.php", "<?php\n// Silence is golden." );
		}

		/**
		 * Get file permissions from the requested directory.
		 *
		 * If the directory permissions cannot be read, a default value of 0644 will be returned.
		 *
		 * @since 1.15.0
		 *
		 * @param string $dir Full path to the file to retrieve permissions from.
		 * @return int|WP_Error The permissions as an int or a WP_Error object if an error occurs.
		 */
		public static function get_permissions( $dir ) {
			if ( ! self::is_dir( $dir ) ) {
				return new WP_Error( 'itsec-lib-dir-get-permissions-missing-dir', sprintf( __( 'Permissions for the directory %s could not be read as the directory could not be found.', 'better-wp-security' ), $dir ) );
			}

			if ( ! ITSEC_Lib_Utility::is_callable_function( 'fileperms' ) ) {
				return new WP_Error( 'itsec-lib-directory-get-permissions-fileperms-is-disabled', sprintf( __( 'Permissions for the directory %s could not be read as the fileperms() function is disabled. This is a system configuration issue.', 'better-wp-security' ), $dir ) );
			}


			$dir = rtrim( $dir, '/' );
			// phpcs:ignore -- Have Tide ignore the following line. We use arguments that don't exist in early versions, but these versions ignore the arguments.
			@clearstatcache( true, $dir );

			return fileperms( $dir ) & 0777;
		}

		/**
		 * Get default file permissions to use for new files.
		 *
		 * @since 1.15.0
		 * @uses FS_CHMOD_DIR Define that sets default file permissions.
		 *
		 * @return int|WP_Error The default permissions as an int or a WP_Error object if an error occurs.
		 */
		public static function get_default_permissions() {
			if ( defined( 'FS_CHMOD_DIR' ) ) {
				return FS_CHMOD_DIR;
			}

			$perms = self::get_permissions( ABSPATH );

			if ( ! is_wp_error( $perms ) ) {
				return $perms;
			}

			return 0755;
		}

		/**
		 * Change directory permissions.
		 *
		 * @since 1.15.0
		 *
		 * @param string $dir   Full path to the directory to change permissions for.
		 * @param int    $perms New permissions to set.
		 * @return bool|WP_Error Boolean true if successful, false if not successful, or WP_Error if the chmod() function
		 *                       is unavailable.
		 */
		public static function chmod( $dir, $perms ) {
			return ITSEC_Lib_File::chmod( $dir, $perms );
		}
	}


	require_once( dirname( __FILE__ ) . '/class-itsec-lib-utility.php' );
	require_once( dirname( __FILE__ ) . '/class-itsec-lib-file.php' );
}
