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 );
}
}