$target_parent = dirname( $target );
while ( '.' !== $target_parent && ! is_dir( $target_parent ) && dirname( $target_parent ) !== $target_parent ) {
$target_parent = dirname( $target_parent );
// Get the permission bits.
$stat = @stat( $target_parent );
$dir_perms = $stat['mode'] & 0007777;
if ( @mkdir( $target, $dir_perms, true ) ) {
* If a umask is set that modifies $dir_perms, we'll have to re-set
* the $dir_perms correctly with chmod()
if ( ( $dir_perms & ~umask() ) != $dir_perms ) {
$folder_parts = explode( '/', substr( $target, strlen( $target_parent ) + 1 ) );
for ( $i = 1, $c = count( $folder_parts ); $i <= $c; $i++ ) {
chmod( $target_parent . '/' . implode( '/', array_slice( $folder_parts, 0, $i ) ), $dir_perms );
* Test if a given filesystem path is absolute.
* For example, '/foo/bar', or 'c:\windows'.
* @param string $path File path.
* @return bool True if path is absolute, false is not absolute.
function path_is_absolute( $path ) {
* Check to see if the path is a stream and check to see if its an actual
* path or file as realpath() does not support stream wrappers.
if ( wp_is_stream( $path ) && ( is_dir( $path ) || is_file( $path ) ) ) {
* This is definitive if true but fails if $path does not exist or contains
if ( realpath( $path ) == $path ) {
if ( strlen( $path ) == 0 || '.' === $path[0] ) {
// Windows allows absolute paths like this.
if ( preg_match( '#^[a-zA-Z]:\\\\#', $path ) ) {
// A path starting with / or \ is absolute; anything else is relative.
return ( '/' === $path[0] || '\\' === $path[0] );
* Join two filesystem paths together.
* For example, 'give me $path relative to $base'. If the $path is absolute,
* then it the full path is returned.
* @param string $base Base path.
* @param string $path Path relative to $base.
* @return string The path with the base or absolute path.
function path_join( $base, $path ) {
if ( path_is_absolute( $path ) ) {
return rtrim( $base, '/' ) . '/' . ltrim( $path, '/' );
* Normalize a filesystem path.
* On windows systems, replaces backslashes with forward slashes
* and forces upper-case drive letters.
* Allows for two leading slashes for Windows network shares, but
* ensures that all other duplicate slashes are reduced to a single.
* @since 4.4.0 Ensures upper-case drive letters on Windows systems.
* @since 4.5.0 Allows for Windows network shares.
* @since 4.9.7 Allows for PHP file wrappers.
* @param string $path Path to normalize.
* @return string Normalized path.
function wp_normalize_path( $path ) {
if ( wp_is_stream( $path ) ) {
list( $wrapper, $path ) = explode( '://', $path, 2 );
// Standardise all paths to use '/'.
$path = str_replace( '\\', '/', $path );
// Replace multiple slashes down to a singular, allowing for network shares having two slashes.
$path = preg_replace( '|(?<=.)/+|', '/', $path );
// Windows paths should uppercase the drive letter.
if ( ':' === substr( $path, 1, 1 ) ) {
$path = ucfirst( $path );
* Determine a writable directory for temporary files.
* Function's preference is the return value of sys_get_temp_dir(),
* followed by your PHP temporary upload directory, followed by WP_CONTENT_DIR,
* before finally defaulting to /tmp/
* In the event that this function does not find a writable location,
* It may be overridden by the WP_TEMP_DIR constant in your wp-config.php file.
* @return string Writable temporary directory.
function get_temp_dir() {
if ( defined( 'WP_TEMP_DIR' ) ) {
return trailingslashit( WP_TEMP_DIR );
return trailingslashit( $temp );
if ( function_exists( 'sys_get_temp_dir' ) ) {
$temp = sys_get_temp_dir();
if ( @is_dir( $temp ) && wp_is_writable( $temp ) ) {
return trailingslashit( $temp );
$temp = ini_get( 'upload_tmp_dir' );
if ( @is_dir( $temp ) && wp_is_writable( $temp ) ) {
return trailingslashit( $temp );
$temp = WP_CONTENT_DIR . '/';
if ( is_dir( $temp ) && wp_is_writable( $temp ) ) {
* Determine if a directory is writable.
* This function is used to work around certain ACL issues in PHP primarily
* affecting Windows Servers.
* @param string $path Path to check for write-ability.
* @return bool Whether the path is writable.
function wp_is_writable( $path ) {
if ( 'WIN' === strtoupper( substr( PHP_OS, 0, 3 ) ) ) {
return win_is_writable( $path );
return @is_writable( $path );
* Workaround for Windows bug in is_writable() function
* PHP has issues with Windows ACL's for determine if a
* directory is writable or not, this works around them by
* checking the ability to open files rather than relying
* upon PHP to interprate the OS ACL.
* @see https://bugs.php.net/bug.php?id=27609
* @see https://bugs.php.net/bug.php?id=30931
* @param string $path Windows path to check for write-ability.
* @return bool Whether the path is writable.
function win_is_writable( $path ) {
if ( '/' === $path[ strlen( $path ) - 1 ] ) {
// If it looks like a directory, check a random file within the directory.
return win_is_writable( $path . uniqid( mt_rand() ) . '.tmp' );
} elseif ( is_dir( $path ) ) {
// If it's a directory (and not a file), check a random file within the directory.
return win_is_writable( $path . '/' . uniqid( mt_rand() ) . '.tmp' );
// Check tmp file for read/write capabilities.
$should_delete_tmp_file = ! file_exists( $path );
$f = @fopen( $path, 'a' );
if ( $should_delete_tmp_file ) {
* Retrieves uploads directory information.
* Same as wp_upload_dir() but "light weight" as it doesn't attempt to create the uploads directory.
* Intended for use in themes, when only 'basedir' and 'baseurl' are needed, generally in all cases
* when not uploading files.
* @return array See wp_upload_dir() for description.
function wp_get_upload_dir() {
return wp_upload_dir( null, false );
* Returns an array containing the current upload directory's path and URL.
* Checks the 'upload_path' option, which should be from the web root folder,
* and if it isn't empty it will be used. If it is empty, then the path will be
* 'WP_CONTENT_DIR/uploads'. If the 'UPLOADS' constant is defined, then it will
* override the 'upload_path' option and 'WP_CONTENT_DIR/uploads' path.
* The upload URL path is set either by the 'upload_url_path' option or by using
* the 'WP_CONTENT_URL' constant and appending '/uploads' to the path.
* If the 'uploads_use_yearmonth_folders' is set to true (checkbox if checked in
* the administration settings panel), then the time will be used. The format
* will be year first and then month.
* If the path couldn't be created, then an error will be returned with the key
* 'error' containing the error message. The error suggests that the parent
* directory is not writable by the server.
* @param string $time Optional. Time formatted in 'yyyy/mm'. Default null.
* @param bool $create_dir Optional. Whether to check and create the uploads directory.
* Default true for backward compatibility.
* @param bool $refresh_cache Optional. Whether to refresh the cache. Default false.
* Array of information about the upload directory.
* @type string $path Base directory and subdirectory or full path to upload directory.
* @type string $url Base URL and subdirectory or absolute URL to upload directory.
* @type string $subdir Subdirectory if uploads use year/month folders option is on.
* @type string $basedir Path without subdir.
* @type string $baseurl URL path without subdir.
* @type string|false $error False or error message.
function wp_upload_dir( $time = null, $create_dir = true, $refresh_cache = false ) {
static $cache = array(), $tested_paths = array();
$key = sprintf( '%d-%s', get_current_blog_id(), (string) $time );
if ( $refresh_cache || empty( $cache[ $key ] ) ) {
$cache[ $key ] = _wp_upload_dir( $time );
* Filters the uploads directory data.
* @param array $uploads {
* Array of information about the upload directory.
* @type string $path Base directory and subdirectory or full path to upload directory.
* @type string $url Base URL and subdirectory or absolute URL to upload directory.
* @type string $subdir Subdirectory if uploads use year/month folders option is on.
* @type string $basedir Path without subdir.
* @type string $baseurl URL path without subdir.
* @type string|false $error False or error message.
$uploads = apply_filters( 'upload_dir', $cache[ $key ] );
$path = $uploads['path'];
if ( array_key_exists( $path, $tested_paths ) ) {
$uploads['error'] = $tested_paths[ $path ];
if ( ! wp_mkdir_p( $path ) ) {
if ( 0 === strpos( $uploads['basedir'], ABSPATH ) ) {
$error_path = str_replace( ABSPATH, '', $uploads['basedir'] ) . $uploads['subdir'];
$error_path = wp_basename( $uploads['basedir'] ) . $uploads['subdir'];
$uploads['error'] = sprintf(
/* translators: %s: Directory path. */
__( 'Unable to create directory %s. Is its parent directory writable by the server?' ),
$tested_paths[ $path ] = $uploads['error'];
* A non-filtered, non-cached version of wp_upload_dir() that doesn't check the path.
* @param string $time Optional. Time formatted in 'yyyy/mm'. Default null.
* @return array See wp_upload_dir()
function _wp_upload_dir( $time = null ) {
$siteurl = get_option( 'siteurl' );
$upload_path = trim( get_option( 'upload_path' ) );
if ( empty( $upload_path ) || 'wp-content/uploads' === $upload_path ) {
$dir = WP_CONTENT_DIR . '/uploads';
} elseif ( 0 !== strpos( $upload_path, ABSPATH ) ) {
// $dir is absolute, $upload_path is (maybe) relative to ABSPATH.
$dir = path_join( ABSPATH, $upload_path );
$url = get_option( 'upload_url_path' );
if ( empty( $upload_path ) || ( 'wp-content/uploads' === $upload_path ) || ( $upload_path == $dir ) ) {
$url = WP_CONTENT_URL . '/uploads';
$url = trailingslashit( $siteurl ) . $upload_path;
* Honor the value of UPLOADS. This happens as long as ms-files rewriting is disabled.
* We also sometimes obey UPLOADS when rewriting is enabled -- see the next block.
if ( defined( 'UPLOADS' ) && ! ( is_multisite() && get_site_option( 'ms_files_rewriting' ) ) ) {
$dir = ABSPATH . UPLOADS;
$url = trailingslashit( $siteurl ) . UPLOADS;
// If multisite (and if not the main site in a post-MU network).
if ( is_multisite() && ! ( is_main_network() && is_main_site() && defined( 'MULTISITE' ) ) ) {
if ( ! get_site_option( 'ms_files_rewriting' ) ) {
* If ms-files rewriting is disabled (networks created post-3.5), it is fairly
* straightforward: Append sites/%d if we're not on the main site (for post-MU
* networks). (The extra directory prevents a four-digit ID from conflicting with
* a year-based directory for the main site. But if a MU-era network has disabled
* ms-files rewriting manually, they don't need the extra directory, as they never
* had wp-content/uploads for the main site.)
if ( defined( 'MULTISITE' ) ) {
$ms_dir = '/sites/' . get_current_blog_id();
$ms_dir = '/' . get_current_blog_id();
} elseif ( defined( 'UPLOADS' ) && ! ms_is_switched() ) {
* Handle the old-form ms-files.php rewriting if the network still has that enabled.
* When ms-files rewriting is enabled, then we only listen to UPLOADS when:
* 1) We are not on the main site in a post-MU network, as wp-content/uploads is used
* 2) We are not switched, as ms_upload_constants() hardcodes these constants to reflect
* Rather than UPLOADS, we actually use BLOGUPLOADDIR if it is set, as it is absolute.
* (And it will be set, see ms_upload_constants().) Otherwise, UPLOADS can be used, as
* as it is relative to ABSPATH. For the final piece: when UPLOADS is used with ms-files
* rewriting in multisite, the resulting URL is /files. (#WP22702 for background.)
if ( defined( 'BLOGUPLOADDIR' ) ) {
$dir = untrailingslashit( BLOGUPLOADDIR );
$dir = ABSPATH . UPLOADS;
$url = trailingslashit( $siteurl ) . 'files';
if ( get_option( 'uploads_use_yearmonth_folders' ) ) {
// Generate the yearly and monthly directories.
$time = current_time( 'mysql' );
$y = substr( $time, 0, 4 );
$m = substr( $time, 5, 2 );
* Get a filename that is sanitized and unique for the given directory.
* If the filename is not unique, then a number will be added to the filename
* before the extension, and will continue adding numbers until the filename
* The callback function allows the caller to use their own method to create
* unique file names. If defined, the callback should take three arguments:
* - directory, base filename, and extension - and return a unique filename.
* @param string $dir Directory.
* @param string $filename File name.
* @param callable $unique_filename_callback Callback. Default null.
* @return string New filename, if given wasn't unique.
function wp_unique_filename( $dir, $filename, $unique_filename_callback = null ) {
// Sanitize the file name before we begin processing.
$filename = sanitize_file_name( $filename );
// Separate the filename into a name and extension.
$ext = pathinfo( $filename, PATHINFO_EXTENSION );
$name = pathinfo( $filename, PATHINFO_BASENAME );
// Edge case: if file is named '.ext', treat as an empty name.
* Increment the file number until we have a unique file to save in $dir.
* Use callback if supplied.
if ( $unique_filename_callback && is_callable( $unique_filename_callback ) ) {
$filename = call_user_func( $unique_filename_callback, $dir, $name, $ext );