/
var
/
www
/
barefootlaw.org
/
wp-content
/
plugins
/
wpforms
/
pro
/
includes
/
fields
/
Upload File
HOME
<?php use WPForms\Pro\Forms\Fields\FileUpload\Chunk; /** * File upload field. * * @since 1.0.0 */ class WPForms_Field_File_Upload extends WPForms_Field { /** * Dropzone plugin version. * * @since 1.5.6 * * @var string */ const DROPZONE_VERSION = '5.7.2'; /** * Classic (old) style of file uploader field. * * @since 1.5.6 * * @var string */ const STYLE_CLASSIC = 'classic'; /** * Modern style of file uploader field. * * @since 1.5.6 * * @var string */ const STYLE_MODERN = 'modern'; /** * Replaceable (either in PHP or JS) template for a maximum file number. * * @since 1.5.8 * * @var string */ const TEMPLATE_MAXFILENUM = '{maxFileNumber}'; /** * File extensions that are now allowed. * * @since 1.0.0 * * @var array */ private $denylist = array( 'ade', 'adp', 'app', 'asp', 'bas', 'bat', 'cer', 'cgi', 'chm', 'cmd', 'com', 'cpl', 'crt', 'csh', 'csr', 'dll', 'drv', 'exe', 'fxp', 'flv', 'hlp', 'hta', 'htaccess', 'htm', 'html', 'htpasswd', 'inf', 'ins', 'isp', 'jar', 'js', 'jse', 'jsp', 'ksh', 'lnk', 'mdb', 'mde', 'mdt', 'mdw', 'msc', 'msi', 'msp', 'mst', 'ops', 'pcd', 'php', 'pif', 'pl', 'prg', 'ps1', 'ps2', 'py', 'rb', 'reg', 'scr', 'sct', 'sh', 'shb', 'shs', 'sys', 'swf', 'tmp', 'torrent', 'url', 'vb', 'vbe', 'vbs', 'vbscript', 'wsc', 'wsf', 'wsf', 'wsh', 'dfxp', 'onetmp' ); /** * Primary class constructor. * * @since 1.0.0 */ public function init() { // Define field type information. $this->name = esc_html__( 'File Upload', 'wpforms' ); $this->type = 'file-upload'; $this->icon = 'fa-upload'; $this->order = 90; $this->group = 'fancy'; // Form frontend javascript. add_action( 'wpforms_frontend_js', array( $this, 'frontend_js' ) ); // Form frontend CSS. add_action( 'wpforms_frontend_css', array( $this, 'frontend_css' ) ); // Field styles for Gutenberg. add_action( 'enqueue_block_editor_assets', array( $this, 'gutenberg_enqueues' ) ); // Define additional field properties. add_filter( 'wpforms_field_properties_file-upload', array( $this, 'field_properties' ), 5, 3 ); // Customize value format for HTML emails. add_filter( 'wpforms_html_field_value', array( $this, 'html_email_value' ), 10, 4 ); // Add builder strings. add_filter( 'wpforms_builder_strings', array( $this, 'add_builder_strings' ), 10, 2 ); // Maybe format/upload file depending on the conditional visibility state. add_action( 'wpforms_process_format_after', array( $this, 'format_conditional' ), 6, 1 ); // Upload file ajax route. add_action( 'wp_ajax_wpforms_file_upload_speed_test', 'wp_send_json_success' ); add_action( 'wp_ajax_nopriv_wpforms_file_upload_speed_test', 'wp_send_json_success' ); // TODO: perhaps remove, chunks uploading replaces this. add_action( 'wp_ajax_wpforms_upload_file', array( $this, 'ajax_modern_upload' ) ); add_action( 'wp_ajax_nopriv_wpforms_upload_file', array( $this, 'ajax_modern_upload' ) ); // Ajax handlers for newest uploads (With chunks and parallel support). add_action( 'wp_ajax_wpforms_upload_chunk_init', array( $this, 'ajax_chunk_upload_init' ) ); add_action( 'wp_ajax_nopriv_wpforms_upload_chunk_init', array( $this, 'ajax_chunk_upload_init' ) ); add_action( 'wp_ajax_wpforms_upload_chunk', array( $this, 'ajax_chunk_upload' ) ); add_action( 'wp_ajax_nopriv_wpforms_upload_chunk', array( $this, 'ajax_chunk_upload' ) ); add_action( 'wp_ajax_wpforms_file_chunks_uploaded', array( $this, 'ajax_chunk_upload_finalize' ) ); add_action( 'wp_ajax_nopriv_wpforms_file_chunks_uploaded', array( $this, 'ajax_chunk_upload_finalize' ) ); // Remove file ajax route. add_action( 'wp_ajax_wpforms_remove_file', array( $this, 'ajax_modern_remove' ) ); add_action( 'wp_ajax_nopriv_wpforms_remove_file', array( $this, 'ajax_modern_remove' ) ); // phpcs:ignore WordPress.Security.NonceVerification if ( ! empty( $_POST['slow'] ) && 'true' === $_POST['slow'] && ! empty( $this->ajax_validate_form_field_modern() ) ) { add_action( 'wpforms_file_upload_chunk_parallel', '__return_false' ); add_action( 'wpforms_file_upload_chunk_size', array( $this, 'get_slow_connection_chunk_size' ) ); } add_filter( 'robots_txt', array( $this, 'disallow_upload_dir_indexing' ), -42 ); } /** * Enqueue frontend field js. * * @since 1.5.6 * * @param array $forms Forms on the current page. */ public function frontend_js( $forms ) { $is_file_modern_style = false; foreach ( $forms as $form ) { if ( $this->is_field_style( $form, self::STYLE_MODERN ) ) { $is_file_modern_style = true; break; } } if ( $is_file_modern_style || wpforms()->frontend->assets_global() ) { $min = wpforms_get_min_suffix(); wp_enqueue_script( 'wpforms-dropzone', WPFORMS_PLUGIN_URL . "pro/assets/js/vendor/dropzone{$min}.js", array( 'jquery' ), self::DROPZONE_VERSION, true ); wp_enqueue_script( 'wpforms-file-upload', WPFORMS_PLUGIN_URL . "pro/assets/js/wpforms-file-upload{$min}.js", array( 'wpforms', 'wp-util', 'wpforms-dropzone' ), WPFORMS_VERSION, true ); wp_localize_script( 'wpforms-dropzone', 'wpforms_file_upload', array( 'url' => admin_url( 'admin-ajax.php' ), 'errors' => array( 'file_not_uploaded' => esc_html__( 'This file was not uploaded.', 'wpforms' ), 'file_limit' => esc_html__( 'File limit has been reached ({fileLimit}).', 'wpforms' ), 'file_extension' => wpforms_setting( 'validation-fileextension', esc_html__( 'File type is not allowed.', 'wpforms' ) ), 'file_size' => wpforms_setting( 'validation-filesize', esc_html__( 'File exceeds the max size allowed.', 'wpforms' ) ), 'post_max_size' => sprintf( /* translators: %s - max allowed file size by a server. */ esc_html__( 'File exceeds the upload limit allowed (%s).', 'wpforms' ), wpforms_max_upload() ), ), 'loading_message' => esc_html__( 'File upload is in progress. Please submit the form once uploading is completed.', 'wpforms' ), ) ); } } /** * Enqueue frontend field CSS. * * @since 1.5.6 * * @param array $forms Forms on the current page. */ public function frontend_css( $forms ) { $is_file_modern_style = false; foreach ( $forms as $form ) { if ( $this->is_field_style( $form, self::STYLE_MODERN ) ) { $is_file_modern_style = true; break; } } if ( $is_file_modern_style || wpforms()->frontend->assets_global() ) { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-dropzone', WPFORMS_PLUGIN_URL . "pro/assets/css/dropzone{$min}.css", array(), self::DROPZONE_VERSION ); } } /** * Whether provided form has a file field with a specified style. * * @since 1.5.6 * * @param array $form Form data. * @param string $style Desired field style. * * @return bool */ protected function is_field_style( $form, $style ) { $is_field_style = false; if ( empty( $form['fields'] ) ) { return $is_field_style; } foreach ( (array) $form['fields'] as $field ) { if ( ! empty( $field['type'] ) && $field['type'] === $this->type && ! empty( $field['style'] ) && $field['style'] === sanitize_key( $style ) ) { $is_field_style = true; break; } } return $is_field_style; } /** * Load enqueues for the Gutenberg editor. * * @since 1.5.6 */ public function gutenberg_enqueues() { $min = wpforms_get_min_suffix(); wp_enqueue_style( 'wpforms-dropzone', WPFORMS_PLUGIN_URL . "pro/assets/css/dropzone{$min}.css", array(), self::DROPZONE_VERSION ); } /** * Define additional field properties. * * @since 1.3.7 * * @param array $properties Field properties. * @param array $field Field data and settings. * @param array $form_data Form data and settings. * * @return array */ public function field_properties( $properties, $field, $form_data ) { $this->form_data = (array) $form_data; $this->form_id = absint( $this->form_data['id'] ); $this->field_id = absint( $field['id'] ); $this->field_data = $this->form_data['fields'][ $this->field_id ]; // Input Primary: adjust name. $properties['inputs']['primary']['attr']['name'] = "wpforms_{$this->form_id}_{$this->field_id}"; // Input Primary: filter files in classic uploader style in files selection window. if ( empty( $this->field_data['style'] ) || self::STYLE_CLASSIC === $this->field_data['style'] ) { $properties['inputs']['primary']['attr']['accept'] = rtrim( '.' . implode( ',.', $this->get_extensions() ), ',.' ); } // Input Primary: allowed file extensions. $properties['inputs']['primary']['data']['rule-extension'] = implode( ',', $this->get_extensions() ); // Input Primary: max file size. $properties['inputs']['primary']['data']['rule-maxsize'] = $this->max_file_size(); return $properties; } /** * Whether current field can be populated dynamically. * * @since 1.5.0 * * @param array $properties Field properties. * @param array $field Current field specific data. * * @return bool */ public function is_dynamic_population_allowed( $properties, $field ) { // We need to disable an ability to steal files from user computer. return false; } /** * Whether current field can be populated dynamically. * * @since 1.5.0 * * @param array $properties Field properties. * @param array $field Current field specific data. * * @return bool */ public function is_fallback_population_allowed( $properties, $field ) { // We need to disable an ability to steal files from user computer. return false; } /** * Customize format for HTML email notifications. * * @since 1.1.3 * @since 1.5.6 Added different link generation for classic and modern uploader. * * @param string $val Field value. * @param array $field Field settings. * @param array $form_data Form data and settings. * @param string $context Value display context. * * @return string */ public function html_email_value( $val, $field, $form_data = array(), $context = '' ) { if ( empty( $field['value'] ) || $field['type'] !== $this->type ) { return $val; } // Process modern uploader. if ( ! empty( $field['value_raw'] ) ) { return wpforms_chain( $field['value_raw'] ) ->map( static function ( $file ) { if ( empty( $file['value'] ) || empty( $file['file_original'] ) ) { return ''; } return sprintf( '<a href="%s" rel="noopener noreferrer" target="_blank">%s</a>', esc_url( $file['value'] ), esc_html( $file['file_original'] ) ); } ) ->array_filter() ->implode( '<br>' ) ->value(); } // Process classic uploader. return sprintf( '<a href="%s" rel="noopener" target="_blank">%s</a>', esc_url( $field['value'] ), esc_html( $field['file_original'] ) ); } /** * File Upload field specific strings. * * @since 1.5.8 * * @return array Field specific strings. */ public function get_strings() { return array( 'preview_title_single' => esc_html__( 'Click or drag a file to this area to upload.', 'wpforms' ), 'preview_title_plural' => esc_html__( 'Click or drag files to this area to upload.', 'wpforms' ), 'preview_hint' => sprintf( /* translators: % - max number of files as a template string (not a number), replaced by a number later. */ esc_html__( 'You can upload up to %s files.', 'wpforms' ), self::TEMPLATE_MAXFILENUM ), ); } /** * Add Builder strings that are passed to JS. * * @since 1.5.8 * * @param array $strings Form Builder strings. * @param array $form Form Data. * * @return array Form Builder strings. */ public function add_builder_strings( $strings, $form ) { $strings['file_upload'] = $this->get_strings(); return $strings; } /** * Field options panel inside the builder. * * @since 1.0.0 * @since 1.5.6 Added modern style uploader options. * * @param array $field Field data and settings. */ public function field_options( $field ) { $style = ! empty( $field['style'] ) ? $field['style'] : self::STYLE_MODERN; /* * Basic field options. */ // Options open markup. $this->field_option( 'basic-options', $field, array( 'markup' => 'open' ) ); // Label. $this->field_option( 'label', $field ); // Description. $this->field_option( 'description', $field ); // Allowed extensions. $lbl = $this->field_element( 'label', $field, array( 'slug' => 'extensions', 'value' => esc_html__( 'Allowed File Extensions', 'wpforms' ), 'tooltip' => esc_html__( 'Enter the extensions you would like to allow, comma separated.', 'wpforms' ), ), false ); $fld = $this->field_element( 'text', $field, array( 'slug' => 'extensions', 'value' => ! empty( $field['extensions'] ) ? $field['extensions'] : '', ), false ); $this->field_element( 'row', $field, array( 'slug' => 'extensions', 'content' => $lbl . $fld, ) ); // Max file size. $lbl = $this->field_element( 'label', $field, array( 'slug' => 'max_size', 'value' => esc_html__( 'Max File Size', 'wpforms' ), /* translators: %s - max upload size. */ 'tooltip' => sprintf( esc_html__( 'Enter the max size of each file, in megabytes, to allow. If left blank, the value defaults to the maximum size the server allows which is %s.', 'wpforms' ), wpforms_max_upload() ), ), false ); $fld = $this->field_element( 'text', $field, array( 'slug' => 'max_size', 'type' => 'number', 'attrs' => array( 'min' => 1, 'max' => 512, 'step' => 1, 'pattern' => '[0-9]', ), 'value' => ! empty( $field['max_size'] ) ? abs( $field['max_size'] ) : '', ), false ); $this->field_element( 'row', $field, array( 'slug' => 'max_size', 'content' => $lbl . $fld, ) ); // Max file number. $lbl = $this->field_element( 'label', $field, array( 'slug' => 'max_file_number', 'value' => esc_html__( 'Max File Number', 'wpforms' ), 'tooltip' => esc_html__( 'Enter the max file number, to allow. If left blank, the value defaults to 1.', 'wpforms' ), ), false ); $fld = $this->field_element( 'text', $field, array( 'slug' => 'max_file_number', 'type' => 'number', 'attrs' => array( 'min' => 1, 'max' => 100, 'step' => 1, 'pattern' => '[0-9]', ), 'value' => ! empty( $field['max_file_number'] ) ? absint( $field['max_file_number'] ) : 1, ), false ); $this->field_element( 'row', $field, array( 'slug' => 'max_file_number', 'content' => $lbl . $fld, 'class' => self::STYLE_CLASSIC === $style ? 'wpforms-row-hide' : '', ) ); // Required toggle. $this->field_option( 'required', $field ); // Options close markup. $this->field_option( 'basic-options', $field, array( 'markup' => 'close' ) ); /* * Advanced field options. */ // Options open markup. $this->field_option( 'advanced-options', $field, array( 'markup' => 'open' ) ); // Style. $lbl = $this->field_element( 'label', $field, array( 'slug' => 'style', 'value' => esc_html__( 'Style', 'wpforms' ), 'tooltip' => esc_html__( 'Modern Style supports multiple file uploads, displays a drag-and-drop upload box, and uses AJAX. Classic Style supports single file upload and displays a traditional upload button.', 'wpforms' ), ), false ); $fld = $this->field_element( 'select', $field, array( 'slug' => 'style', 'value' => $style, 'options' => array( self::STYLE_MODERN => esc_html__( 'Modern', 'wpforms' ), self::STYLE_CLASSIC => esc_html__( 'Classic', 'wpforms' ), ), ), false ); $this->field_element( 'row', $field, array( 'slug' => 'style', 'content' => $lbl . $fld, ) ); // Hide Label. $this->field_option( 'label_hide', $field ); // Media Library toggle. $fld = $this->field_element( 'checkbox', $field, array( 'slug' => 'media_library', 'value' => ! empty( $field['media_library'] ) ? 1 : '', 'desc' => esc_html__( 'Store file in WordPress Media Library', 'wpforms' ), 'tooltip' => esc_html__( 'Check this option to store the final uploaded file in the WordPress Media Library', 'wpforms' ), ), false ); $this->field_element( 'row', $field, array( 'slug' => 'media_library', 'content' => $fld, ) ); // Custom CSS classes. $this->field_option( 'css', $field ); // Options close markup. $this->field_option( 'advanced-options', $field, array( 'markup' => 'close', ) ); } /** * Field preview panel inside the builder. * * @since 1.0.0 * @since 1.5.6 Added modern style uploader logic. * * @param array $field Field data. */ public function field_preview( $field ) { // Label. $this->field_preview_option( 'label', $field ); $modern_classes = array( 'wpforms-file-upload-builder-modern' ); $classic_classes = array( 'wpforms-file-upload-builder-classic' ); if ( empty( $field['style'] ) || self::STYLE_CLASSIC !== $field['style'] ) { $classic_classes[] = 'wpforms-hide'; } else { $modern_classes[] = 'wpforms-hide'; } $strings = $this->get_strings(); $max_file_number = ! empty( $field['max_file_number'] ) ? max( 1, absint( $field['max_file_number'] ) ) : 1; // Primary input. echo wpforms_render( 'fields/file-upload-backend', array( 'max_file_number' => $max_file_number, 'preview_hint' => str_replace( self::TEMPLATE_MAXFILENUM, $max_file_number, $strings['preview_hint'] ), 'modern_classes' => implode( ' ', $modern_classes ), 'classic_classes' => implode( ' ', $classic_classes ), ), true ); // Description. $this->field_preview_option( 'description', $field ); } /** * Field display on the form front-end. * * @since 1.0.0 * @since 1.5.6 Added modern style uploader logic. * * @param array $field Field data and settings. * @param array $deprecated Deprecated field attributes. Use field properties. * @param array $form_data Form data and settings. */ public function field_display( $field, $deprecated, $form_data ) { // Define data. $primary = $field['properties']['inputs']['primary']; // Modern style. if ( ! empty( $field['style'] ) && self::STYLE_MODERN === $field['style'] ) { $strings = $this->get_strings(); $max_file_number = ! empty( $field['max_file_number'] ) ? max( 1, absint( $field['max_file_number'] ) ) : 1; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wpforms_render( 'fields/file-upload-frontend', array( 'field_id' => (int) $field['id'], 'form_id' => (int) $form_data['id'], 'url' => admin_url( 'admin-ajax.php' ), 'input_name' => $this->get_input_name(), 'required' => $primary['required'], 'extensions' => $primary['data']['rule-extension'], 'max_size' => abs( $primary['data']['rule-maxsize'] ), 'chunk_size' => $this->get_chunk_size(), 'max_file_number' => $max_file_number, 'preview_hint' => str_replace( self::TEMPLATE_MAXFILENUM, $max_file_number, $strings['preview_hint'] ), 'post_max_size' => wp_max_upload_size(), ), true ); } else { // Classic style. printf( '<input type="file" %s %s>', wpforms_html_attributes( $primary['id'], $primary['class'], $primary['data'], $primary['attr'] ), $primary['required'] // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } } /** * Input name. * * The input name is name in which the data is expected to be sent in from the client. * * @since 1.6.2 * * @return string */ public function get_input_name() { return sprintf( 'wpforms_%d_%d', $this->form_id, $this->field_id ); } /** * Maximum size for a chunk in file uploads. * * @since 1.6.2 * * @return int */ public function get_chunk_size() { return min( apply_filters( 'wpforms_file_upload_chunk_size', 2 * 1024 * 1024 ), wp_max_upload_size(), $this->max_file_size() ); } /** * Maximum chunk for slow connections. * * @since 1.6.2 * * @return int Chunk size expected for slow connections. */ public function get_slow_connection_chunk_size() { return min( 512 * 1024, wp_max_upload_size(), $this->max_file_size() ); } /** * Validate field for various errors on form submit. * * @since 1.0.0 * @since 1.5.6 Added modern style uploader logic. * * @param int $field_id Field ID. * @param array $field_submit Submitted field value. * @param array $form_data Form data and settings. */ public function validate( $field_id, $field_submit, $form_data ) { $this->form_data = (array) $form_data; $this->form_id = absint( $this->form_data['id'] ); $this->field_id = absint( $field_id ); $this->field_data = $this->form_data['fields'][ $this->field_id ]; $input_name = $this->get_input_name(); $style = ! empty( $this->field_data['style'] ) ? $this->field_data['style'] : self::STYLE_CLASSIC; // Add modern validate. if ( self::STYLE_CLASSIC === $style ) { $this->validate_classic( $input_name ); } else { $this->validate_modern( $input_name ); } } /** * Validate classic file uploader field data. * * @since 1.5.6 * * @param string $input_name Input name inside the form on front-end. */ protected function validate_classic( $input_name ) { if ( empty( $_FILES[ $input_name ] ) ) { return; } /* * If nothing is uploaded and it is not required, don't process. */ if ( $_FILES[ $input_name ]['error'] === 4 && ! $this->is_required() ) { return; } /* * Basic file upload validation. */ $validated_basic = $this->validate_basic( (int) $_FILES[ $input_name ]['error'] ); if ( ! empty( $validated_basic ) ) { wpforms()->process->errors[ $this->form_id ][ $this->field_id ] = $validated_basic; return; } /* * Validate if file is required and provided. */ if ( ( empty( $_FILES[ $input_name ]['tmp_name'] ) || 4 === $_FILES[ $input_name ]['error'] ) && $this->is_required() ) { wpforms()->process->errors[ $this->form_id ][ $this->field_id ] = wpforms_get_required_label(); return; } /* * Validate file size. */ $validated_size = $this->validate_size(); if ( ! empty( $validated_size ) ) { wpforms()->process->errors[ $this->form_id ][ $this->field_id ] = $validated_size; return; } /* * Validate file extension. */ $ext = strtolower( pathinfo( $_FILES[ $input_name ]['name'], PATHINFO_EXTENSION ) ); $validated_ext = $this->validate_extension( $ext ); if ( ! empty( $validated_ext ) ) { wpforms()->process->errors[ $this->form_id ][ $this->field_id ] = $validated_ext; return; } /* * Validate file against what WordPress is set to allow. * At the end of the day, if you try to upload a file that WordPress * doesn't allow, we won't allow it either. Users can use a plugin to * filter the allowed mime types in WordPress if this is an issue. */ $validated_filetype = $this->validate_wp_filetype_and_ext( $_FILES[ $input_name ]['tmp_name'], sanitize_file_name( wp_unslash( $_FILES[ $input_name ]['name'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput if ( ! empty( $validated_filetype ) ) { wpforms()->process->errors[ $this->form_id ][ $this->field_id ] = $validated_filetype; return; } } /** * Validate modern file uploader field data. * * @since 1.5.6 * * @param string $input_name Input name inside the form on front-end. */ protected function validate_modern( $input_name ) { if ( ! $this->is_required() ) { return; } $value = ''; if ( ! empty( $_POST[ $input_name ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $value = json_decode( wp_unslash( $_POST[ $input_name ] ), true ); // phpcs:ignore WordPress.Security } if ( empty( $value ) ) { wpforms()->process->errors[ $this->form_id ][ $this->field_id ] = wpforms_get_required_label(); } } /** * Format and sanitize field. * * @since 1.0.0 * * @param int $field_id Field ID. * @param array $field_submit Submitted field value. * @param array $form_data Form data and settings. */ public function format( $field_id, $field_submit, $form_data ) { // Setup class properties to reuse everywhere. $this->form_data = (array) $form_data; $this->form_id = absint( $this->form_data['id'] ); $this->field_id = absint( $field_id ); $this->field_data = $this->form_data['fields'][ $this->field_id ]; $field_label = ! empty( $this->form_data['fields'][ $this->field_id ]['label'] ) ? $this->form_data['fields'][ $this->field_id ]['label'] : ''; $input_name = sprintf( 'wpforms_%d_%d', $this->form_id, $this->field_id ); $style = ! empty( $this->field_data['style'] ) ? $this->field_data['style'] : self::STYLE_CLASSIC; if ( self::STYLE_CLASSIC === $style ) { $this->format_classic( $field_label, $input_name ); } else { $this->format_modern( $field_label, $input_name ); } } /** * Format and sanitize classic style of file upload field. * * @since 1.5.6 * * @param string $field_label Field label. * @param string $input_name Input name inside the form on front-end. */ protected function format_classic( $field_label, $input_name ) { $file = ! empty( $_FILES[ $input_name ] ) ? $_FILES[ $input_name ] : false; // phpcs:ignore // Preserve field CL visibility state before processing the field. $visible = isset( wpforms()->process->fields[ $this->field_id ]['visible'] ) ? wpforms()->process->fields[ $this->field_id ]['visible'] : false; // If there was no file uploaded or if this field has conditional logic // rules active, stop here before we continue with the // upload process. if ( ! $file || 0 !== $file['error'] || in_array( $this->field_id, $this->form_data['conditional_fields'], true ) ) { wpforms()->process->fields[ $this->field_id ] = array( 'name' => sanitize_text_field( $field_label ), 'value' => '', 'file' => '', 'file_original' => '', 'ext' => '', 'id' => absint( $this->field_id ), 'type' => $this->type, ); if ( $visible ) { wpforms()->process->fields[ $this->field_id ]['visible'] = $visible; } return; } // Define data. $file_name = sanitize_file_name( $file['name'] ); $file_ext = pathinfo( $file_name, PATHINFO_EXTENSION ); $file_base = wp_basename( $file_name, '.' . $file_ext ); $file_name_new = sprintf( '%s-%s.%s', $file_base, wp_hash( wp_rand() . microtime() . $this->form_id . $this->field_id ), strtolower( $file_ext ) ); $upload_dir = wpforms_upload_dir(); $upload_path = $upload_dir['path']; // Old dir. $form_directory = absint( $this->form_id ) . '-' . md5( $this->form_id . $this->form_data['created'] ); $upload_path_form = trailingslashit( $upload_path ) . $form_directory; // Check for form upload directory destination. if ( ! file_exists( $upload_path_form ) ) { // New one. $form_directory = absint( $this->form_id ) . '-' . wp_hash( $this->form_data['created'] . $this->form_id ); $upload_path_form = trailingslashit( $upload_path ) . $form_directory; // Check once again and make directory if it's not exists. if ( ! file_exists( $upload_path_form ) ) { wp_mkdir_p( $upload_path_form ); } } $file_new = trailingslashit( $upload_path_form ) . $file_name_new; $file_name_new = wp_basename( trailingslashit( dirname( $file_new ) ) . $file_name_new ); $file_new = trailingslashit( dirname( $file_new ) ) . $file_name_new; $file_url = trailingslashit( $upload_dir['url'] ) . trailingslashit( $form_directory ) . $file_name_new; $attachment_id = '0'; // Check if the .htaccess exists in the upload directory, if not - create it. wpforms_create_upload_dir_htaccess_file(); // Check if the index.html exists in the directories, if not - create it. wpforms_create_index_html_file( $upload_path ); wpforms_create_index_html_file( $upload_path_form ); // Move the file to the uploads dir - similar to _wp_handle_upload(). $move_new_file = @move_uploaded_file( $file['tmp_name'], $file_new ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( false === $move_new_file ) { wpforms_log( 'Upload Error, could not upload file', $file_url, array( 'type' => array( 'entry', 'error' ), 'form_id' => $this->form_data['id'], ) ); return; } $this->set_file_fs_permissions( $file_new ); // Maybe move file to the WordPress media library. if ( $this->is_media_integrated() ) { // Include necessary code from core. require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; // Copy our file into WordPress uploads. $file_args = array( 'error' => '', 'tmp_name' => $file_new, 'name' => $file_name_new, 'type' => $file['type'], 'size' => $file['size'], ); $upload = wp_handle_sideload( $file_args, array( 'test_form' => false ) ); if ( ! empty( $upload['file'] ) ) { // Create a Media attachment for the file. $attachment_id = wp_insert_attachment( array( 'post_title' => $this->get_wp_media_file_title( $file ), 'post_content' => $this->get_wp_media_file_desc( $file ), 'post_status' => 'publish', 'post_mime_type' => $file['type'], ), $upload['file'] ); if ( ! empty( $attachment_id ) ) { // Generate attachment meta. wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); // Update file url/name. $file_url = wp_get_attachment_url( $attachment_id ); $file_name_new = wp_basename( $file_url ); } } } // Set final field details. wpforms()->process->fields[ $this->field_id ] = array( 'name' => sanitize_text_field( $field_label ), 'value' => esc_url_raw( $file_url ), 'file' => $file_name_new, 'file_original' => $file_name, 'file_user_name' => sanitize_text_field( $file['name'] ), 'ext' => $file_ext, 'attachment_id' => absint( $attachment_id ), 'id' => absint( $this->field_id ), 'type' => $this->type, ); // Save field CL visibility state after field processing. if ( $visible ) { wpforms()->process->fields[ $this->field_id ]['visible'] = $visible; } } /** * Format and sanitize modern style of file upload field. * * @since 1.5.6 * * @param string $field_label Field label. * @param string $input_name Input name inside the form on front-end. */ protected function format_modern( $field_label, $input_name ) { $processed = array( 'name' => sanitize_text_field( $field_label ), 'value' => '', 'value_raw' => '', 'id' => $this->field_id, 'type' => $this->type, 'style' => self::STYLE_MODERN, ); // Preserve field CL visibility state before processing the field. if ( isset( wpforms()->process->fields[ $this->field_id ]['visible'] ) ) { $processed['visible'] = wpforms()->process->fields[ $this->field_id ]['visible']; } // We should actually receive some files info. if ( empty( $_POST[ $input_name ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing wpforms()->process->fields[ $this->field_id ] = $processed; return; } if ( ! empty( wpforms()->process->fields[ $this->field_id ] ) ) { return; } // Make sure json_decode() doesn't fail on newer PHP. try { $raw_files = json_decode( wp_unslash( $_POST[ $input_name ] ), true ); // phpcs:ignore WordPress.Security } catch ( Exception $e ) { wpforms()->process->fields[ $this->field_id ] = $processed; return; } // Make sure we actually have some files. if ( empty( $raw_files ) || ! is_array( $raw_files ) ) { wpforms()->process->fields[ $this->field_id ] = $processed; return; } // Make sure we process only submitted files with the expected structure and keys. $files = array_filter( $raw_files, static function ( $file ) { return is_array( $file ) && count( $file ) === 3 && ! empty( $file['file'] ) && ! empty( $file['name'] ); } ); if ( empty( $files ) ) { wpforms()->process->fields[ $this->field_id ] = $processed; return; } wpforms_create_upload_dir_htaccess_file(); $upload_dir = wpforms_upload_dir(); if ( empty( $upload_dir['error'] ) ) { wpforms_create_index_html_file( $upload_dir['path'] ); } $data = array(); foreach ( $files as $file ) { $file = $this->generate_file_info( $file ); if ( $this->is_media_integrated() ) { $file['path'] = $file['tmp_path']; $file = $this->generate_file_attachment( $file ); } else { // Create form upload directory if needed. $this->create_dir( dirname( $file['path'] ) ); rename( $file['tmp_path'], $file['path'] ); $this->set_file_fs_permissions( $file['path'] ); } $data[] = $this->generate_file_data( $file ); } if ( ! empty( $data ) ) { $processed = wp_parse_args( array( 'value_raw' => $data, 'value' => wpforms_chain( $data ) ->map( static function ( $file ) { return $file['value']; } ) ->implode( "\n" ) ->value(), ), $processed ); } wpforms()->process->fields[ $this->field_id ] = $processed; } /** * Add additional information to the files array for each file. * * @since 1.5.6 * * @param array $file Submitted file basic info. * * @return array */ protected function generate_file_info( $file ) { $dir = $this->get_form_files_dir(); $file['tmp_path'] = trailingslashit( $this->get_tmp_dir() ) . $file['file']; $file['type'] = 'application/octet-stream'; if ( is_file( $file['tmp_path'] ) ) { $filetype = wp_check_filetype( $file['tmp_path'] ); $file['type'] = $filetype['type']; } // Data for no media case. $file_ext = pathinfo( $file['name'], PATHINFO_EXTENSION ); $file_base = wp_basename( $file['name'], '.' . $file_ext ); $file['file_name_new'] = sanitize_file_name( sprintf( '%s-%s.%s', $file_base, wp_hash( wp_rand() . microtime() . $this->form_data['id'] . $this->field_id ), strtolower( $file_ext ) ) ); $file['file_url'] = trailingslashit( $dir['url'] ) . $file['file_name_new']; $file['path'] = trailingslashit( $dir['path'] ) . $file['file_name_new']; $file['attachment_id'] = 0; return $file; } /** * Create a Media Library attachment. * * @since 1.5.6 * * @param array $file File to create Media Library attachment for. * * @return array */ protected function generate_file_attachment( $file ) { // Include necessary code from core. require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $file_args = array( 'error' => '', 'tmp_name' => $file['path'], 'name' => $file['file_name_new'], 'type' => $file['type'], ); $upload = wp_handle_sideload( $file_args, array( 'test_form' => false ) ); if ( empty( $upload['file'] ) ) { return $file; } // Create a Media attachment for the file. $attachment_id = wp_insert_attachment( array( 'post_title' => $this->get_wp_media_file_title( $file ), 'post_content' => $this->get_wp_media_file_desc( $file ), 'post_status' => 'publish', 'post_mime_type' => $file['type'], ), $upload['file'] ); if ( empty( $attachment_id ) ) { return $file; } // Generate and update attachment meta. wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); // Update file information. $file_url = wp_get_attachment_url( $attachment_id ); $file['path'] = $upload['file']; $file['file_url'] = $file_url; $file['file_name_new'] = wp_basename( $file_url ); $file['attachment_id'] = $attachment_id; return $file; } /** * Generate an attachment title used in WP Media library for an uploaded file. * * @since 1.6.1 * * @param array $file File data. * * @return string */ private function get_wp_media_file_title( $file ) { $title = apply_filters( 'wpforms_field_' . $this->type . '_media_file_title', sprintf( '%s: %s', $this->field_data['label'], $file['name'] ), $file, $this->field_data ); return wpforms_sanitize_text_deeply( $title ); } /** * Generate an attachment description used in WP Media library for an uploaded file. * * @since 1.6.1 * * @param array $file File data. * * @return string */ private function get_wp_media_file_desc( $file ) { $desc = apply_filters( 'wpforms_field_' . $this->type . '_media_file_desc', $this->field_data['description'], $file, $this->field_data ); return wp_kses_post_deep( $desc ); } /** * Generate a ready for DB data for each file. * * @since 1.5.6 * * @param array $file File to generate data for. * * @return array */ protected function generate_file_data( $file ) { return array( 'name' => sanitize_text_field( $file['name'] ), 'value' => esc_url_raw( $file['file_url'] ), 'file' => $file['file_name_new'], 'file_original' => $file['name'], 'file_user_name' => sanitize_text_field( $file['file_user_name'] ), 'ext' => wpforms_chain( $file['file'] )->explode( '.' )->pop()->value(), 'attachment_id' => isset( $file['attachment_id'] ) ? absint( $file['attachment_id'] ) : 0, 'id' => $this->field_id, 'type' => $file['type'], ); } /** * Format, sanitize, and upload files for fields that have conditional logic rules applied. * * @since 1.3.8 * * @param array $form_data Form data and settings. */ public function format_conditional( $form_data ) { // If the form contains no fields with conditional logic no need to // continue processing. if ( empty( $form_data['conditional_fields'] ) ) { return; } // Loop through each field that has conditional logic rules. foreach ( $form_data['conditional_fields'] as $key => $field_id ) { // Check if the field exists. if ( empty( wpforms()->process->fields[ $field_id ] ) ) { continue; } // Check if the 'type' exists. if ( empty( wpforms()->process->fields[ $field_id ]['type'] ) ) { continue; } // We are only concerned with file upload fields. if ( wpforms()->process->fields[ $field_id ]['type'] !== $this->type ) { continue; } // If the upload field was no visible at submit then ignore it. if ( empty( wpforms()->process->fields[ $field_id ]['visible'] ) ) { continue; } // If there are errors pertaining to this form, its not going to // process, so bail and avoid file upload. if ( ! empty( wpforms()->process->errors[ $form_data['id'] ] ) ) { continue; } /* * We made it this far, so we can assume we have a file upload field * which was visible during submit, inside a form which does not * contain any errors, so at last we can proceed with uploading the * file. */ // Unset this field from conditional fields so the format method will proceed. unset( $form_data['conditional_fields'][ $key ] ); // Upload the file and celebrate. $this->format( $field_id, array(), $form_data ); } } /** * Determine the max allowed file size in bytes as per field options. * * @since 1.0.0 * * @return int Number of bytes allowed. */ public function max_file_size() { if ( ! empty( $this->field_data['max_size'] ) ) { // Strip any suffix provided (eg M, MB etc), which leaves us with the raw MB value. $max_size = preg_replace( '/[^0-9.]/', '', $this->field_data['max_size'] ); $max_size = wpforms_size_to_bytes( $max_size . 'M' ); } else { $max_size = wpforms_max_upload( true ); } return $max_size; } /** * Clean up the tmp folder - remove all old files every day (filterable interval). * * @since 1.5.6 */ protected function clean_tmp_files() { $files = glob( trailingslashit( $this->get_tmp_dir() ) . '*' ); if ( ! is_array( $files ) || empty( $files ) ) { return; } $lifespan = (int) apply_filters( 'wpforms_field_' . $this->type . '_clean_tmp_files_lifespan', DAY_IN_SECONDS ); foreach ( $files as $file ) { if ( 'index.html' === $file || ! is_file( $file ) ) { continue; } // In some cases filemtime() can return false, in that case - pretend this is a new file and do nothing. $modified = (int) filemtime( $file ); if ( empty( $modified ) ) { $modified = time(); } if ( ( time() - $modified ) >= $lifespan ) { @unlink( $file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged } } } /** * Remove the file from the temporary directory. * * @since 1.5.6 */ public function ajax_modern_remove() { $default_error = esc_html__( 'Something went wrong while removing the file.', 'wpforms' ); $validated_form_field = $this->ajax_validate_form_field_modern(); if ( empty( $validated_form_field ) ) { wp_send_json_error( $default_error, 400 ); } if ( empty( $_POST['file'] ) ) { wp_send_json_error( $default_error, 403 ); } $file = sanitize_file_name( wp_unslash( $_POST['file'] ) ); $tmp_path = wp_normalize_path( $this->get_tmp_dir() . '/' . $file ); // Requested file does not exist, which is good. if ( ! is_file( $tmp_path ) ) { wp_send_json_success( $file ); } if ( @unlink( $tmp_path ) ) { wp_send_json_success( $file ); } wp_send_json_error( $default_error, 400 ); } /** * Upload the file, used during AJAX requests. * * @deprecated 1.6.2 * * @since 1.5.6 */ public function ajax_modern_upload() { $default_error = esc_html__( 'Something went wrong, please try again.', 'wpforms' ); $validated_form_field = $this->ajax_validate_form_field_modern(); if ( empty( $validated_form_field ) ) { wp_send_json_error( $default_error, 403 ); } // Make sure we have required values from $_FILES. if ( empty( $_FILES['file']['name'] ) ) { wp_send_json_error( $default_error, 403 ); } if ( empty( $_FILES['file']['tmp_name'] ) ) { wp_send_json_error( $default_error, 403 ); } $error = empty( $_FILES['file']['error'] ) ? 0 : (int) $_FILES['file']['error']; $name = sanitize_file_name( wp_unslash( $_FILES['file']['name'] ) ); $file_user_name = sanitize_text_field( wp_unslash( $_FILES['file']['name'] ) ); $path = $_FILES['file']['tmp_name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $extension = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) ); $errors = wpforms_chain( array() ) ->array_merge( (array) $this->validate_basic( $error ) ) ->array_merge( (array) $this->validate_size() ) ->array_merge( (array) $this->validate_extension( $extension ) ) ->array_merge( (array) $this->validate_wp_filetype_and_ext( $path, $name ) ) ->array_filter() ->value(); if ( count( $errors ) ) { wp_send_json_error( implode( ',', $errors ), 400 ); } $tmp_dir = $this->get_tmp_dir(); $tmp_name = $this->get_tmp_file_name( $extension ); $tmp_path = wp_normalize_path( $tmp_dir . '/' . $tmp_name ); $tmp = $this->move_file( $path, $tmp_path ); if ( ! $tmp ) { wp_send_json_error( $default_error, 400 ); } $this->clean_tmp_files(); wp_send_json_success( array( 'name' => $name, 'file' => pathinfo( $tmp, PATHINFO_FILENAME ) . '.' . pathinfo( $tmp, PATHINFO_EXTENSION ), 'file_user_name' => $file_user_name, ) ); } /** * Initializes the chunk upload process. * * No data is being send by the client, they expecting an authorization from this method * before sending any chunk. * * The server may return different configs to the uploader client (smaller chunks, disable * parallel uploads etc). * * This method would validate the file extension, maximum size and other things. * * @since 1.6.2 */ public function ajax_chunk_upload_init() { $default_error = esc_html__( 'Something went wrong, please try again.', 'wpforms' ); $validated_form_field = $this->ajax_validate_form_field_modern(); if ( empty( $validated_form_field ) ) { wp_send_json_error( $default_error ); } $handler = Chunk::from_current_request( $this ); if ( ! $handler || ! $handler->create_metadata() ) { wp_send_json_error( $default_error, 403 ); } $error = 0; $name = sanitize_file_name( wp_unslash( $handler->get_file_name() ) ); $extension = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) ); $errors = wpforms_chain( array() ) ->array_merge( (array) $this->validate_basic( $error ) ) ->array_merge( (array) $this->validate_size( [ $handler->get_file_size() ] ) ) ->array_merge( (array) $this->validate_extension( $extension ) ) ->array_filter() ->value(); if ( count( $errors ) > 0 ) { wp_send_json_error( implode( ',', $errors ) ); } wp_send_json( [ 'success' => true, 'data' => [ 'dzchunksize' => $handler->get_chunk_size(), 'parallelChunkUploads' => apply_filters( 'wpforms_file_upload_chunk_parallel', true ), ], ] ); } /** * Upload the files using chunks. * * @since 1.6.2 */ public function ajax_chunk_upload() { $default_error = esc_html__( 'Something went wrong, please try again.', 'wpforms' ); $validated_form_field = $this->ajax_validate_form_field_modern(); if ( empty( $validated_form_field ) ) { wp_send_json_error( $default_error ); } $handler = Chunk::from_current_request( $this ); if ( ! $handler || ! $handler->load_metadata() ) { wp_send_json_error( $default_error, 403 ); } if ( ! $handler->write() ) { wp_send_json_error( $default_error, 403 ); } wp_send_json( [ 'success' => true ] ); } /** * Ajax handler for finalizing a chunked upload. * * @since 1.6.2 */ public function ajax_chunk_upload_finalize() { $default_error = esc_html__( 'Something went wrong, please try again.', 'wpforms' ); $handler = Chunk::from_current_request( $this ); if ( ! $handler || ! $handler->load_metadata() ) { wp_send_json_error( $default_error, 403 ); } $file_name = $handler->get_file_name(); $file_user_name = $handler->get_file_user_name(); $extension = strtolower( pathinfo( $file_name, PATHINFO_EXTENSION ) ); $tmp_dir = $this->get_tmp_dir(); $tmp_name = $this->get_tmp_file_name( $extension ); $tmp_path = wp_normalize_path( $tmp_dir . '/' . $tmp_name ); if ( ! $handler->finalize( $tmp_path ) ) { wp_send_json_error( $default_error, 403 ); } $is_valid_type = $this->validate_wp_filetype_and_ext( $tmp_path, $file_name ); if ( false !== $is_valid_type ) { wp_send_json_error( $is_valid_type, 403 ); } $this->clean_tmp_files(); wp_send_json_success( array( 'name' => $file_name, 'file' => pathinfo( $tmp_path, PATHINFO_FILENAME ) . '.' . pathinfo( $tmp_path, PATHINFO_EXTENSION ), 'file_user_name' => $file_user_name, ) ); } /** * Validate form ID, field ID and field style for existence and that they are actually valid. * * @since 1.5.6 * * @return array Empty array on any kind of failure. */ protected function ajax_validate_form_field_modern() { if ( empty( $_POST['form_id'] ) || empty( $_POST['field_id'] ) ) { return array(); } // phpcs:ignore WordPress.Security.NonceVerification.Missing $form_data = wpforms()->form->get( (int) $_POST['form_id'], [ 'content_only' => true ] ); if ( empty( $form_data ) || ! is_array( $form_data ) ) { return array(); } // phpcs:ignore WordPress.Security.NonceVerification.Missing $field_id = (int) $_POST['field_id']; if ( ! isset( $form_data['fields'][ $field_id ]['style'] ) || self::STYLE_MODERN !== $form_data['fields'][ $field_id ]['style'] ) { return array(); } // Make data available everywhere in the class, so we don't need to pass it manually. $this->form_data = $form_data; $this->form_id = $this->form_data['id']; $this->field_id = $field_id; $this->field_data = $this->form_data['fields'][ $this->field_id ]; return array( 'form_data' => $form_data, 'field_id' => $field_id, ); } /** * Basic file upload validation. * * @since 1.5.6 * * @param int $error Error ID provided by PHP. * * @return false|string False if no errors found, error text otherwise. */ protected function validate_basic( $error ) { if ( 0 === $error || 4 === $error ) { return false; } $errors = array( false, esc_html__( 'The uploaded file exceeds the upload_max_filesize directive in php.ini.', 'wpforms' ), esc_html__( 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', 'wpforms' ), esc_html__( 'The uploaded file was only partially uploaded.', 'wpforms' ), esc_html__( 'No file was uploaded.', 'wpforms' ), '', esc_html__( 'Missing a temporary folder.', 'wpforms' ), esc_html__( 'Failed to write file to disk.', 'wpforms' ), esc_html__( 'File upload stopped by extension.', 'wpforms' ), ); if ( array_key_exists( $error, $errors ) ) { /* translators: %s - error text. */ return sprintf( esc_html__( 'File upload error. %s', 'wpforms' ), $errors[ $error ] ); } return false; } /** * Validate file size. * * @since 1.5.6 * * @param array $sizes Array with all file sizes in bytes. * * @return false|string False if no errors found, error text otherwise. */ protected function validate_size( $sizes = null ) { if ( null === $sizes && ! empty( $_FILES ) ) { $sizes = []; foreach ( $_FILES as $file ) { $sizes[] = $file['size']; } } if ( ! is_array( $sizes ) ) { return false; } $max_size = min( wp_max_upload_size(), $this->max_file_size() ); foreach ( $sizes as $size ) { if ( $size > $max_size ) { return sprintf( /* translators: $s - allowed file size in Mb. */ esc_html__( 'File exceeds max size allowed (%s).', 'wpforms' ), size_format( $max_size ) ); } } return false; } /** * Validate extension against denylist and admin-provided list. * There are certain extensions we do not allow under any circumstances, * with no exceptions, for security purposes. * * @since 1.5.6 * * @param string $ext Extension. * * @return false|string False if no errors found, error text otherwise. */ protected function validate_extension( $ext ) { // Make sure file has an extension first. if ( empty( $ext ) ) { return esc_html__( 'File must have an extension.', 'wpforms' ); } // Validate extension against all allowed values. if ( ! in_array( $ext, $this->get_extensions(), true ) ) { return esc_html__( 'File type is not allowed.', 'wpforms' ); } return false; } /** * Validate file against what WordPress is set to allow. * At the end of the day, if you try to upload a file that WordPress * doesn't allow, we won't allow it either. Users can use a plugin to * filter the allowed mime types in WordPress if this is an issue. * * @since 1.5.6 * * @param string $path Path to a newly uploaded file. * @param string $name Name of a newly uploaded file. * * @return false|string False if no errors found, error text otherwise. */ protected function validate_wp_filetype_and_ext( $path, $name ) { $wp_filetype = wp_check_filetype_and_ext( $path, $name ); $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']; if ( $proper_filename || ! $ext || ! $type ) { return esc_html__( 'File type is not allowed.', 'wpforms' ); } return false; } /** * Get form-specific uploads directory path and URL. * * @since 1.5.6 * * @return array */ protected function get_form_files_dir() { $upload_dir = wpforms_upload_dir(); $folder = absint( $this->form_data['id'] ) . '-' . wp_hash( $this->form_data['created'] . $this->form_data['id'] ); return array( 'path' => trailingslashit( $upload_dir['path'] ) . $folder, 'url' => trailingslashit( $upload_dir['url'] ) . $folder, ); } /** * Get tmp dir for files. * * @since 1.5.6 * * @return string */ public function get_tmp_dir() { $upload_dir = wpforms_upload_dir(); $tmp_root = $upload_dir['path'] . '/tmp'; if ( ! file_exists( $tmp_root ) || ! wp_is_writable( $tmp_root ) ) { wp_mkdir_p( $tmp_root ); } // Check if the index.html exists in the directory, if not - create it. wpforms_create_index_html_file( $tmp_root ); return $tmp_root; } /** * Create both the directory and index.html file in it if any of them doesn't exist. * * @since 1.5.6 * * @param string $path Path to the directory. * * @return string Path to the newly created directory. */ protected function create_dir( $path ) { if ( ! file_exists( $path ) ) { wp_mkdir_p( $path ); } // Check if the index.html exists in the path, if not - create it. wpforms_create_index_html_file( $path ); return $path; } /** * Get tmp file name. * * @since 1.5.6 * * @param string $extension File extension. * * @return string */ protected function get_tmp_file_name( $extension ) { return wp_hash( wp_rand() . microtime() . $this->form_id . $this->field_id ) . '.' . $extension; } /** * Move file to a permanent location. * * @since 1.5.6 * * @param string $path_from From. * @param string $path_to To. * * @return false|string False on error. */ protected function move_file( $path_from, $path_to ) { $this->create_dir( dirname( $path_to ) ); if ( false === move_uploaded_file( $path_from, $path_to ) ) { wpforms_log( 'Upload Error, could not upload file', $path_from, array( 'type' => array( 'entry', 'error' ), ) ); return false; } $this->set_file_fs_permissions( $path_to ); return $path_to; } /** * Set correct file permissions in the file system. * * @since 1.5.6 * * @param string $path File to set permissions for. */ protected function set_file_fs_permissions( $path ) { // Set correct file permissions. $stat = stat( dirname( $path ) ); @chmod( $path, $stat['mode'] & 0000666 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged } /** * Get all allowed extensions. * Check against user-entered extensions. * * @since 1.5.6 * * @return array */ protected function get_extensions() { // Allowed file extensions by default. $default_extensions = $this->get_default_extensions(); // Allowed file extensions. $extensions = ! empty( $this->field_data['extensions'] ) ? explode( ',', $this->field_data['extensions'] ) : $default_extensions; return wpforms_chain( $extensions ) ->map( static function ( $ext ) { return strtolower( preg_replace( '/[^A-Za-z0-9]/', '', $ext ) ); } ) ->array_filter() ->array_intersect( $default_extensions ) ->value(); } /** * Get default extensions supported by WordPress * without those that we manually denylist. * * @since 1.5.6 * * @return array */ protected function get_default_extensions() { return wpforms_chain( get_allowed_mime_types() ) ->array_keys() ->implode( '|' ) ->explode( '|' ) ->array_diff( $this->denylist ) ->value(); } /** * Whether field is required or not. * * @uses $this->field_data * * @since 1.5.6 * * @return bool */ protected function is_required() { return ! empty( $this->field_data['required'] ); } /** * Whether field is integrated with WordPress Media Library. * * @uses $this->field_data * * @since 1.5.6 * * @return bool */ protected function is_media_integrated() { return ! empty( $this->field_data['media_library'] ) && '1' === $this->field_data['media_library']; } /** * Disallow WPForms upload directory indexing in robots.txt. * * @since 1.6.1 * * @param string $output Robots.txt output. * * @return string */ public function disallow_upload_dir_indexing( $output ) { $upload_dir = wpforms_upload_dir(); if ( ! empty( $upload_dir['error'] ) ) { return $output; } $site_url = site_url(); $upload_root = str_replace( $site_url, '', $upload_dir['url'] ); $upload_root = trailingslashit( $upload_root ); $site_url_parts = wp_parse_url( $site_url ); if ( ! empty( $site_url_parts['path'] ) ) { $upload_root = $site_url_parts['path'] . $upload_root; } $output .= "Disallow: $upload_root\n"; return $output; } } new WPForms_Field_File_Upload();