wp_handle_upload возвращает ответ с критической ошибкой или недействительную отправку формы

Вопрос или проблема

Я пытаюсь создать плагин, который сможет загружать файлы прямо в WP. Несмотря на то, что я копировал различные блоки кода из Интернета, он, похоже, упорно не дает мне это сделать.

Способ, которым я пытаюсь это сделать, заключается в использовании XMLHttpRequest к php файлу с wp_handle_upload. Я прикрепляю элемент ввода файла к formdata и отправляю его через запрос. Данные, которые отправляются, правильные и в порядке, так как я неоднократно выводил их, чтобы убедиться в этом, но когда они доходят до wp_handle_upload, выполнение прекращается, и вкладка сети показывает, что произошла ошибка 500.

Я пробовал вызывать wp_handle_upload только с первым параметром, но он говорит, что отправка формы недействительна. Я также парсил данные файла в новую переменную, как это сделано здесь, но результат остался тем же.

javascript код, который обрабатывает элемент ввода файла:

if ($('#active_p').is(":checked")) personDataActive = 1; // так как checked не является логическим свойством, нам нужно проанализировать его, чтобы получить правильное значение
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());
})

// в зависимости от значения id, он либо обновит, либо вставит человека
if ($('#id_p').val() > 0) id = $('#id_p').val(); // это новый человек
else id = 0; // это уже существующий человек           

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)}
});

// передаем все данные как массив, чтобы сделать ajax данные более чистыми. 0 — это id, 1 — имя, 2 — активен, 3 — дата рождения, 4 — внутренний id, 5 — медиа-url, 6 — пользователи и 7 — места

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

// ajax вызов для добавления нового человека!!!
$.ajax({
    url: 'ajaxToPhpCalls.php', // файл, в который следует отправить данные. желательно оставить его в той же папке, чтобы избежать проблем
    type: 'POST', // это всегда может быть POST, даже если намерение заключается в получении значений, все-таки лучше указывать предполагаемый тип, чтобы случайно не перезаписать данные
    data: { callFunction : "Insert", personData: personData.join(',')}, // мы не можем отправить массив напрямую, нам нужно использовать функцию .join(), чтобы отправить его целиком
    success: function(response) {
        // ответ может быть использован для отладки, так как вернет те же данные, что и в Postman
        //alert("Success");
        console.log(response);

        // скрыть модальное окно при успешном выполнении, чтобы пользователь получил визуальный сигнал. если неудача, они могут повторно отправить запрос
        hideCreateModal();

        var total = $("#totalNumber").val();
        $("#totalNumber").val(total++); // добавляем нового человека к общему количеству. val()++ не работает вне, может работать внутри ()
        //console.log($("#totalNumber").val());
        $("body").css("cursor", "default");
        resetNewPersonFields();
        ResetTable();
        $("body").css("cursor", "default");
    },
    error: function() {alert("Error"); $("body").css("cursor", "default");},
});

imageUpload.php для загрузки медиа в WP

<?php

// загружаем оба, чтобы убедиться, что функции ядра wordpress загружены
require( dirname(__FILE__) . '/../../../../../wp-config.php' );
require( dirname(__FILE__) . '/../../../../../wp-load.php' );

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

// проверяем, есть ли в запросе требуемая переменная
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";

html для ввода файла

<div id="Imagens" class="tabcontent" style="display: none;">
    <div class="form-group">
        <label>Изображение</label><br>                                
        <input type="file" id="profile_picture" name="profile_picture"><br>
        <label>или</label><br>
        <a href="#" id="insertImageAnchor" class="button">Добавить изображение</a><br>
        <img id="preview" src="https://cdn-icons-png.flaticon.com/512/983/983213.png" width=350px>
        </form>
    </div>
</div>

Примечание: В настоящее время не протестировано, но я публикую это здесь по мере написания

PHP может находиться в плагине или в functions.php вашей темы и даст вам конечную точку по адресу yoursite.com/wp-json/demonipo/v1/profilephoto, к которой вы можете выполнять POST запросы через AJAX. Обратите внимание, что предполагается, что только авторизованные пользователи смогут это использовать. Если это не так, измените is_user_logged_in на __return_true. Не забудьте обновить постоянные ссылки перед тем, как попробовать использовать это:

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', // функция, которая будет выполнена при вызове
        'permission_callback' => 'is_user_logged_in', // только для авторизованных пользователей
    ] );
}
/**
 * Обработка загрузки профиля
 *
 * @param \WP_REST_Request $request AJAX-запрос со всеми полями, которые WP получил
 * @return mixed возвращает WP_Error, если что-то пошло не так, иначе возвращает
 *               массив с информацией о загруженном файле
 */
function demonipo_rest_profilephoto( $request ) {
    // Получаем все параметры, файл и пост, к которому нужно его прикрепить

    // Получаем ID поста и проверяем, что он действителен
    if ( empty( $request['postid'] ) ) {
        return new WP_Error( 'invalid', 'Вы должны указать, какой пост будет обновлен' );
    }

    $post_id = $request['postid'];

    // это действительно пост?
    $post = get_post( $post_id );
    if ( ! $post instanceof WP_Post ) {
        return new WP_Error( 'invalid', 'Пост с указанным ID не найден' );
    }

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

    if ( empty( $files['profile_picture'] ) ) {
        return new WP_Error( 'invalid', 'Профильное фото не найдено!' );
    }

    $file = $files['profile_picture'];

    // Проверьте, что загрузка прошла успешно и действительна:

    // подтверждаем, что файл загружен через POST
    if ( ! is_uploaded_file( $file['tmp_name'] ) ) {
        return new WP_Error( 'error', 'Проверка загруженного файла не прошла!' );
    }

    // подтверждаем, что нет ошибок с файлом
    if (! $file['error'] === UPLOAD_ERR_OK ) {
        return new WP_Error( 'error', 'Ошибка загрузки!' . $file['error'] );
    }

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

    // если добавление не удалось, верните ошибку, чтобы мы могли понять, что произошло:
    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',
    ];

    // Все отлично! Обновляем метаданные поста
    $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 rest_ensure_response( $new_data );

}

Важные примечания:

  • Я заметил, что в PHP использовался $post_id, но он нигде не был определен, поэтому не было способа узнать, какой пост нужно обновить. Убедитесь, что вы включили поле post_id в ваш форм-дата.
  • wp_handle_upload перемещает файл в нужное место, но не создаёт вложения, поэтому я заменил его на media_handle_upload. Это недостающая часть, которая нужна для отображения в медиабиблиотеке. Я также установил, чтобы это стало вложением к посту, который вы передаете.
  • Эта статья была очень информативной при написании этого: https://firxworx.com/blog/wordpress/adding-an-endpoint-to-wordpress-rest-api-for-file-uploads/, вы можете узнать некоторые части проверки ошибок.
  • много дополнительной проверки ошибок, она никогда не должна срабатывать, но если это произойдет, REST API вернет это вашему javascript и jQuery выдаст ошибку, которую вы сможете прочитать.
  • Сначала я не знал, как получить mimetype, поэтому просто закодировал его как image/jpeg. Я также сказал, чтобы он возвращал изображение в полном размере, но WP также создаст миниатюры и т.д.
  • Если возможно, постарайтесь сохранить ID вложения, а не имя файла/URL, это упростит миграции.
  • Эта статья/гист также была информативной, хотя я бы рекомендовал использовать PHP-базированный блок, а не шорткод в наши дни: https://gist.github.com/ahmadawais/0ccb8a32ea795ffac4adfae84797c19a

Этот последний гист содержит JS код, который близок к тому, что вам нужно:

// Проверьте, если файл выбран.
if ( 'undefined' === typeof( jQuery( '#profile_picture' )[0].files[0] ) ) {
    alert( 'Выберите файл!' );
    return;
}

// Получаем файл из ввода.
var file = jQuery( '#profile_picture' )[0].files[0];
var formData = new FormData();
formData.append( 'file', file );

// TODO: Добавьте ID поста в ваш HTML где-то
var post_id = jQuery( '#post_id' )
formData.append( 'post_id', post_id );

// Отправляем запрос.
jQuery.ajax( {
    url: '/wp-json/demonipo/v1/profilephoto',
    method: 'POST',
    processData: false,
    contentType: false,
    data: formData
} ).success( function ( response ) {
    console.log( 'успех!' );
    console.log( response );
} ).error( function( response ) {
    console.log( 'ошибка' );
    console.log( response );
});

Вам нужно будет добавить скрытый ввод с ID поста, названный post_id, с таким же ID и именем рядом с этим вводом файла, чтобы это работало.

например.

<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>

Есть одна вещь, которую я упустил, которую, я думаю, не нужно, это REST API nonce. Обычно для аутентифицированных запросов вам нужно передать nonce, что не сложно, но я не думаю, что вам это нужно. Если да, то API просто сообщит вам, что запрос запрещен, если это так, дайте знать. Как это сделать, описано в некоторых из ссылок, которые я добавил, так что это не слишком много работы.

Я также использовал jQuery вместо $, так как jQuery, который поставляется с WordPress, по умолчанию использует режим без конфликтов, но вы можете изменить это.


В качестве общего руководства, вы можете взять случайный PHP файл, как этот:

<?php
// загружаем оба, чтобы убедиться, что функции ядра wordpress загружены
require( dirname(__FILE__) . '/../../../../../wp-config.php' );
require( dirname(__FILE__) . '/../../../../../wp-load.php' );

echo "Привет " . $_GET['name'];

И преобразовать его в красивую URL конечную точку REST API так:

<?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', // функция, которая будет выполнена при вызове
        'permission_callback' => '__return_true',
    ] );
}

function demonipo_randomphp( $request ) {
    return 'Привет' . $request['name'];
}

Ответ или решение

При создании плагина для загрузки файлов в WordPress через wp_handle_upload, вы столкнулись с проблемами, которые могут быть вызваны несколькими факторами. Давайте разберемся, как правильно обрабатывать загрузку файлов и избегать ошибок, таких как "500 Internal Server Error" и "Недопустимая форма отправки".

Анализ и решение проблемы

  1. Проверка файлового ввода: Убедитесь, что файл действительно загружается. В вашем случае, вы используете $_FILES['image'], однако при отправке AJAX-запроса вы передаете его как profile_picture. Соответственно, вам нужно использовать $_FILES['profile_picture'] в вашем PHP-коде.

    if (!empty($_FILES["profile_picture"])) {
  2. Параметры загрузки: В массиве $upload_overrides, в параметре 'test_form' вы указываете значение FALSE. Это правильный подход, чтобы обойти проверку формы. Однако также рекомендуется убедиться, что все необходимые параметры передаются в wp_handle_upload.

  3. Проверка ошибок загрузки: Важно добавить обработку ошибок, чтобы выяснить, что именно идет не так. Используйте error_log или wp_die, чтобы вывести код ошибки.

    if (isset($movefile['error'])) {
        error_log('Upload error: ' . $movefile['error']);
        wp_die($movefile['error']);
    }
  4. Использование media_handle_upload вместо wp_handle_upload: Чтобы файл не только загружался, но и регистрировался как вложение в медиа-библиотеку, стоит использовать media_handle_upload.

    $att_id = media_handle_upload('profile_picture', $post_id);
    
    if (is_wp_error($att_id)) {
        error_log('Media upload error: ' . $att_id->get_error_message());
        wp_die($att_id);
    }
  5. Проверка ID записи (post_id): Убедитесь, что post_id передается в AJAX-запросе и корректно извлекается в PHP. Если post_id не установлен, это может вызвать ошибки.

Пример исправленного кода для PHP

require( dirname(__FILE__) . '/../../../../../wp-config.php' );
require( dirname(__FILE__) . '/../../../../../wp-load.php' );
require_once( ABSPATH . 'wp-admin/includes/file.php' );

if (!empty($_FILES["profile_picture"])) {
    $upload_overrides = array('test_form' => false);

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

    if (is_wp_error($att_id)) {
        error_log('Media upload error: ' . $att_id->get_error_message());
        wp_die($att_id);
    }

    // Файл успешно загружен
    // Можете вернуть информацию о файле, если это необходимо
    wp_send_json_success($att_id);
} else {
    wp_send_json_error('No files detected');
}

Рекомендации по JavaScript

В вашем JavaScript-коде убедитесь, что вы правильно передаете нужные данные, включая post_id:

var post_id = $('#post_id').val();
formData.append('post_id', post_id);

Также проверьте, что загружаемый файл является допустимым:

if (!$('#profile_picture').prop('files').length) {
    alert('Select a file!');
    return;
}

Заключение

Следуя указаниям выше и реализовав предложенные изменения, вы сможете устранить основные проблемы с загрузкой файлов в WordPress с использованием wp_handle_upload. Кроме того, не забывайте использовать инструменты отладки и логи для анализа возникающих ошибок, что поможет вам быстрее находить и исправлять проблемы. Удачи в разработке вашего плагина!

Оцените материал
Добавить комментарий

Капча загружается...