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( '', array_reduce( $data['_tcb_files'], static function ( $carry, $file ) { return $carry . sprintf( '
  • %s
  • ', 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