Dashboard
PHP:
8.4.22
OS:
Linux
User:
provencez
/
/
home
/
provencez
/
www
/
wp-content
/
plugins
/
flexa-block
/
includes
📤 Upload
📝 New File
📁 New Folder
Close
Editing: class-form-handler.php
<?php declare(strict_types=1); /** * Form Handler — server side of the Subscribe Form submission. * * Fields are collected on the front end (view.js) and POSTed to admin-ajax * (action `flexa_subscribe`). This class verifies the nonce, drops obvious spam * (honeypot), re-reads the form block from the saved post to find the trusted * destination email (so the recipient can never be set from the browser), builds * an HTML email from the submitted values and sends it with wp_mail(). No data * is stored in the database. * * @package Flexa\Block */ namespace Flexa\Block; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * AJAX submission handler for flexa/subscribe-form. */ class Form_Handler { /** * Max number of fields accepted from one submission. * * @var int */ const MAX_FIELDS = 50; /** * Max characters kept per submitted value. * * @var int */ const MAX_VALUE_LENGTH = 5000; /** * Max size (bytes) accepted per uploaded file. * * @var int */ const MAX_UPLOAD_BYTES = 10485760; // 10 MB. /** * Max number of files attached to one submission. * * @var int */ const MAX_UPLOAD_FILES = 10; /** * Hook registration. */ public static function init() { add_action( 'wp_ajax_flexa_subscribe', [ __CLASS__, 'process' ] ); add_action( 'wp_ajax_nopriv_flexa_subscribe', [ __CLASS__, 'process' ] ); } /** * Handle a subscribe-form submission. */ public static function process() { check_ajax_referer( 'flexa_subscribe', 'nonce' ); // Honeypot: a bot filled the hidden field — accept silently, send nothing. if ( ! empty( $_POST['flexa_hp'] ) ) { wp_send_json_success(); } $post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0; $block_id = isset( $_POST['block_id'] ) ? sanitize_html_class( wp_unslash( $_POST['block_id'] ) ) : ''; $raw_data = isset( $_POST['form_data'] ) ? json_decode( wp_unslash( $_POST['form_data'] ), true ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- decoded values are sanitized below. $form_data = self::sanitize_form_data( is_array( $raw_data ) ? $raw_data : [] ); // Uploaded files: validated + moved to temp copies, their names folded into // the email body, the copies attached and cleaned up after sending. $uploads = self::collect_uploads(); foreach ( $uploads['names'] as $key => $names ) { $form_data[ $key ] = $names; } if ( empty( $form_data ) ) { self::cleanup_temp( $uploads['temp'] ); wp_send_json_error( [ 'message' => 'empty' ] ); } $config = self::resolve_form_config( $post_id, $block_id ); $to = $config['to']; if ( '' === $to || ! is_email( $to ) ) { self::cleanup_temp( $uploads['temp'] ); wp_send_json_error( [ 'message' => 'no_recipient' ] ); } $sent = wp_mail( $to, $config['subject'], self::build_body( $form_data ), self::build_headers( $form_data ), $uploads['attachments'] ); self::cleanup_temp( $uploads['temp'] ); if ( $sent ) { do_action( 'flexa_block_subscribe_submitted', $form_data, $config, $post_id ); wp_send_json_success(); } wp_send_json_error( [ 'message' => 'send_failed' ] ); } /** * Validate and stage uploaded files: check each against WordPress's allowed * MIME types and the size cap, move it to a temp copy, and collect the copy * paths (for attachment), the temp paths (for cleanup) and per-field name * summaries (for the email body). * * @return array{attachments:array<int,string>,temp:array<int,string>,names:array<string,string>} */ private static function collect_uploads() { $result = [ 'attachments' => [], 'temp' => [], 'names' => [], ]; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified in process() via check_ajax_referer() before this method runs. if ( empty( $_FILES ) || ! is_array( $_FILES ) ) { return $result; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing -- each field/value is validated below; nonce verified in process(). foreach ( $_FILES as $field => $data ) { $field_key = sanitize_key( (string) $field ); if ( '' === $field_key ) { continue; } $collected = []; foreach ( self::normalize_files( $data ) as $file ) { if ( count( $result['attachments'] ) >= self::MAX_UPLOAD_FILES ) { break 2; } if ( ! isset( $file['error'] ) || UPLOAD_ERR_OK !== (int) $file['error'] ) { continue; } $tmp = (string) ( $file['tmp_name'] ?? '' ); if ( '' === $tmp || ! is_uploaded_file( $tmp ) ) { continue; } if ( (int) ( $file['size'] ?? 0 ) > self::MAX_UPLOAD_BYTES ) { continue; } $filename = sanitize_file_name( (string) ( $file['name'] ?? '' ) ); if ( '' === $filename ) { continue; } // WordPress's own allow-list keeps out executables (php, exe, …). $check = wp_check_filetype( $filename ); if ( empty( $check['ext'] ) ) { continue; } $dest = wp_tempnam( $filename ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, Generic.PHP.ForbiddenFunctions.Found -- is_uploaded_file() is verified above; move_uploaded_file() is the correct primitive for staging a raw HTTP upload. if ( ! $dest || ! @move_uploaded_file( $tmp, $dest ) ) { if ( $dest ) { wp_delete_file( $dest ); } continue; } $result['attachments'][] = $dest; $result['temp'][] = $dest; $collected[] = $filename; } if ( ! empty( $collected ) ) { $result['names'][ $field_key ] = implode( ', ', $collected ); } } return $result; } /** * Normalise a single `$_FILES` entry into a list of per-file arrays, covering * both the single-file and multiple-file (`name[]`) shapes. * * @param mixed $data One `$_FILES` entry. * @return array<int,array<string,mixed>> */ private static function normalize_files( $data ) { if ( ! is_array( $data ) || ! isset( $data['name'] ) ) { return []; } if ( is_array( $data['name'] ) ) { $out = []; $count = count( $data['name'] ); for ( $i = 0; $i < $count; $i++ ) { $out[] = [ 'name' => $data['name'][ $i ] ?? '', 'tmp_name' => $data['tmp_name'][ $i ] ?? '', 'error' => $data['error'][ $i ] ?? UPLOAD_ERR_NO_FILE, 'size' => $data['size'][ $i ] ?? 0, ]; } return $out; } return [ $data ]; } /** * Delete the temp upload copies made in collect_uploads(). * * @param array<int,string> $paths Temp file paths. */ private static function cleanup_temp( $paths ) { foreach ( $paths as $path ) { if ( is_string( $path ) && '' !== $path && file_exists( $path ) ) { wp_delete_file( $path ); } } } /** * Sanitise the decoded form data: cap the field count, keys to a safe slug, * values to text with a length cap. * * @param array $data Raw decoded data. * @return array<string,string> */ private static function sanitize_form_data( $data ) { $clean = []; $count = 0; foreach ( $data as $key => $value ) { if ( $count >= self::MAX_FIELDS ) { break; } $key = sanitize_key( (string) $key ); if ( '' === $key ) { continue; } if ( is_array( $value ) ) { $value = implode( ', ', array_map( 'strval', $value ) ); } $value = sanitize_textarea_field( (string) $value ); if ( '' === $value ) { continue; } if ( strlen( $value ) > self::MAX_VALUE_LENGTH ) { $value = substr( $value, 0, self::MAX_VALUE_LENGTH ); } $clean[ $key ] = $value; ++$count; } return $clean; } /** * Read the destination email + subject from the saved form block. Falls back * to the site admin email + a default subject when the block or its settings * can't be resolved, so the recipient is always a value the site owner * controls — never one supplied by the request. * * @param int $post_id Post the form lives on. * @param string $block_id The form's blockId. * @return array{to:string,subject:string} */ private static function resolve_form_config( $post_id, $block_id ) { $admin_email = (string) get_option( 'admin_email' ); $config = [ 'to' => $admin_email, 'subject' => __( 'New subscription', 'flexa-block' ), ]; if ( ! $post_id || '' === $block_id ) { return $config; } $content = get_post_field( 'post_content', $post_id ); if ( ! is_string( $content ) || '' === $content ) { return $config; } $attrs = self::find_form_attrs( parse_blocks( $content ), $block_id ); if ( null === $attrs ) { return $config; } $to = trim( (string) ( $attrs['toEmail'] ?? '' ) ); if ( '' !== $to ) { $config['to'] = sanitize_email( $to ); } $subject = trim( (string) ( $attrs['emailSubject'] ?? '' ) ); if ( '' !== $subject ) { $config['subject'] = sanitize_text_field( $subject ); } return $config; } /** * Recursively locate the flexa/subscribe-form block with a given blockId and * return its attributes. * * @param array $blocks Parsed blocks. * @param string $block_id Target blockId. * @return array|null */ private static function find_form_attrs( $blocks, $block_id ) { foreach ( $blocks as $block ) { if ( ! is_array( $block ) ) { continue; } if ( 'flexa/subscribe-form' === ( $block['blockName'] ?? '' ) && isset( $block['attrs']['blockId'] ) && $block['attrs']['blockId'] === $block_id ) { return $block['attrs']; } if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { $found = self::find_form_attrs( $block['innerBlocks'], $block_id ); if ( null !== $found ) { return $found; } } } return null; } /** * Build the HTML email body from the submitted fields. * * @param array<string,string> $form_data Sanitised field data. * @return string */ private static function build_body( $form_data ) { $rows = ''; foreach ( $form_data as $key => $value ) { $label = ucwords( str_replace( '_', ' ', $key ) ); $rows .= '<tr>' . '<td style="padding:6px 12px;font-weight:bold;vertical-align:top;">' . esc_html( $label ) . '</td>' . '<td style="padding:6px 12px;">' . nl2br( esc_html( $value ) ) . '</td>' . '</tr>'; } $intro = sprintf( /* translators: %s: site name. */ esc_html__( 'A new subscription was submitted on %s.', 'flexa-block' ), esc_html( (string) get_bloginfo( 'name' ) ) ); return '<div style="font-family:sans-serif;max-width:600px;">' . '<p>' . $intro . '</p>' . '<table style="border-collapse:collapse;width:100%;">' . $rows . '</table>' . '</div>'; } /** * Build the wp_mail headers (HTML + a Reply-To when the entry carries an email). * * @param array<string,string> $form_data Sanitised field data. * @return array<int,string> */ private static function build_headers( $form_data ) { $headers = [ 'Content-Type: text/html; charset=UTF-8' ]; foreach ( $form_data as $value ) { if ( is_email( $value ) ) { $headers[] = 'Reply-To: ' . sanitize_email( $value ); break; } } return $headers; } }
Save
Cancel