javascript - wp_handle_upload returns a critical error response or invalid form submission

admin2025-01-08  7

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=".png" width=350px>
        </form>
    </div>
</div>

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>
Share Improve this question edited Jul 28, 2022 at 13:59 Demonipo asked Jul 26, 2022 at 8:48 DemonipoDemonipo 13 bronze badges 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
 |  Show 15 more comments

1 Answer 1

Reset to default 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'];
}
转载请注明原文地址:http://conceptsofalgorithm.com/Algorithm/1736265898a1078.html

最新回复(0)