Переопределить JSON-кодирование в REST API

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

Я пытаюсь создать плагин для безопасной загрузки.

Я нашел API REST как лучший вариант для этой задачи, но не могу изменить заголовки, такие как Content-Type:. В коллбэке register_rest_route эти заголовки уже установлены.
Если я установлю Content-disposition: attachment, filename=asd.txt, я получаю ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_DISPOSITION в Chrome.

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

Похоже, данные загружаются в формате JSON.

Я устал от “магии” WordPress. Эти задачи были бы такими простыми, если бы использовался чистый PHP, но WordPress только усложняет всё.

Пожалуйста, кто-нибудь скажите мне, как отключить эти магические заголовки *** или порекомендуйте способ для безопасных (PHP) загрузок в моем плагине.

(Нет, я не хочу использовать сторонние плагины. Я пробовал многие из них. Ни один из них пока не сработал… 😀 Весь этот WordPress — это огромный шаг назад после Symfony…)

Вот мой экспериментальный код:

add_action('rest_api_init', function() {
    register_rest_route('secure-downloads/v1', '/download/(?P<filename>.*)', 
    array(
        'methods' => 'GET',
        'callback' => 'secure_downloads_handle_download'
    ));
});
function secure_downloads_handle_download(WP_REST_Request $request)
{
    $filename = urldecode($request->offsetGet('filename'));
    if(strpos($filename, '/..'))
    {
        return new WP_REST_Response('', 403);
    }
    $path = SECURE_DOWNLOADS_UPLOAD_DIR . $filename;
    return new WP_REST_Response(file_get_contents($path), 200, array(
        'Content-Type' => 'application/octet-stream',
        'Content-Transfer-Encoding' => 'Binary',
        'Content-disposition' => 'attachment, filename=\\' . $filename . '\\'
    ));
}

Должно быть достаточно добавить поле Content-Disposition.

Но конкретно это Content-Disposition, а не Content-disposition

Я также добавил бы некоторую проверку вашего параметра filename, который передается в file_get_contents, чтобы убедиться, что он существует и является допустимым. В противном случае вы можете быть уязвимы для атак обхода каталогов или удаленных запросов URL.

Вот что я выяснил:

Ошибка ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_DISPOSITION была вызвана неправильным составлением заголовка Content-Disposition. Вот правильный:

array(
    'Content-Disposition' => 'attachment; filename="' . $filename . '"'
)

Обратите внимание на точку с запятой после attachment, двойные кавычки вокруг имени файла и заглавную D в слове disposition, как указал @Tom J Nowell.

Это решило проблему с заголовками, но WP_REST_Response все равно выводит файлы, как если бы они были закодированы в формате JSON. Поэтому я пошел традиционным путем:

header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Type: application/octet-stream');
readfile($path);
exit;

Существует хук именно для этой ситуации, который позволяет избежать преждевременного завершения выполнения скрипта и отражает обработку WP-запросов методом HEAD.
Разместите что-то подобное этому в конце вашей функции обработчика REST-ответа:

...

add_filter(
    'rest_pre_serve_request',
    function($served, $result, $request, $rest_server) use ($path) {
        if (!$served) { # убедитесь, что запрос еще не был обработан
            $served = true;
            if ('HEAD' !== $request->get_method()) {
                # выводите содержимое вашего ответа здесь, но не возвращайте или не завершайте
                readfile($path);
            }
        }
        return $served;
    },
    1, # измените приоритет по необходимости
    4
);

$result = new WP_REST_Response();
$result->set_headers([
    'Content-Type' => mime_content_type($path),
    'Content-Length' => filesize($path),
    ...
]);
return $result;

.

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

Для решения проблемы с настройкой заголовков в REST API в контексте создания плагина для безопасной загрузки файлов в WordPress, необходимо глубже разобраться в механике работы WordPress и его REST API. Давайте пошагово разберем, что происходит и как можно обойти возникшие сложности.

Теория

1. Проблема заголовков в REST API: Когда вы регистрируете маршрут через register_rest_route и предоставляете колбэк, WordPress обрабатывает возвратное значение вашей функции, рассматривая его как JSON-ответ по умолчанию. Это объясняет, почему ваши файлы загружаются как JSON, даже когда вы пытаетесь установить другие заголовки.

2. Конфликт с Content-Disposition: Ошибка ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_DISPOSITION вызвана некорректной компоновкой заголовка Content-Disposition. Эта ошибка особенно актуальна для браузеров, которые строго проверяют заголовки.

3. Безопасность и валидация данных: Необходимо тщательно проверять входные данные, особенно если это параметр filename, чтобы предотвратить атаки типа обхода каталогов, которые могут привести к несанкционированному доступу к файлам за пределами ожидаемой директории.

Пример и Практическое Применение

Для решения задачи с правильной отправкой заголовков и обеспечения безопасности можно применить следующий подход:

add_action('rest_api_init', function() {
    register_rest_route('secure-downloads/v1', '/download/(?P<filename>.+)', array(
        'methods' => 'GET',
        'callback' => 'secure_downloads_handle_download'
    ));
});

function secure_downloads_handle_download(WP_REST_Request $request)
{
    $filename = urldecode($request->get_param('filename'));

    // Проверка на попытки обхода каталога
    if (strpos($filename, '../') !== false) {
        return new WP_REST_Response('', 403);
    }

    $path = SECURE_DOWNLOADS_UPLOAD_DIR . $filename;

    // Дополнительная проверка существования файла
    if (!file_exists($path)) {
        return new WP_REST_Response('File not found', 404);
    }

    // Установка заголовков перед выключением внешней обработки
    add_filter('rest_pre_serve_request', function($served, $result, $request, $rest_server) use ($path) {
        if (!$served) {
            $served = true;

            // Установка корректных заголовков для скачивания файла
            header('Content-Disposition: attachment; filename="' . basename($path) . '"');
            header('Content-Type: ' . mime_content_type($path));
            header('Content-Length: ' . filesize($path));
            // Вывод файлового содержимого
            readfile($path);
        }
        return $served;
    }, 10, 4);

    // Возвращение пустого ответа, так как обслуживание будет обработано по фильтру
    return new WP_REST_Response();
}

Применение

  1. Управление заголовками: Использование фильтра rest_pre_serve_request позволяет переопределить стандартное поведение Rest API WordPress для прямой отправки файла, сохраняя при этом возможность изначальной конфигурации плагина.

  2. Защита от уязвимостей: Внедрение проверки параметров, таких как filename, предотвращает потенциальные угрозы, связанные с несанкционированным доступом к файлам.

  3. Гибкость и расширяемость: Такой подход обеспечивает больше контроля и гибкости, что позволяет проще адаптировать решение под будущие потребности, включая, например, изменение типа контента в зависимости от расширения файла.

  4. Удобство интеграции: Этот подход сохраняет интеграцию с остальной системой WordPress, позволяет легко добавлять новые функциональные особенности и поддерживать их обновления.

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

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

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