- WordPress core y plugins - Tema Twenty Twenty-Four configurado - Plugin allow-unfiltered-html.php simplificado - .gitignore configurado para excluir wp-config.php y uploads 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
599 lines
16 KiB
PHP
Executable File
599 lines
16 KiB
PHP
Executable File
<?php
|
|
|
|
namespace TCB\inc\helpers;
|
|
|
|
/**
|
|
* Class FileUploadConfig
|
|
*
|
|
* @package TCB\inc\helpers
|
|
*
|
|
* @property int $max_size
|
|
* @property int $max_files
|
|
* @property array $file_types
|
|
* @property array $custom_file_types custom extensions
|
|
* @property string $api key of the connected file upload API service
|
|
* @property string $name Filename template
|
|
* @property string $folder identification for the destination folder
|
|
* @property string $required Whether or not the field is required
|
|
*/
|
|
class FileUploadConfig extends FormSettings {
|
|
|
|
const EMAIL_FILENAME_TEMPLATE = '___T_USR_EMAIL___';
|
|
|
|
const POST_TYPE = '_tcb_file_upload';
|
|
|
|
/**
|
|
* Default configuration for file uploads
|
|
*
|
|
* @var array
|
|
*/
|
|
public static $defaults = [
|
|
'max_files' => 1,
|
|
'max_size' => 1, // in MB
|
|
'file_types' => [ 'documents' ],
|
|
'custom_file_types' => [],
|
|
'name' => '{match}', // by default, match the filename with the one uploaded by the visitor
|
|
];
|
|
|
|
/**
|
|
* Get a list of each file group, it's title, icon and associated extensions
|
|
*
|
|
* @return array[]
|
|
*/
|
|
public static function get_allowed_file_groups() {
|
|
return array(
|
|
'documents' => array(
|
|
'name' => __( 'Documents', 'thrive-cb' ),
|
|
'icon' => 'file',
|
|
'extensions' => [ 'pdf', 'doc', 'docx', 'ppt', 'pptx', 'pps', 'ppsx', 'odt', 'xls', 'xlsx', 'psd', 'txt' ],
|
|
),
|
|
'images' => array(
|
|
'name' => __( 'Images', 'thrive-cb' ),
|
|
'icon' => 'image2',
|
|
'extensions' => [ 'jpg', 'jpeg', 'png', 'gif', 'ico' ],
|
|
),
|
|
'audio' => array(
|
|
'name' => __( 'Audio files', 'thrive-cb' ),
|
|
'icon' => 'audio',
|
|
'extensions' => [ 'mp3', 'm4a', 'ogg', 'wav' ],
|
|
),
|
|
'video' => array(
|
|
'name' => __( 'Video files', 'thrive-cb' ),
|
|
'icon' => 'video',
|
|
'extensions' => [ 'mp4', 'm4v', 'mov', 'wmv', 'avi', 'mpg', 'ogv', '3gp', '3g2' ],
|
|
),
|
|
'zip' => array(
|
|
'name' => __( 'Zip archives', 'thrive-cb' ),
|
|
'icon' => 'archive',
|
|
'extensions' => [ 'zip', 'tar', 'gzip', 'gz' ],
|
|
),
|
|
'custom' => array(
|
|
'name' => __( 'Custom', 'thrive-cb' ),
|
|
'icon' => 'wrench',
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get a nonce key to use for file uploads
|
|
*
|
|
* @param string $file_id
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function get_nonce_key( $file_id ) {
|
|
return "tcb_file_api_{$file_id}";
|
|
}
|
|
|
|
/**
|
|
* Generate a unique nonce for each file_id
|
|
*
|
|
* @param string $file_id
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function create_nonce( $file_id ) {
|
|
return wp_create_nonce( static::get_nonce_key( $file_id ) );
|
|
}
|
|
|
|
/**
|
|
* @param string $nonce user-submitted nonce key
|
|
* @param string $file_id
|
|
*
|
|
* @return boolean|int
|
|
*/
|
|
public static function verify_nonce( $nonce, $file_id ) {
|
|
return wp_verify_nonce( $nonce, static::get_nonce_key( $file_id ) );
|
|
}
|
|
|
|
/**
|
|
* Setter for config
|
|
*
|
|
* @param array $config
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function set_config( $config ) {
|
|
parent::set_config( $config );
|
|
/* normalize required value to integer */
|
|
$this->config['required'] = ! empty( $this->config['required'] ) ? 1 : 0;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get a subset of the configuration, that only applies for frontend
|
|
*
|
|
* @param bool $json get it as JSON
|
|
*
|
|
* @return string|array|false
|
|
*/
|
|
public function get_frontend_config( $json = true ) {
|
|
$config = array(
|
|
'max_size' => $this->max_size,
|
|
'max_files' => $this->max_files,
|
|
'allowed' => $this->get_allowed_extensions(),
|
|
'required' => $this->required,
|
|
);
|
|
|
|
return $json ? json_encode( $config ) : $config;
|
|
}
|
|
|
|
/**
|
|
* Get a list of allowed file extensions
|
|
*
|
|
* @return array
|
|
*/
|
|
public function get_allowed_extensions() {
|
|
$file_types = $this->file_types;
|
|
if ( ! is_array( $file_types ) ) {
|
|
$file_types = [];
|
|
}
|
|
$extensions = [];
|
|
if ( is_array( $this->custom_file_types ) && in_array( 'custom', $file_types, true ) ) {
|
|
$extensions = $this->custom_file_types;
|
|
}
|
|
|
|
$extension_groups = [ $extensions ];
|
|
foreach ( static::get_allowed_file_groups() as $key => $group ) {
|
|
if ( isset( $group['extensions'] ) && in_array( $key, $file_types, true ) ) {
|
|
$extension_groups [] = $group['extensions'];
|
|
}
|
|
}
|
|
|
|
return call_user_func_array( 'array_merge', $extension_groups );
|
|
}
|
|
|
|
/**
|
|
* Check if the extension is allowed
|
|
*
|
|
* @param string $extension
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function extension_allowed( $extension ) {
|
|
$extension = strtolower( $extension );
|
|
|
|
/* if blacklisted, don't allow this extension */
|
|
if ( in_array( $extension, static::get_extensions_blacklist(), true ) ) {
|
|
return false;
|
|
}
|
|
|
|
return in_array( $extension, $this->get_allowed_extensions(), true );
|
|
}
|
|
|
|
/**
|
|
* Check if the filesize is allowed (if it's lower than max accepted file size)
|
|
*
|
|
* @param int $uploaded_file_size
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function size_allowed( $uploaded_file_size ) {
|
|
/* $this->max_size = maximum file size in MB */
|
|
return $uploaded_file_size <= wp_convert_hr_to_bytes( $this->max_size . 'm' );
|
|
}
|
|
|
|
/**
|
|
* Calculate the filename used to store this on the API service
|
|
*
|
|
* @param string $original_name original filename, without extension
|
|
*
|
|
* @return string filename without extension
|
|
*/
|
|
public function get_upload_filename( $original_name ) {
|
|
$templates = array(
|
|
'{match}' => sanitize_file_name( $original_name ),
|
|
'{date}' => current_time( 'm-d-Y' ),
|
|
'{time}' => current_time( 'Hi' ),
|
|
/**
|
|
* email is not known at the time of file upload.
|
|
* Solution is to use this template and rename the files via API when the form is submitted
|
|
*/
|
|
'{email}' => static::EMAIL_FILENAME_TEMPLATE,
|
|
);
|
|
|
|
return str_replace( array_keys( $templates ), $templates, $this->name ) . '_' . mt_rand( 1000000, 9999999 );
|
|
}
|
|
|
|
/**
|
|
* Gets the service API that's been setup with this file upload configuration
|
|
*
|
|
* @return \Thrive_Dash_List_Connection_Abstract|\WP_Error
|
|
*/
|
|
public function get_service() {
|
|
$api_connection = $this->api;
|
|
|
|
if ( ! $api_connection ) {
|
|
return new \WP_Error( 'Missing file upload service. Please contact site owner' );
|
|
}
|
|
|
|
return \Thrive_Dash_List_Manager::connection_instance( $api_connection );
|
|
}
|
|
|
|
/**
|
|
* Validates the main LG submitted form
|
|
*
|
|
* @param array $data
|
|
*
|
|
* @return string|bool
|
|
*/
|
|
public function validate_form_submit( $data ) {
|
|
|
|
$files = ! empty( $data['_tcb_files'] ) && is_array( $data['_tcb_files'] ) ? $data['_tcb_files'] : [];
|
|
$min_files = $this->required ? 1 : 0;
|
|
$file_count = count( $files );
|
|
$result = true;
|
|
|
|
if ( ! $this->ID ) {
|
|
$result = __( 'Invalid file configuration', 'thrive-cb' );
|
|
} elseif ( $file_count < $min_files || $file_count > $this->max_files ) {
|
|
$result = __( 'Invalid number of files', 'thrive-cb' );
|
|
}
|
|
|
|
/* validate nonces */
|
|
foreach ( $files as $nonce => $file_id ) {
|
|
if ( ! static::verify_nonce( $nonce, $file_id ) ) {
|
|
$result = __( 'Invalid request', 'thrive-cb' );
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Checks the configuration to see if any files need to be renamed
|
|
* This can happen if the user adds '{email}' to the file name settings
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function needs_file_rename() {
|
|
return strpos( $this->name, '{email}' ) !== false;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of file extensions that should always be forbidden, regardless of user settings
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function get_extensions_blacklist() {
|
|
return [
|
|
'0xe',
|
|
'a6p',
|
|
'action',
|
|
'app',
|
|
'applescript',
|
|
'bash',
|
|
'bat',
|
|
'cgi',
|
|
'cod',
|
|
'com',
|
|
'dek',
|
|
'dex',
|
|
'dmg',
|
|
'ebm',
|
|
'elf',
|
|
'es',
|
|
'esh',
|
|
'ex4',
|
|
'exe',
|
|
'exopc',
|
|
'fpi',
|
|
'gpe',
|
|
'gpu',
|
|
'hms',
|
|
'hta',
|
|
'ipa',
|
|
'isu',
|
|
'jar',
|
|
'jsx',
|
|
'kix',
|
|
'mau',
|
|
'mel',
|
|
'mem',
|
|
'mrc',
|
|
'exe',
|
|
'pex',
|
|
'pif',
|
|
'plsc',
|
|
'pkg',
|
|
'prg',
|
|
'ps1',
|
|
'pwc',
|
|
'qit',
|
|
'rbx',
|
|
'rox',
|
|
'rxe',
|
|
'scar',
|
|
'scb',
|
|
'scpt',
|
|
'sct',
|
|
'seed',
|
|
'sh',
|
|
'u3p',
|
|
'vb',
|
|
'vbe',
|
|
'vbs',
|
|
'vbscript',
|
|
'vlx',
|
|
'widget',
|
|
'workflow',
|
|
'ws',
|
|
'xbe',
|
|
'xex',
|
|
'xys',
|
|
];
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse the content and replace file_upload shortcode configuration IDs with the actual config
|
|
*
|
|
*/
|
|
add_filter( 'tve_thrive_shortcodes', static function ( $content, $is_editor_page ) {
|
|
$content = preg_replace_callback( '#__FILE_SETUP__(\d+)__FILE_SETUP__#', static function ( $matches ) use ( $is_editor_page ) {
|
|
$file_config = FileUploadConfig::get_one( $matches[1] );
|
|
$replacement = $is_editor_page ? $file_config->get_config() : $file_config->get_frontend_config();
|
|
|
|
return esc_attr( $replacement );
|
|
}, $content );
|
|
|
|
return $content;
|
|
}, 10, 2 );
|
|
|
|
/**
|
|
* Sends an error response to be picked up by the plupload library
|
|
*
|
|
* @param string $error
|
|
* @param int $code
|
|
*/
|
|
function upload_error_handler( $error, $code = 400 ) {
|
|
status_header( $code );
|
|
echo esc_html( $error );
|
|
die();
|
|
}
|
|
|
|
/**
|
|
* Handle file uploads submitted through the Lead Gen element
|
|
*/
|
|
function handle_upload() {
|
|
if ( empty( $_REQUEST['id'] ) ) {
|
|
upload_error_handler( 'Missing required parameter' );
|
|
}
|
|
|
|
// this must mean that the uploaded file is too large
|
|
if ( empty( $_FILES ) ) {
|
|
upload_error_handler( 'Missing file, or file is too large' );
|
|
}
|
|
|
|
if ( ! empty( $_FILES['file']['error'] ) ) {
|
|
upload_error_handler( 'Error uploading file', 500 );
|
|
}
|
|
|
|
$config = FileUploadConfig::get_one( isset( $_REQUEST['id'] ) ? sanitize_text_field( $_REQUEST['id'] ) : '' );
|
|
if ( ! $config->ID ) {
|
|
upload_error_handler( 'Missing / Invalid request parameter' );
|
|
}
|
|
|
|
$info = pathinfo( $_FILES['file']['name'] );
|
|
if ( empty( $info['extension'] ) ) {
|
|
/* something is wrong here */
|
|
upload_error_handler( 'Invalid file name' );
|
|
}
|
|
|
|
if ( ! $config->extension_allowed( $info['extension'] ) ) {
|
|
upload_error_handler( 'This type of file is not accepted' );
|
|
}
|
|
|
|
if ( ! $config->size_allowed( filesize( $_FILES['file']['tmp_name'] ) ) ) { // phpcs:ignore
|
|
upload_error_handler( 'File is too big' );
|
|
}
|
|
|
|
$api = $config->get_service();
|
|
if ( is_wp_error( $api ) ) {
|
|
upload_error_handler( 'Could not determine API' );
|
|
}
|
|
|
|
$file_meta = array(
|
|
'originalName' => $_FILES['file']['name'], // phpcs:ignore
|
|
'name' => $config->get_upload_filename( $info['filename'] ) . '.' . $info['extension'],
|
|
);
|
|
|
|
/** @var string|\WP_Error $result */
|
|
$result = $api->upload( file_get_contents( $_FILES['file']['tmp_name'] ), $config->folder, $file_meta ); // phpcs:ignore
|
|
if ( is_wp_error( $result ) ) {
|
|
/**
|
|
* Log API Error
|
|
*/
|
|
global $wpdb;
|
|
|
|
$log_data = array(
|
|
'date' => date( 'Y-m-d H:i:s' ),
|
|
'error_message' => sanitize_text_field( $result->get_error_message() ),
|
|
'api_data' => serialize( array(
|
|
'email' => ! empty( $_REQUEST['email'] ) ? sanitize_email( $_REQUEST['email'] ) : '-',
|
|
'file_name' => $file_meta['name'],
|
|
) ),
|
|
'connection' => $config->api,
|
|
'list_id' => '',
|
|
);
|
|
|
|
$wpdb->insert( $wpdb->prefix . 'tcb_api_error_log', $log_data );
|
|
upload_error_handler( $result->get_error_message() );
|
|
}
|
|
|
|
/**
|
|
* This looks like a successful operation
|
|
*/
|
|
wp_send_json( array(
|
|
'success' => true,
|
|
'nonce' => FileUploadConfig::create_nonce( $result ), // generate nonce so that it can be used in file delete requests and validate the subsequent POST
|
|
'file_id' => $result,
|
|
'file_info' => $api->get_file_data( $result ),
|
|
) );
|
|
}
|
|
|
|
/**
|
|
* Handle file removal
|
|
*/
|
|
function handle_remove() {
|
|
if ( empty( $_POST['nonce'] ) || empty( $_POST['file_id'] ) || empty( $_POST['id'] ) ) {
|
|
/* don't generate any error messages */
|
|
exit();
|
|
}
|
|
|
|
if ( ! FileUploadConfig::verify_nonce( sanitize_text_field( $_POST['nonce'] ), sanitize_text_field( $_POST['file_id'] ) ) ) {
|
|
exit();
|
|
}
|
|
|
|
$api = FileUploadConfig::get_one( sanitize_text_field( $_POST['id'] ) )->get_service();
|
|
if ( is_wp_error( $api ) ) {
|
|
exit();
|
|
}
|
|
|
|
$api->delete( sanitize_text_field( $_POST['file_id'] ) );
|
|
exit();
|
|
}
|
|
|
|
/**
|
|
* Process the form data before sending it to various APIs
|
|
* Checks if the form data contains any files and, if a mapping is defined for the file field, make sure data contains a field populated with mapping data
|
|
* Mapped data must contain URLs to uploaded files
|
|
*
|
|
* @param array $data submitted post data
|
|
*
|
|
* @return array
|
|
*/
|
|
function process_form_data( $data ) {
|
|
if ( empty( $data['tcb_file_id'] ) ) {
|
|
return $data;
|
|
}
|
|
$config = FileUploadConfig::get_one( $data['tcb_file_id'] );
|
|
$api = $config->get_service();
|
|
if ( is_wp_error( $api ) ) {
|
|
return $data;
|
|
}
|
|
|
|
$file_data = [];
|
|
$file_urls = [];
|
|
$file_ids = [];
|
|
/* transform storage file IDs into URLs */
|
|
if ( ! empty( $data['_tcb_files'] ) ) {
|
|
/* if email has been submitted, and the filenames should include the email, we need to trigger a file rename for each submitted file */
|
|
if ( ! empty( $data['email'] ) && $config->needs_file_rename() ) {
|
|
foreach ( $data['_tcb_files'] as $file_id ) {
|
|
$file_data[ $file_id ] = $api->rename_file( $file_id, function ( $filename ) use ( $data, $api ) {
|
|
// "@" is allowed in filenames in Dropbox. Untested in other services.
|
|
if ( 'Thrive_Dash_List_Connection_FileUpload_Dropbox' === get_class( $api ) ) {
|
|
$email = sanitize_file_name( $data['email'] );
|
|
} else {
|
|
$email = str_replace( '@', '__A-Round__', sanitize_email( $data['email'] ) );
|
|
$email = str_replace( '__A-Round__', '+', sanitize_file_name( $email ) );
|
|
}
|
|
|
|
return str_replace( FileUploadConfig::EMAIL_FILENAME_TEMPLATE, $email, $filename );
|
|
} );
|
|
}
|
|
}
|
|
|
|
foreach ( $data['_tcb_files'] as $index => $file_id ) {
|
|
/* filedata gets populated if the file needs to be renamed. Using it to avoid extra API calls */
|
|
$data['_tcb_files'][ $index ] = isset( $file_data[ $file_id ] ) ? $file_data[ $file_id ] : $api->get_file_data( $file_id );
|
|
$file_urls[] = $data['_tcb_files'][ $index ]['url'];
|
|
$file_ids[ $index ] = $file_id;
|
|
}
|
|
}
|
|
/**
|
|
* Mapped field: needs the file URL to be sent to the autoresponder
|
|
*/
|
|
$data[ $data['tcb_file_field'] ] = $file_urls;
|
|
/**
|
|
* Preserve the file IDs in the form data so that they can be used later on
|
|
*/
|
|
$data['_tcb_files_upload_ids'] = $file_ids;
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Build an HTML list of file URLs based on the submitted files
|
|
*
|
|
* @param array $data post data
|
|
*
|
|
* @return string
|
|
*/
|
|
function build_html_file_list( $data ) {
|
|
if ( empty( $data['_tcb_files'] ) || ! is_array( $data['_tcb_files'] ) ) {
|
|
return '';
|
|
}
|
|
|
|
return sprintf(
|
|
'<ul style="margin:0">%s</ul>',
|
|
array_reduce( $data['_tcb_files'], static function ( $carry, $file ) {
|
|
return $carry . sprintf( '<li><a href="%s">%s</a></li>', esc_attr( $file['url'] ), esc_html( $file['name'] ) );
|
|
}, '' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Search and replace the [uploaded_files] shortcode with an HTML list of file links
|
|
*
|
|
* @param string $message
|
|
* @param array $data
|
|
*
|
|
* @return string
|
|
*/
|
|
function process_email_message( $message, $data ) {
|
|
return str_replace( '[uploaded_files]', build_html_file_list( $data ), $message );
|
|
}
|
|
|
|
/**
|
|
* Build the HTML to be displayed in [all_fields] shortcode inside the email message.
|
|
* Outputs the file list a HTML <ul> list element containing links to files and file names.
|
|
*
|
|
* @param string $value current value
|
|
* @param string $field field name. Only process if it starts with `mapping_file_`
|
|
* @param array $data form submission data
|
|
*
|
|
* @return string
|
|
*/
|
|
function process_email_field( $value, $field, $data ) {
|
|
|
|
if ( strpos( $field, 'mapping_file_' ) === 0 ) {
|
|
$value = build_html_file_list( $data );
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
add_action( 'wp_ajax_nopriv_tcb_file_upload', 'TCB\inc\helpers\handle_upload' );
|
|
add_action( 'wp_ajax_tcb_file_upload', 'TCB\inc\helpers\handle_upload' );
|
|
|
|
add_action( 'wp_ajax_nopriv_tcb_file_remove', 'TCB\inc\helpers\handle_remove' );
|
|
add_action( 'wp_ajax_tcb_file_remove', 'TCB\inc\helpers\handle_remove' );
|
|
|
|
add_filter( 'tcb_before_api_subscribe_data', 'TCB\inc\helpers\process_form_data' );
|
|
add_filter( 'thrive_api_email_message', 'TCB\inc\helpers\process_email_message', 10, 2 );
|
|
add_filter( 'thrive_email_message_field', 'TCB\inc\helpers\process_email_field', 10, 3 );
|