WordPress Version: 6.4
/**
* Handles PHP uploads in WordPress.
*
* Sanitizes file names, checks extensions for mime type, and moves the file
* to the appropriate directory within the uploads directory.
*
* @access private
* @since 4.0.0
*
* @see wp_handle_upload_error
*
* @param array $file {
* Reference to a single element from `$_FILES`. Call the function once for each uploaded file.
*
* @type string $name The original name of the file on the client machine.
* @type string $type The mime type of the file, if the browser provided this information.
* @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
* @type int $size The size, in bytes, of the uploaded file.
* @type int $error The error code associated with this file upload.
* }
* @param array|false $overrides {
* An array of override parameters for this file, or boolean false if none are provided.
*
* @type callable $upload_error_handler Function to call when there is an error during the upload process.
* See {@see wp_handle_upload_error()}.
* @type callable $unique_filename_callback Function to call when determining a unique file name for the file.
* See {@see wp_unique_filename()}.
* @type string[] $upload_error_strings The strings that describe the error indicated in
* `$_FILES[{form field}]['error']`.
* @type bool $test_form Whether to test that the `$_POST['action']` parameter is as expected.
* @type bool $test_size Whether to test that the file size is greater than zero bytes.
* @type bool $test_type Whether to test that the mime type of the file is as expected.
* @type string[] $mimes Array of allowed mime types keyed by their file extension regex.
* }
* @param string $time Time formatted in 'yyyy/mm'.
* @param string $action Expected value for `$_POST['action']`.
* @return array {
* On success, returns an associative array of file attributes.
* On failure, returns `$overrides['upload_error_handler']( &$file, $message )`
* or `array( 'error' => $message )`.
*
* @type string $file Filename of the newly-uploaded file.
* @type string $url URL of the newly-uploaded file.
* @type string $type Mime type of the newly-uploaded file.
* }
*/
function _wp_handle_upload(&$file, $overrides, $time, $action)
{
// The default error handler.
if (!function_exists('wp_handle_upload_error')) {
function wp_handle_upload_error(&$file, $message)
{
return array('error' => $message);
}
}
/**
* Filters the data for a file before it is uploaded to WordPress.
*
* The dynamic portion of the hook name, `$action`, refers to the post action.
*
* Possible hook names include:
*
* - `wp_handle_sideload_prefilter`
* - `wp_handle_upload_prefilter`
*
* @since 2.9.0 as 'wp_handle_upload_prefilter'.
* @since 4.0.0 Converted to a dynamic hook with `$action`.
*
* @param array $file {
* Reference to a single element from `$_FILES`.
*
* @type string $name The original name of the file on the client machine.
* @type string $type The mime type of the file, if the browser provided this information.
* @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
* @type int $size The size, in bytes, of the uploaded file.
* @type int $error The error code associated with this file upload.
* }
*/
$file = apply_filters("{$action}_prefilter", $file);
/**
* Filters the override parameters for a file before it is uploaded to WordPress.
*
* The dynamic portion of the hook name, `$action`, refers to the post action.
*
* Possible hook names include:
*
* - `wp_handle_sideload_overrides`
* - `wp_handle_upload_overrides`
*
* @since 5.7.0
*
* @param array|false $overrides An array of override parameters for this file. Boolean false if none are
* provided. See {@see _wp_handle_upload()}.
* @param array $file {
* Reference to a single element from `$_FILES`.
*
* @type string $name The original name of the file on the client machine.
* @type string $type The mime type of the file, if the browser provided this information.
* @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
* @type int $size The size, in bytes, of the uploaded file.
* @type int $error The error code associated with this file upload.
* }
*/
$overrides = apply_filters("{$action}_overrides", $overrides, $file);
// You may define your own function and pass the name in $overrides['upload_error_handler'].
$upload_error_handler = 'wp_handle_upload_error';
if (isset($overrides['upload_error_handler'])) {
$upload_error_handler = $overrides['upload_error_handler'];
}
// You may have had one or more 'wp_handle_upload_prefilter' functions error out the file. Handle that gracefully.
if (isset($file['error']) && !is_numeric($file['error']) && $file['error']) {
return call_user_func_array($upload_error_handler, array(&$file, $file['error']));
}
// Install user overrides. Did we mention that this voids your warranty?
// You may define your own function and pass the name in $overrides['unique_filename_callback'].
$unique_filename_callback = null;
if (isset($overrides['unique_filename_callback'])) {
$unique_filename_callback = $overrides['unique_filename_callback'];
}
/*
* This may not have originally been intended to be overridable,
* but historically has been.
*/
if (isset($overrides['upload_error_strings'])) {
$upload_error_strings = $overrides['upload_error_strings'];
} else {
// Courtesy of php.net, the strings that describe the error indicated in $_FILES[{form field}]['error'].
$upload_error_strings = array(false, sprintf(
/* translators: 1: upload_max_filesize, 2: php.ini */
__('The uploaded file exceeds the %1$s directive in %2$s.'),
'upload_max_filesize',
'php.ini'
), sprintf(
/* translators: %s: MAX_FILE_SIZE */
__('The uploaded file exceeds the %s directive that was specified in the HTML form.'),
'MAX_FILE_SIZE'
), __('The uploaded file was only partially uploaded.'), __('No file was uploaded.'), '', __('Missing a temporary folder.'), __('Failed to write file to disk.'), __('File upload stopped by extension.'));
}
// All tests are on by default. Most can be turned off by $overrides[{test_name}] = false;
$test_form = isset($overrides['test_form']) ? $overrides['test_form'] : true;
$test_size = isset($overrides['test_size']) ? $overrides['test_size'] : true;
// If you override this, you must provide $ext and $type!!
$test_type = isset($overrides['test_type']) ? $overrides['test_type'] : true;
$mimes = isset($overrides['mimes']) ? $overrides['mimes'] : null;
// A correct form post will pass this test.
if ($test_form && (!isset($_POST['action']) || $_POST['action'] !== $action)) {
return call_user_func_array($upload_error_handler, array(&$file, __('Invalid form submission.')));
}
// A successful upload will pass this test. It makes no sense to override this one.
if (isset($file['error']) && $file['error'] > 0) {
return call_user_func_array($upload_error_handler, array(&$file, $upload_error_strings[$file['error']]));
}
// A properly uploaded file will pass this test. There should be no reason to override this one.
$test_uploaded_file = ('wp_handle_upload' === $action) ? is_uploaded_file($file['tmp_name']) : @is_readable($file['tmp_name']);
if (!$test_uploaded_file) {
return call_user_func_array($upload_error_handler, array(&$file, __('Specified file failed upload test.')));
}
$test_file_size = ('wp_handle_upload' === $action) ? $file['size'] : filesize($file['tmp_name']);
// A non-empty file will pass this test.
if ($test_size && !($test_file_size > 0)) {
if (is_multisite()) {
$error_msg = __('File is empty. Please upload something more substantial.');
} else {
$error_msg = sprintf(
/* translators: 1: php.ini, 2: post_max_size, 3: upload_max_filesize */
__('File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your %1$s file or by %2$s being defined as smaller than %3$s in %1$s.'),
'php.ini',
'post_max_size',
'upload_max_filesize'
);
}
return call_user_func_array($upload_error_handler, array(&$file, $error_msg));
}
// A correct MIME type will pass this test. Override $mimes or use the upload_mimes filter.
if ($test_type) {
$wp_filetype = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], $mimes);
$ext = empty($wp_filetype['ext']) ? '' : $wp_filetype['ext'];
$type = empty($wp_filetype['type']) ? '' : $wp_filetype['type'];
$proper_filename = empty($wp_filetype['proper_filename']) ? '' : $wp_filetype['proper_filename'];
// Check to see if wp_check_filetype_and_ext() determined the filename was incorrect.
if ($proper_filename) {
$file['name'] = $proper_filename;
}
if ((!$type || !$ext) && !current_user_can('unfiltered_upload')) {
return call_user_func_array($upload_error_handler, array(&$file, __('Sorry, you are not allowed to upload this file type.')));
}
if (!$type) {
$type = $file['type'];
}
} else {
$type = '';
}
/*
* A writable uploads dir will pass this test. Again, there's no point
* overriding this one.
*/
$uploads = wp_upload_dir($time);
if (!($uploads && false === $uploads['error'])) {
return call_user_func_array($upload_error_handler, array(&$file, $uploads['error']));
}
$filename = wp_unique_filename($uploads['path'], $file['name'], $unique_filename_callback);
// Move the file to the uploads dir.
$new_file = $uploads['path'] . "/{$filename}";
/**
* Filters whether to short-circuit moving the uploaded file after passing all checks.
*
* If a non-null value is returned from the filter, moving the file and any related
* error reporting will be completely skipped.
*
* @since 4.9.0
*
* @param mixed $move_new_file If null (default) move the file after the upload.
* @param array $file {
* Reference to a single element from `$_FILES`.
*
* @type string $name The original name of the file on the client machine.
* @type string $type The mime type of the file, if the browser provided this information.
* @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
* @type int $size The size, in bytes, of the uploaded file.
* @type int $error The error code associated with this file upload.
* }
* @param string $new_file Filename of the newly-uploaded file.
* @param string $type Mime type of the newly-uploaded file.
*/
$move_new_file = apply_filters('pre_move_uploaded_file', null, $file, $new_file, $type);
if (null === $move_new_file) {
if ('wp_handle_upload' === $action) {
$move_new_file = @move_uploaded_file($file['tmp_name'], $new_file);
} else {
// Use copy and unlink because rename breaks streams.
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
$move_new_file = @copy($file['tmp_name'], $new_file);
unlink($file['tmp_name']);
}
if (false === $move_new_file) {
if (str_starts_with($uploads['basedir'], ABSPATH)) {
$error_path = str_replace(ABSPATH, '', $uploads['basedir']) . $uploads['subdir'];
} else {
$error_path = basename($uploads['basedir']) . $uploads['subdir'];
}
return $upload_error_handler($file, sprintf(
/* translators: %s: Destination file path. */
__('The uploaded file could not be moved to %s.'),
$error_path
));
}
}
// Set correct file permissions.
$stat = stat(dirname($new_file));
$perms = $stat['mode'] & 0666;
chmod($new_file, $perms);
// Compute the URL.
$url = $uploads['url'] . "/{$filename}";
if (is_multisite()) {
clean_dirsize_cache($new_file);
}
/**
* Filters the data array for the uploaded file.
*
* @since 2.1.0
*
* @param array $upload {
* Array of upload data.
*
* @type string $file Filename of the newly-uploaded file.
* @type string $url URL of the newly-uploaded file.
* @type string $type Mime type of the newly-uploaded file.
* }
* @param string $context The type of upload action. Values include 'upload' or 'sideload'.
*/
return apply_filters('wp_handle_upload', array('file' => $new_file, 'url' => $url, 'type' => $type), ('wp_handle_sideload' === $action) ? 'sideload' : 'upload');
}