How to send a contact form in Wordpress with multiple attachments via email without using any plugins

The main objective is to enable users to submit their own content for new articles (including text and files) to the administrator's email.

A form has been created for this purpose:

<form class="form form-send" enctype="multipart/form-data">
    <div class="send__inputs-block">
        <div class="sliding-label__block send__textarea-block">
            <textarea name="comment" id="comment" class="input textarea send__textarea" placeholder=" "></textarea>
            <label for="comment" class="text text--fz-18 sliding-label">
                <?php echo get_post_meta( get_the_ID(), 'joint_send_comment_text', true ); ?>
            </label>
        </div>
        <div class="sliding-label__block">
            <input id="name" type="text" name="name" class="input input-text" placeholder=" ">
            <label for="name" class="text text--fz-18 sliding-label">
                <?php echo get_post_meta( get_the_ID(), 'joint_send_name_text', true ); ?>
            </label>
        </div>
    </div>
    <div class="send__file-block">
        <input id="file" type="file" name="file" class="input-file" multiple>
        <label for="file" class="label label-file">
            <i class="joint-upload icon"></i>
            <span class="text text--fz-14 upload__text">
                <?php echo get_post_meta( get_the_ID(), 'joint_send_file_text', true ); ?>
            </span>
        </label>
    </div>
    <button type="submit" class="button form-button">
        <span class="button-text">
            <?php echo get_post_meta( get_the_ID(), 'joint_send_submit_text', true ); ?>
        </span>
    </button>
</form>

JS code implementation:

function handleFormSubmission(e, form) {
    e.preventDefault()

    if (validateForm(form)) return

    let serverEndpoint = jointAjax.ajaxurl
    let currentPage = ''

    let formData = new FormData(form)

    if (form.classList.contains('form-contacts')) {
       currentPage = 'contacts'
    }
    else if (form.classList.contains('form-send')){
        currentPage = 'send'
        let uploadedFiles = []
        for (let file of form.file.files) {
            uploadedFiles.push(file)
        }
        console.log(uploadedFiles[0])
        formData.append('file', uploadedFiles)
    }
    else {
        return
    }

    formData.append('action', currentPage)

    fetch(serverEndpoint, {
       method: 'POST',
       body: formData,
    })
        .then(form.reset())
        .catch(error => {
            error.json().then(response => {
               alert(response.message)
            })
        })
}

PHP code snippet:

add_action('wp_ajax_send', 'handleFormSubmission');
add_action('wp_ajax_nopriv_send', 'handleFormSubmission');
function handleFormSubmission() {
    global $joint_settings;

    $data = json_encode($_POST);
    $data = json_decode($data, true);

    $attachments = array();
    if (isset($_FILES['file'])) {
        foreach($_FILES['file'] as $key => $file) {
            $attachments[] = $file['tmp_name'] . $file['name'];
        }
    }

    $emailBody = '';

    foreach ($data as $key => $value) {
        if ($key === 'action' || $key === 'file') continue;
        if (!empty($data[$key]))
            $emailBody .= '<p><strong>' . ucfirst($key) . ':</strong> ' . esc_html($value) . '</p>';
    }

    $headers = array(
        'From: Joint Admin <' . SMTP_FROM . '>',
        'content-type: text/html'
    );

    wp_mail(
        $joint_settings['send_mail_to'],
        $joint_settings['send_mail_theme'],
        $emailBody,
        $headers,
        WP_CONTENT_DIR . '\\' . $_FILES['file']['name']
    );

    // wp_send_json_success($_FILES['file']['tmp_name'] . '\\' . $_FILES['file']['name']);

I've exhaustively searched through various forums, articles, and tutorials, but I'm still struggling with this task.

In the wp_mail function, we need to provide the full path to the file, but where can we obtain that path?

Despite my efforts to process multiple files, the function only returns the name of the last file in response.

SMTP settings are correct, emails are being received, but without any attachments.

Please assist me - I'm at a loss on how to proceed.

Answer №1

I finally cracked the code - after studying the implementation within the Contact form 7 plugin and grasping the underlying principles.

The main concept involves relocating the uploaded file to the site's internal directory initially, before forwarding it via email from there.

In JS, I made a single alteration (enabling direct file sending without prior processing):

else if (form.classList.contains('form-send')){
    formData.append('file', form.file.files)
    curPage = 'send'
}

My handler function is structured as follows:

add_action('wp_ajax_send', 'joint_send_send_form');
add_action('wp_ajax_nopriv_send', 'joint_send_send_form');
function joint_send_send_form() {
    global $joint_settings;

    $data = json_encode($_POST);
    $data = json_decode($data, true);

    $mailBody = '';

    foreach ($data as $key => $value) {
        if ($key === 'action' || $key === 'file') continue;
        if (!empty($data[$key]))
            $mailBody .= '<p><strong>' . ucfirst($key) . ':</strong> ' . esc_html($value) . '</p>';
    }

    $headers = array(
        'From: Joint Admin <' . SMTP_FROM . '>',
        'content-type: text/html'
    );

    $file = $_FILES['file'];
    // joint_array_flatten() - converts multi-dimensional array to a flat array
    $names = joint_array_flatten( $file['name'] );
    $tmp_names = joint_array_flatten( $file['tmp_name'] );

    $uploads = wp_get_upload_dir()['basedir'];
    $uploads_dir = path_join( $uploads, 'joint_uploads' );

    $uploaded_files = array();

    foreach ( $names as $key => $filename ) {
        $tmp_name = $tmp_names[$key];

        if ( empty( $tmp_name ) or ! is_uploaded_file( $tmp_name ) ) {
            continue;
        }

        // joint_antiscript_file_name() - conversion of file name to prevent script execution
        $filename = joint_antiscript_file_name( $filename );

        $filename = wp_unique_filename( $uploads_dir, $filename );
        $new_file = path_join( $uploads_dir, $filename );

        if ( false === @move_uploaded_file( $tmp_name, $new_file ) ) {
            wp_send_json_error( json_encode( array('message' => 'Upload error') ) );
            return;
        }

        // Ensuring file permissions only allow reading by owner process
        chmod( $new_file, 0400 );

        $uploaded_files[] = $new_file;
    }

    wp_mail(
        $joint_settings['send_mail_to'],
        $joint_settings['send_mail_theme'],
        $mailBody,
        $headers,
        $uploaded_files
    );

    // Deleting sent files post emailing
    foreach ( $uploaded_files as $filepath ) {
        wp_delete_file( $filepath );
    }
}

I borrowed some ideas from Contact form 7. The anti-script feature is definitely valuable, but I'm not certain about the necessity of joint_array_flatten - I'm too exhausted to ponder further.

Lastly, I clear out downloaded files from the directory to avoid clutter accumulation.

Also, as noted correctly by user @TangentiallyPerpendicular, remember to include [] in the file-input name for transmitting multiple files:

<input id="file" type="file" name="file[]" class="input-file" multiple>

That's all for now. Hopefully, this guide will assist someone else down the line, sparing them the agony I endured on a similar task)

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Shift the content downwards within an 'a' snippet located in the sidebar when it reaches the edge of the right border

I'm having an issue with my style.css file. As a beginner in programming, I am trying to position text next to an image attached in the sidebar. However, the text is overflowing past the border. Here's the HTML code: Please see this first to ide ...

The error message states that the property "user" is not found in the type "Session & Partial<SessionData>"

I recently had a javascript code that I'm now attempting to convert into typescript route.get('/order', async(req,res) => { var sessionData = req.session; if(typeof sessionData.user === 'undefined') { ...

Add an External PHP File to PHP Configuration File

How do I properly include a PHP library in my php.ini file so that it can be accessed by all sites on the server? I have added the following line to my php.ini: include_path = ".:/Users/myname/Sites/edr/includeroot/application_top.php" However, the libra ...

Adjust font size using jQuery to its maximum and minimum limits

My jQuery script enables me to adjust the font-size and line-height of my website's CSS. However, I want to restrict the increase size to three clicks and allow the decrease size only after the increase size link has been clicked - ensuring that the d ...

I'm having trouble getting my accordion menu to work properly. Whenever I click on the question title, the answer doesn't seem to show up or collapse. What could be causing this issue?

I am currently working on a project to create an accordion menu where the answers are hidden and only revealed upon clicking the question. However, when I test it out, nothing happens when I click on the question. Here is my code: .accordion-Section ...

How to set up 'ng serve' command in Angular to automatically open a private browsing window?

I am looking for a way to open my project in an Incognito Mode browser without storing any cache. Is there a specific Angular CLI flag that can be included in the ng serve -o command or in the Angular CLI configuration file to enable opening a browser in ...

Tips for showcasing JSON data within an array of objects

I'm trying to work with a JSON file that has the following data: {"name": "Mohamed"} In my JavaScript file, I want to read the value from an array structured like this: [{value: "name"}] Any suggestions on how I can acc ...

The error message "Unexpected node environment at this time" occurred unexpectedly

I am currently watching a tutorial on YouTube to enhance my knowledge of Node.js and Express. To enable the use of nodemon, I made modifications to my package.json file as shown below: package.json "scripts": { "start": "if [[ $NODE_ENV == 'p ...

Selection of checkboxes and utilization of arrays

I currently have a total of 9 check boxes on my select.php page <form method="post" action="test.php"> <input type="checkbox" name="g1[]" id="c1" value="c1"> <input type="checkbox" name="g1[]" id="c2" value="c2"> <input type="checkbox ...

Nested arrays in the JavaScript programming language

Is this JavaScript code accurate: var options = []; options.push(["Tom","Smith"]); options.push(["Mary","Jones"]); where each item in options is a two-element array of strings. I plan to add items to options using a for loop. Furthermore, can I i ...

Tips for concealing XHR Requests within a react-based single page application

Is there a way to hide the endpoint visible in Chrome's devtools under the network tab when data is fetched in React? Can server-side rendering solve this issue? ...

Emphasizing the text while making edits to an item within the dhtmlx tree

Whenever I need the user to rename an item on the tree, I trigger the editor for them: tree.editItem(tree.getSelectedItemId()); However, I want the text in the editor to be automatically selected (highlighted). Currently, the cursor is placed at the end ...

Is it possible to identify the form triggering the ajax call within a callback function?

There are multiple forms on my website that share the same structure and classes. The objective is to submit form data to the server using the POST method, and display an error message if any issues arise. Here's how the HTML code for the forms look ...

Navigating through multiple pages with React Native stack navigation

I'm currently in the process of developing a react native app and I'm facing some confusion with regards to page navigation. It appears that there is a glitch in the navigation flow, causing it to skip a page. <NavigationContainer> ...

If the FedEx function does not receive a payment, it will need to return a value of Payment Required

I am struggling with understanding functions, parameters, and arguments in JavaScript as I am new to it. My goal is to have a function that returns different values based on the payment amount. If no payment is passed, it should return "Payment Required", ...

Do you need to finish the Subject when implementing the takeUntil approach to unsubscribing from Observables?

In order to prevent memory leaks in my Angular application, I make sure to unsubscribe from Observables using the following established pattern: unsubscribe = new Subject(); ngOnInit() { this.myService.getStuff() .pipe(takeUntil(this.unsubscr ...

Having difficulty displaying form errors using handlebars

My form validation is not working properly. When I enter incorrect information, it alerts correctly, but when I submit the form, it returns [Object object]. What could be causing this issue in my code and how should I handle the data? https://i.stack.imgu ...

Array contains a copy of an object

The outcome I am striving for is: dataset: [ dataset: [ { seriesname: "", data: [ { value: "123", }, { value: &q ...

What is the method for defining the current date variable within a .json object?

When using a post .json method to send an object, I encounter the following situation: { "targetSigningDate": "2021-09-22T21:00:00.000Z" } The "targetSigningDate" always needs to be 'sysdate'. Rather than manually c ...

Ways of converting a negative lookbehind into an ES5-friendly expression

In my code, I have a RegExp that works perfectly, but it only functions with ES2018 due to its use of negative lookbehinds. The problem is that a library is using this RegExp function, so modifying how it's used is not an option. I attempted to add n ...