0

Trying to make a plugin that can upload files directly to WP. Despite copying different blocks of code online, it seems determined to not let me upload it.

The way I'm attempting is through a XMLHttpRequest to a php file with wp_handle_upload. I append the input file element to a formdata and send it through the request. The data that's sent is correct and in order, as i echoed it multiple times to make sure of it, but when it gets to wp_handle_upload, it stops working and the network tab says there was a 500 error on the request.

I've tried calling wp_handle_upload with only the first parameter but it tells me that the form submission is invalid. I've also parsed the file data to a new variable like what's done here but to the same result.

javascript code that handles the input element file:

if ($('#active_p').is(":checked")) personDataActive = 1; //since checked isn't a boolean property, we have to analyze it to get the correct value
else personDataActive = 0;

var personDataUsers = new Array;
var personDataPlaces = new Array;

$("input[name='check_list_user[]']:checked").each(function () {
    personDataUsers.push($(this).val());
})
$("input[name='check_list_places[]']:checked").each(function () {
    personDataPlaces.push($(this).val());
})

//depending on the value of the id, it'll either update or insert a person
if ($('#id_p').val() > 0) id = $('#id_p').val(); //its a new person
else id = 0; //its an already existing person           

let image = $('#profile_picture').prop('files');

const formData = new FormData();
formData.append("profile_picture", image[0]);
            
formData = new FormData();
formData.append('image', image[0]);
$.ajax({
    url: "imageUpload.php",                
    data: formData,
    processData: false,
    contentType: false,
    type: "POST",
    success: function(response){
        console.log(response);
    },
    error: function(response) { console.log("error: " + response)}
});
            
//pass all of the data as an array to make the ajax data more clean. 0 is id, 1 is name, 2 is active, 3 is birthdate, 4 is internalid, 5 is media url, 6 is users and 7 is places

personData = [id, $('#name_p').val(), personDataActive, $('#birthdate_p').val(), $('#internalid_p').val(), personDataUsers, personDataPlaces];

//ajax call to add new person!!!
$.ajax({
    url: 'ajaxToPhpCalls.php', //the file to send the data to. preferably, leave it on the same folder to avoid problems
    type: 'POST', //it can always be POST even if the intention is getting values, still, it's better to declare the intended type to not accidentaly overwrite stuff
    data: { callFunction : "Insert", personData: personData.join(',')}, //we can't send an array directly, we have to use the .join() function to send it complete
    success: function(response) {
        //the response can be used for debug reasons, since it will return the same data as in Postman
        //alert("Success");
        console.log(response);

        //hide the modal on success so there's a visual cue for the user. if it fails, they can resend the request
        hideCreateModal();

        var total = $("#totalNumber").val();
        $("#totalNumber").val(total++); //add the new person to the total. val()++ doesn't work outside, may work inside the ()
        //console.log($("#totalNumber").val());
        $("body").css("cursor", "default");
        resetNewPersonFields();
        ResetTable();
        $("body").css("cursor", "default");
    },
    error: function() {alert("Error"); $("body").css("cursor", "default");},
});

imageUpload.php for WP media upload

<?php

//load both to make sure wordpress core functions are loaded
require( dirname(__FILE__) . '/../../../../../wp-config.php' );
require( dirname(__FILE__) . '/../../../../../wp-load.php' );

require_once( ABSPATH . 'wp-admin/includes/file.php' );

//check if request has required variable
if (! empty($_FILES["image"]))
{        
    $upload_overrides = array( 'test_form' => FALSE );

    $movefile = wp_handle_upload( $_FILES['image'], $upload_overrides);
    
    if ( $movefile && !isset( $movefile['error'] ) ) {
        $ufiles = get_post_meta( $post_id, 'my_files', true );
        if( empty( $ufiles ) ) $ufiles = array();
        $ufiles[] = $movefile;
        update_post_meta( $post_id, 'my_files', $ufiles );
    }
    exit;
}
else return "No files detected";

file input html

<div id="Imagens" class="tabcontent" style="display: none;">
    <div class="form-group">
        <label>Imagem</label><br>                                
        <input type="file" id="profile_picture" name="profile_picture"><br>
        <label>ou</label><br>
        <a href="#" id="insertImageAnchor" class="button">Adicionar Imagem</a><br>
        <img id="preview" src="https://cdn-icons-png.flaticon.com/512/983/983213.png" width=350px>
        </form>
    </div>
</div>
20
  • you should not be sending requests to a standalone PHP file in your theme/plugin, it's fragile and a major security problem. Use the REST API or admin AJAX instead, then you can handle the request in a filter or callback without dangling files. imageUpload.php is a major security mistake. Also check your PHP error log
    – Tom J Nowell
    Commented Jul 26, 2022 at 10:49
  • I'm conscious of that, I'm using this method to make 100% sure that it is working and can be incorporated to the rest of the code. I'd rather have it isolated to guarantee the problem is in a few lines/places than already implemented throughout multiple files. The error log doesn't point anything out from the file, even with wp_debug activated
    – Demonipo
    Commented Jul 26, 2022 at 11:33
  • this method will be flagged by a lot of security software and CDN security rules as suspicious, it is not helping your debug process it is hurting you, and also preventing you from using a lot of useful tools. For example Query Monitor has special handling for errors in AJAX and REST endpoints. As for the error, I'd suggest adding in a lot more code, it's not clear what the HTML structure is, so it's difficult to see if the first code snippet is correct
    – Tom J Nowell
    Commented Jul 26, 2022 at 16:49
  • just added more code related to it, thanks for the suggestion. I haven't noticed if it's been flagged or not, but doing some return "hit"; or echo "hit"; has been successful without any issues, hence being able to pinpoint the issue to the wp_handle_upload() snippet. I'm also still learning some wordpress aspects so I appreciate your tips.
    – Demonipo
    Commented Jul 27, 2022 at 8:47
  • also, you say if this then do that but there are no else do this or otherwise do that to print out messages. So e.g if if (! empty($_FILES["profile_picture"])) is false no code is ever run to tell you that, it's just assumed it will work. PS: There are good plugins for handling local uploads of avatars and profile pics, you don't need to reinvent the wheel
    – Tom J Nowell
    Commented Jul 27, 2022 at 12:36

1 Answer 1

0

Note: Currently untested, but posting here as I write it

The PHP can go in a plugin or your themes functions.php and will give you an endpoint at yoursite.com/wp-json/demonipo/v1/profilephoto that you can make POST requests to via AJAX. Note it assumes you want only logged in users to use this. If that's not the case, change is_user_logged_in to __return_true. Don't forget to resave permalinks before trying to use it:

PHP:

<?php

add_action( 'rest_api_init', 'demonipo_rest_api_init' );

function demonipo_rest_api_init() : void {
    register_rest_route( 'demonipo/v1' , '/profilephoto/', [
        'methods' => 'POST',
        'callback' => 'demonipo_rest_profilephoto', // function to run when this is called
        'permission_callback' => 'is_user_logged_in', // only logged in users
    ] );
}
/**
 * Handle the uploading of a profile photo
 *
 * @param \WP_REST_Request $request the AJAX request with all the fields WP recieved
 * @return mixed returns a WP_Error if something went wrong, otherwise it returns an
 *               array with some info about the uploaded file
 */
function demonipo_rest_profilephoto( $request ) {
    // Grab all our parameters, the file and the post to add it to

    // Grab Post ID and check it's legit
    if ( empty( $request['postid'] ) ) {
        return new WP_Error( 'invalid', 'You need to tell us which post this is going on' );
    }

    $post_id = $request['postid'];

    // is it really a post though?
    $post = get_post( $post_id );
    if ( ! $post instanceof WP_Post ) {
        return new WP_Error( 'invalid', 'No post was found using the post ID you gave' );
    }

    $files = $request->get_file_params();
    $headers = $request->get_headers();

    if ( empty( $files['profile_picture'] ) ) {
        return new WP_Error( 'invalid', 'No profile photo was found!' );
    }

    $file = $files['profile_picture'];

    // Check the upload worked and is valid:

    // confirm file uploaded via POST
    if ( ! is_uploaded_file( $file['tmp_name'] ) ) {
        return new WP_Error( 'error', 'Is uploaded file check failed!' );
    }

    // confirm no file errors
    if (! $file['error'] === UPLOAD_ERR_OK ) {
        return new WP_Error( 'error', 'Upload error!' . $file['error'] );
    }

    $att_id = media_handle_upload( 'profile_picture', $post_id );

    // if sideloading failed, return the error so we know what happened:
    if ( is_wp_error( $att_id ) ) {
        return $att_id;
    }

    $new_data = [
        'file' => basename( wp_get_attachment_image_url( $att_id, 'full' ) ),
        'url' => wp_get_attachment_image_url( $att_id, 'full' ),
        'type' => 'image/jpeg',
    ];

    // All is good! Update the post meta
    $ufiles = get_post_meta( $post_id, 'my_files', true );
    if( empty( $ufiles ) ) {
        $ufiles = [];
    }
    $ufiles[] = $new_data;
    update_post_meta( $post_id, 'my_files', $ufiles );

    // return any necessary data in the response here
    return rest_ensure_response( $new_data );

}

Important notes:

  • I noticed it used $post_id in the PHP but it was never defined anywhere so it had no way to know which post to update. Make sure to include a post_id field in your formdata.
  • wp_handle_upload will move the file into the right place, but it won't create attachments, so I swapped it for media_handle_upload. This is the missing piece you needed for it to show in the media library. I've also set it to attach it to the post you pass it
  • This article was very informative in writing this: https://firxworx.com/blog/wordpress/adding-an-endpoint-to-wordpress-rest-api-for-file-uploads/ you may recognise some parts of the error checking
  • lots more error checking, it should never be triggered but if it does then the REST API will pass it back to your javascript and jQuery will kick up an error you can read
  • I wasn't sure off the top of my head how to grab the mimetype so I just hardcoded it to image/jpeg. I also told it to return the fullsize image, but WP will also have created thumbnail sizes etc
  • If you can, try to store the attachment ID rather than the filename/URL, it'll make migrations much easier
  • This article/gist was also informative, though I would recommend using a PHP based block rather than a shortcode these days: https://gist.github.com/ahmadawais/0ccb8a32ea795ffac4adfae84797c19a

That last gist has JS code that's close to what you want:

// Check, if a file is selected.
if ( 'undefined' === typeof( jQuery( '#profile_picture' )[0].files[0] ) ) {
    alert( 'Select a file!' );
    return;
}

// Grab the file from the input.
var file = jQuery( '#profile_picture' )[0].files[0];
var formData = new FormData();
formData.append( 'file', file );

// TODO: Add Put the post ID in your HTML somewhere
var post_id = jQuery( '#post_id' )
formData.append( 'post_id', post_id );

// Fire the request.
jQuery.ajax( {
    url: '/wp-json/demonipo/v1/profilephoto',
    method: 'POST',
    processData: false,
    contentType: false,
    data: formData
} ).success( function ( response ) {
    console.log( 'success!' );
    console.log( response );
} ).error( function( response ) {
    console.log( 'error' );
    console.log( response );
});

You will need to add a hidden input with the post ID named post_id with the same ID and name alongside that file input for this to work.

e.g.

<input type="hidden" id="post_id" name="post_id" value="<?php ECHO POST ID HERE ?>" />
<input type="file" id="profile_picture" name="profile_picture"><br>

There is one thing I left out that I don't think is necessary, the REST API nonce. Normally for authenticated requests you have to pass a nonce, which isn't hard but I don't think you need to. If you do, the API will just tell you by saying the request is forbidden, if so let me know. How to do that is covered in some of the links I added so it isn't too much work.

I've also used jQuery instead of $ as the jQuery that comes with WordPress uses no conflict mode by default, but feel free to change that.


As a general guide, you can take a random PHP file like this:

<?php
//load both to make sure wordpress core functions are loaded
require( dirname(__FILE__) . '/../../../../../wp-config.php' );
require( dirname(__FILE__) . '/../../../../../wp-load.php' );

echo "Hi there " . $_GET['name'];

And convert it into a pretty URL REST API endpoint like this:

<?php

add_action( 'rest_api_init', 'demonipo_rest_api_init' );

function demonipo_rest_api_init() : void {
    register_rest_route( 'demonipo/v1' , '/randomphp/', [
        'methods' => 'GET',
        'callback' => 'demonipo_randomphp', // function to run when this is called
        'permission_callback' => '__return_true',
    ] );
}

function demonipo_randomphp( $request ) {
    return 'Hi there' . $request['name'];
}
11
  • 1
    I just tested it and it does return an authentication error. tried to check what part of the code needed the token but I'm too used to CURL requests, so I'm confused as to where I should add it. I'm going to try and figure out how to deal with $post_id when this starts working, I'm not a fan of hidden inputs so if I find a way to avoid them I'll go with that. With that out of the way, this is so awesome!!! I was expecting something more difficult but it really does look easy. the comments also help a lot and get a giggle out of me. this is really dope!
    – Demonipo
    Commented Jul 29, 2022 at 9:03
  • switch to __return_true for now, as for hidden inputs you shouldn't be afraid of them, they're a part of the HTML spec for a reason and serve a valuable purpose. Otherwise you have to insert them as data attributes and it isn't as semantic. Ideally your UI would work as just plaintext HTML, or at least be close to it for accessibility purposes. I'm not sure how I can help with retrieving the post ID though as I'm not sure how the rest of that page works or the setup you have but if you find any other issues with my code let me know and I'll try to update it, it hasn't been tested thoroughly
    – Tom J Nowell
    Commented Jul 29, 2022 at 12:43
  • it does seem to be working, at least in terms of saying im sending the data wrong eheh. I've switched the code so it is incorporated with the rest of the plugin, while being able to make a curl call that sends the token im saving for other calls that need it. while i don't have a problem anymore with authentication, I do have regarding $post_id. the way it's working is that it is looking for a post that already has that ID right? since i want to add the image to WP, can I reverse it to if ( $post instanceof WP_Post ){"not allowed since there's already one with that id!"}?
    – Demonipo
    Commented Jul 29, 2022 at 14:11
  • the goal there is to make sure the post actually exists, after all you can't update the meta of a post that does not exist, e.g. on a site with 5 posts would it make sense to say upload this photo, and the post ID is 1967845? Inverting it wouldn't make sense as you're not creating a post. Remember update_post_meta and get_post_meta need the ID of the post you're updating/getting the meta from, that's why $post_id is there and why it's checking if the post exists or not
    – Tom J Nowell
    Commented Jul 29, 2022 at 15:58
  • while the website was down, i tried the if statement i mentioned before, and I've come across if ( ! is_uploaded_file( $file['tmp_name'] ). since the file doesn't have this when appended, could it be changed to be related to its name? the file object appended has: lastModified: lastModifiedDate: name: size: type: webkitRelativePath: being a curl request, is_uploaded_file is gonna say it is false since it doesn't recognize it as a HTTP Post request right? the file object isn't recognized with $_FILES so I'm doing $request['profile_picture']
    – Demonipo
    Commented Jul 29, 2022 at 15:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.