diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index c4ecb81f66160..40ba926857c6b 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -2027,6 +2027,7 @@ function wp_get_original_referer() { * Will attempt to set permissions on folders. * * @since 2.0.1 + * @since 6.5.0 Added `before_create_directory`, `after_create_directory` and `create_directory_failed` hooks. * * @param string $target Full path to attempt to create. * @return bool Whether the path was created. True if path already exists. @@ -2079,6 +2080,16 @@ function wp_mkdir_p( $target ) { $dir_perms = 0777; } + /** + * Fires before the directory creation. + * + * @since 6.5.0 + * + * @param string $target Full path to attempt to create. + * @param int $dir_perms Directory permissions. + */ + do_action( 'before_create_directory', $target, $dir_perms ); + if ( @mkdir( $target, $dir_perms, true ) ) { /* @@ -2092,9 +2103,29 @@ function wp_mkdir_p( $target ) { } } + /** + * Fires after the directory is created and the permissions are set. + * + * @since 6.5.0 + * + * @param string $target Full path to created directory. + * @param int $dir_perms Directory permissions. + */ + do_action( 'after_create_directory', $target, $dir_perms ); + return true; } + /** + * Fires when directory creation fails. + * + * @since 6.5.0 + * + * @param string $target Full path to the directory that failed to create. + * @param int $dir_perms Directory permissions. + */ + do_action( 'create_directory_failed', $target, $dir_perms ); + return false; } diff --git a/tests/phpunit/tests/filesystem/wpMkdirP.php b/tests/phpunit/tests/filesystem/wpMkdirP.php new file mode 100644 index 0000000000000..e26e17dbb65d1 --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpMkdirP.php @@ -0,0 +1,196 @@ +files_in_dir( self::$test_directory ) as $file ) { + $this->unlink( $file ); + } + + $matched_dirs = $this->scandir( self::$test_directory ); + foreach ( array_reverse( $matched_dirs ) as $dir ) { + rmdir( $dir ); + } + + parent::tear_down(); + } + + /** + * Deletes the test directory after all tests have run. + */ + public static function tear_down_after_class() { + rmdir( self::$test_directory ); + + parent::tear_down_after_class(); + } + + /** + * Tests that `wp_mkdir_p()` fires an action. + * + * @ticket 44083 + * + * @dataProvider data_actions + * + * @param string $hook_name The name of the action hook. + */ + public function test_wp_mkdir_p_should_fire_action( $hook_name ) { + $action = new MockAction(); + add_action( $hook_name, array( $action, 'action' ) ); + + wp_mkdir_p( self::$test_directory . $hook_name ); + + $this->assertSame( 1, $action->get_call_count() ); + } + + /** + * Tests that `wp_mkdir_p()` does not fire an action when a file exists of the same name. + * + * @ticket 44083 + * + * @dataProvider data_actions + * + * @param string $hook_name The name of the action hook. + */ + public function test_wp_mkdir_p_should_not_fire_action_when_a_file_exists_of_the_same_name( $hook_name ) { + $action = new MockAction(); + add_action( $hook_name, array( $action, 'action' ) ); + + $target = self::$test_directory . $hook_name; + + // Force a failure. + $this->touch( $target ); + + wp_mkdir_p( $target ); + + $this->assertSame( 0, $action->get_call_count() ); + } + + /** + * Tests that `wp_mkdir_p()` does not fire an action when the target directory contains '../'. + * + * @ticket 44083 + * + * @dataProvider data_actions + * + * @param string $hook_name The name of the action hook. + */ + public function test_wp_mkdir_p_should_not_fire_action_when_the_target_contains_path_traversal( $hook_name ) { + $action = new MockAction(); + add_action( $hook_name, array( $action, 'action' ) ); + + // Force a failure by not using `realpath()`. + $target = DIR_TESTDATA . "/test_wp_mkdir_p/$hook_name/"; + + wp_mkdir_p( $target ); + + $this->assertSame( 0, $action->get_call_count() ); + } + + /** + * Tests that `wp_mkdir_p()` does not fire an action when the target directory contains '..DIRECTORY_SEPARATOR'. + * + * @ticket 44083 + * + * @dataProvider data_actions + * + * @param string $hook_name The name of the action hook. + */ + public function test_wp_mkdir_p_should_not_fire_action_when_the_target_contains_path_traversal_with_directory_separator( $hook_name ) { + $action = new MockAction(); + add_action( $hook_name, array( $action, 'action' ) ); + + // Force a failure. + $target = str_replace( '../', '..' . DIRECTORY_SEPARATOR, DIR_TESTDATA ) . "/test_wp_mkdir_p/$hook_name"; + + wp_mkdir_p( $target ); + + $this->assertSame( 0, $action->get_call_count() ); + } + + /** + * Data provider. + * + * @throws Exception + * + * @return array[] + */ + public function data_actions() { + return self::text_array_to_dataprovider( array( 'before_create_directory', 'after_create_directory' ) ); + } + + /** + * Tests that `wp_mkdir_p()` does not fire the 'after_create_directory' action when `mkdir()` fails. + * + * @ticket 44083 + */ + public function test_wp_mkdir_p_should_not_fire_the_after_create_directory_action_when_mkdir_fails() { + $action = new MockAction(); + add_action( 'after_create_directory', array( $action, 'action' ) ); + + add_action( + 'before_create_directory', + function ( $target ) { + /* + * Force a failure by creating a file of the same name + * just before `mkdir()` runs. + */ + $this->touch( $target ); + } + ); + + wp_mkdir_p( self::$test_directory . 'after_create_directory' ); + + $this->assertSame( 0, $action->get_call_count() ); + } + + /** + * Tests that `wp_mkdir_p()` fires the 'create_directory_failed' action when `mkdir()` fails. + * + * @ticket 44083 + */ + public function test_wp_mkdir_p_should_fire_the_create_directory_failed_action_when_mkdir_fails() { + $action = new MockAction(); + add_action( 'create_directory_failed', array( $action, 'action' ) ); + + add_action( + 'before_create_directory', + function ( $target ) { + /* + * Force a failure by creating a file of the same name + * just before `mkdir()` runs. + */ + $this->touch( $target ); + } + ); + + wp_mkdir_p( self::$test_directory . 'create_directory_failed' ); + + $this->assertSame( 1, $action->get_call_count() ); + } +}