extensions */ public static function presets() { return array( 'documents' => array( 'pdf', 'doc', 'docx', 'odt', 'rtf', 'txt' ), 'images' => array( 'jpg', 'jpeg', 'png', 'gif', 'webp' ), 'both' => array( 'pdf', 'doc', 'docx', 'odt', 'rtf', 'txt', 'jpg', 'jpeg', 'png', 'gif', 'webp' ), ); } /** * Allowed extensions for a field. * * @param array $field Normalized field. * @return string[] */ public static function allowed_extensions( array $field ) { $presets = self::presets(); if ( 'custom' === $field['accept'] ) { $exts = preg_split( '/[\s,;]+/', strtolower( (string) $field['extensions'] ) ); $exts = array_map( static function ( $e ) { return ltrim( trim( $e ), '.' ); }, $exts ); } else { $exts = isset( $presets[ $field['accept'] ] ) ? $presets[ $field['accept'] ] : $presets['documents']; } $known = array(); foreach ( array_keys( get_allowed_mime_types() ) as $group ) { foreach ( explode( '|', $group ) as $e ) { $known[ $e ] = true; } } $exts = array_filter( array_unique( $exts ), static function ( $e ) use ( $known ) { return '' !== $e && isset( $known[ $e ] ) && ! in_array( $e, self::$blocked, true ); } ); return array_values( $exts ); } /** * Maximum size in bytes for a field (capped by the server limit). * * @param array $field Normalized field. * @return int */ public static function max_bytes( array $field ) { $mb = max( 1, (int) $field['max_size'] ); return (int) min( $mb * MB_IN_BYTES, wp_max_upload_size() ); } /** * Normalize the structure of $_FILES['af'] for one field. * * @param string $field_id Field ID. * @return array[] List of array( name, type, tmp_name, error, size ). */ public static function from_request( $field_id ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- protected by the form token. if ( empty( $_FILES['af']['name'][ $field_id ] ) ) { return array(); } $raw = $_FILES['af']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput,WordPress.Security.NonceVerification.Missing $out = array(); $keys = array( 'name', 'type', 'tmp_name', 'error', 'size' ); $name = $raw['name'][ $field_id ]; if ( is_array( $name ) ) { foreach ( array_keys( $name ) as $i ) { $file = array(); foreach ( $keys as $k ) { $file[ $k ] = isset( $raw[ $k ][ $field_id ][ $i ] ) ? $raw[ $k ][ $field_id ][ $i ] : ''; } $out[] = $file; } } else { $file = array(); foreach ( $keys as $k ) { $file[ $k ] = isset( $raw[ $k ][ $field_id ] ) ? $raw[ $k ][ $field_id ] : ''; } $out[] = $file; } // Ignore empty file inputs. return array_values( array_filter( $out, static function ( $f ) { return UPLOAD_ERR_NO_FILE !== (int) $f['error'] && '' !== (string) $f['name']; } ) ); } /** * Validate the uploaded files of a field. * * @param array $field Normalized field. * @param array[] $files From from_request(). * @return string|null Error message or null. */ public static function validate( array $field, array $files ) { $m = Fields::messages(); $label = $field['label']; if ( ! $files ) { return $field['required'] ? sprintf( $m['file_required'], $label ) : null; } if ( count( $files ) > (int) $field['max_files'] ) { return sprintf( $m['file_count'], (int) $field['max_files'], $label ); } $allowed = self::allowed_extensions( $field ); $max = self::max_bytes( $field ); foreach ( $files as $f ) { $err = (int) $f['error']; $name = self::display_name( $f['name'] ); if ( UPLOAD_ERR_INI_SIZE === $err || UPLOAD_ERR_FORM_SIZE === $err || (int) $f['size'] > $max ) { return sprintf( $m['file_size'], $name, size_format( $max ) ); } if ( UPLOAD_ERR_OK !== $err || ! is_uploaded_file( $f['tmp_name'] ) ) { /* translators: %s: file name */ return sprintf( __( '"%s" could not be uploaded. Please try again.', 'alpeg-forms' ), $name ); } $ext = strtolower( pathinfo( (string) $f['name'], PATHINFO_EXTENSION ) ); if ( ! in_array( $ext, $allowed, true ) ) { return sprintf( $m['file_type'], $name, strtoupper( implode( ', ', $allowed ) ) ); } // Check the real content type, not only the extension. $check = wp_check_filetype_and_ext( $f['tmp_name'], 'upload.' . $ext ); if ( empty( $check['ext'] ) || empty( $check['type'] ) || ! in_array( $check['ext'], $allowed, true ) ) { /* translators: %s: file name */ return sprintf( __( 'The content of "%s" does not match its file type.', 'alpeg-forms' ), $name ); } } return null; } /** * Move validated files into protected storage. * * @param array[] $files From from_request() (already validated). * @return array[] Stored files: name, path, size, type. */ public static function store( array $files ) { require_once ABSPATH . 'wp-admin/includes/file.php'; $base = self::base_dir(); $sub = gmdate( 'Y/m' ); $dir = $base . '/' . $sub; if ( ! wp_mkdir_p( $dir ) ) { return array(); } self::protect_directory(); $stored = array(); foreach ( $files as $f ) { $ext = strtolower( pathinfo( (string) $f['name'], PATHINFO_EXTENSION ) ); $check = wp_check_filetype_and_ext( $f['tmp_name'], 'upload.' . $ext ); $destination = static function ( $uploads ) use ( $dir, $sub ) { $uploads['path'] = $dir; $uploads['url'] = trailingslashit( $uploads['baseurl'] ) . 'alpeg-forms/' . $sub; $uploads['subdir'] = '/alpeg-forms/' . $sub; return $uploads; }; add_filter( 'upload_dir', $destination ); try { $result = wp_handle_upload( $f, array( 'test_form' => false, 'unique_filename_callback' => static function ( $folder ) use ( $ext ) { do { $filename = strtolower( wp_generate_password( 32, false ) ) . '.' . $ext; } while ( file_exists( $folder . '/' . $filename ) ); return $filename; }, ) ); } finally { remove_filter( 'upload_dir', $destination ); } if ( ! empty( $result['error'] ) || empty( $result['file'] ) ) { continue; } $name = basename( $result['file'] ); @chmod( $dir . '/' . $name, 0640 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions $stored[] = array( 'name' => self::display_name( $f['name'] ), 'path' => $sub . '/' . $name, 'size' => (int) $f['size'], 'type' => (string) $check['type'], ); } return $stored; } /** * Absolute path of a stored file, or '' if it is invalid or missing. * * @param string $relative Relative path (YYYY/MM/name.ext). * @return string */ public static function absolute_path( $relative ) { if ( ! preg_match( '#^\d{4}/\d{2}/[a-z0-9]{32}\.[a-z0-9]{1,8}$#', (string) $relative ) ) { return ''; } $path = self::base_dir() . '/' . $relative; return is_file( $path ) ? $path : ''; } /** * Delete all files of an entry. * * @param object $entry Entry (hydrated). */ public static function delete_for_entry( $entry ) { foreach ( (array) $entry->data as $row ) { if ( empty( $row['files'] ) || ! is_array( $row['files'] ) ) { continue; } foreach ( $row['files'] as $file ) { $path = self::absolute_path( isset( $file['path'] ) ? $file['path'] : '' ); if ( $path ) { wp_delete_file( $path ); } } } } /** * Stream a file to an authorized admin (admin-post handler). */ public static function handle_download() { $entry_id = isset( $_GET['entry'] ) ? absint( $_GET['entry'] ) : 0; $field = isset( $_GET['field'] ) ? sanitize_key( wp_unslash( $_GET['field'] ) ) : ''; $index = isset( $_GET['i'] ) ? absint( $_GET['i'] ) : 0; if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'alpeg-forms' ), 403 ); } check_admin_referer( 'alpeg_forms_file_' . $entry_id ); $entry = Entries::get( $entry_id ); $file = null; if ( $entry ) { foreach ( $entry->data as $row ) { if ( $row['id'] === $field && isset( $row['files'][ $index ] ) ) { $file = $row['files'][ $index ]; } } } $path = $file ? self::absolute_path( $file['path'] ) : ''; if ( ! $path ) { wp_die( esc_html__( 'The file no longer exists.', 'alpeg-forms' ), 404 ); } nocache_headers(); header( 'Content-Type: application/octet-stream' ); header( 'Content-Disposition: attachment; filename="' . str_replace( array( '"', "\r", "\n" ), '', $file['name'] ) . '"; filename*=UTF-8\'\'' . rawurlencode( $file['name'] ) ); header( 'Content-Length: ' . filesize( $path ) ); header( 'X-Content-Type-Options: nosniff' ); readfile( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions exit; } /** * Download URL for the admin. * * @param int $entry_id Entry ID. * @param string $field_id Field ID. * @param int $index File index. * @return string */ public static function download_url( $entry_id, $field_id, $index ) { return wp_nonce_url( add_query_arg( array( 'action' => 'alpeg_forms_file', 'entry' => (int) $entry_id, 'field' => $field_id, 'i' => (int) $index, ), admin_url( 'admin-post.php' ) ), 'alpeg_forms_file_' . (int) $entry_id ); } /** * Base directory for uploads. * * @return string */ public static function base_dir() { $uploads = wp_upload_dir( null, false ); return untrailingslashit( $uploads['basedir'] ) . '/alpeg-forms'; } /** * Create protection files against direct access. */ public static function protect_directory() { $base = self::base_dir(); if ( ! wp_mkdir_p( $base ) ) { return; } $files = array( '.htaccess' => "# ALPEG Forms: no direct access\n\n\tRequire all denied\n\n\n\tOrder deny,allow\n\tDeny from all\n\nOptions -Indexes\n", 'web.config' => "\n\n\t\n\t\t\n\t\t\t\n\t\t\n\t\n\n", 'index.php' => " $content ) { if ( ! file_exists( $base . '/' . $name ) ) { file_put_contents( $base . '/' . $name, $content ); // phpcs:ignore WordPress.WP.AlternativeFunctions } } } /** * Remove the whole upload directory (uninstall). */ public static function remove_all() { $base = self::base_dir(); if ( ! is_dir( $base ) ) { return; } $it = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $base, \FilesystemIterator::SKIP_DOTS ), \RecursiveIteratorIterator::CHILD_FIRST ); foreach ( $it as $item ) { $item->isDir() ? @rmdir( $item->getPathname() ) : @unlink( $item->getPathname() ); // phpcs:ignore } @rmdir( $base ); // phpcs:ignore } /** * Safe display name of an uploaded file. * * @param string $name Original name. * @return string */ public static function display_name( $name ) { $name = wp_basename( str_replace( '\\', '/', (string) $name ) ); $name = sanitize_text_field( $name ); $name = preg_replace( '/[\x00-\x1F"<>|:*?]/u', '', $name ); return mb_substr( '' !== $name ? $name : 'file', 0, 150 ); } }