Версия 1.2.2: JSON-ошибки как на бэкенде, лимиты 100 МБ, овал-подсказка, безопасный парсинг ответов
- med_diag_json_error() — ошибки в формате {'error': '...'} (WP_REST_Response)
- 4 проверки лимита 100 МБ (CONTENT_LENGTH, UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE, size)
- Проверка расширения файла (mp4/avi/mov/mkv/webm/m4v)
- Имя файла только ASCII (med-diag-upload.ext) — защита multipart
- filename скачивания из Content-Disposition бэкенда
- Овал .med-scan-oval (inset 12%, затемнение вне зоны)
- escapeHtml() + parseJson() — безопасная обработка ответов (XSS, HTML-ошибки)
- README: nginx лимиты, таблица ошибок
This commit is contained in:
1 parent
867fe7bc66
commit
90d9576425
4 files changed
+179
-39
No files matched your search
@@ -72,6 +72,31 @@ max_execution_time = 300
|
|||||||
|
|
||||||
Проверить текущие значения: `php -i | grep -E "upload_max_filesize|post_max_size|max_execution_time"`.
|
Проверить текущие значения: `php -i | grep -E "upload_max_filesize|post_max_size|max_execution_time"`.
|
||||||
|
|
||||||
|
Если WordPress стоит за nginx, поднимите лимит и там — иначе nginx вернёт HTML-страницу `413 Request Entity Too Large`:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
client_max_body_size 100M;
|
||||||
|
client_body_timeout 300s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Обработка ошибок и лимитов
|
||||||
|
|
||||||
|
Серверная часть и плагин отдают ошибки **всегда в JSON** (`{"error": "..."}`), поэтому фронтенд никогда не получает HTML-страницу
|
||||||
|
вместо данных и не падает с `Unexpected token '<'` (аналог обработчиков `413/404/500` во Flask-бэкенде):
|
||||||
|
|
||||||
|
| Ситуация | Ответ | Сообщение |
|
||||||
|
|---|---|---|
|
||||||
|
| Файл больше 100 МБ (в т.ч. тело отброшено из-за `post_max_size`, `UPLOAD_ERR_INI_SIZE`) | `413` | «Видео слишком большое (максимум 100 МБ)» |
|
||||||
|
| Файл не передан | `400` | «Видео не загружено» |
|
||||||
|
| Формат не из списка `mp4/avi/mov/mkv/webm/m4v` | `400` | «Неподдерживаемый формат…» |
|
||||||
|
| Бэкенд недоступен | `502` | «Бэкенд недоступен: …» |
|
||||||
|
| Бэкенд вернул не JSON (HTML-страница ошибки nginx/Flask) | `502` | «Бэкенд вернул некорректный ответ (HTTP …)» |
|
||||||
|
| Ошибка анализа на бэкенде | статус бэкенда | текст бэкенда + `video_url` (ссылка «Скачать записанное видео») |
|
||||||
|
|
||||||
|
На стороне браузера ответ разбирается безопасно: если вместо JSON пришёл HTML, из него вырезаются теги и показывается короткое
|
||||||
|
текстовое сообщение (и оно экранируется перед вставкой в страницу).
|
||||||
|
|
||||||
## Требования к бэкенду (Flask)
|
## Требования к бэкенду (Flask)
|
||||||
|
|
||||||
В `app.py` серверного репозитория уже добавлена поддержка CORS (нужна только в режиме «Напрямую»):
|
В `app.py` серверного репозитория уже добавлена поддержка CORS (нужна только в режиме «Напрямую»):
|
||||||
@@ -79,6 +104,10 @@ max_execution_time = 300
|
|||||||
- `Access-Control-Allow-Origin` — по умолчанию `*`, можно ограничить через env `CORS_ORIGINS` (список через запятую, например `https://site1.ru,https://site2.ru`).
|
- `Access-Control-Allow-Origin` — по умолчанию `*`, можно ограничить через env `CORS_ORIGINS` (список через запятую, например `https://site1.ru,https://site2.ru`).
|
||||||
- Preflight `OPTIONS /analyze` возвращает `204`.
|
- Preflight `OPTIONS /analyze` возвращает `204`.
|
||||||
|
|
||||||
|
Также во Flask добавлены обработчики ошибок `413/404/500`, всегда возвращающие JSON, и `try/except` вокруг сохранения видео —
|
||||||
|
поэтому и `POST /analyze`, и `/download/...` отдают ошибки в виде `{"error": "..."}`. Плагин ожидает такой же формат
|
||||||
|
(см. «Обработка ошибок и лимитов»), а на не-JSON (HTML) ответы реагирует безопасно.
|
||||||
|
|
||||||
## Локальное тестирование
|
## Локальное тестирование
|
||||||
|
|
||||||
1. Поднимите API-бэкенд локально (см. README серверного репозитория) или используйте прод `https://med.my-dpr.ru`.
|
1. Поднимите API-бэкенд локально (см. README серверного репозитория) или используйте прод `https://med.my-dpr.ru`.
|
||||||
|
|||||||
+17
-2
@@ -161,8 +161,12 @@
|
|||||||
.med-scan-modal.open { display: flex; }
|
.med-scan-modal.open { display: flex; }
|
||||||
|
|
||||||
.med-scan-video-wrap {
|
.med-scan-video-wrap {
|
||||||
width: 280px;
|
/* width: 280px; */
|
||||||
height: 280px;
|
/* height: 280px; */
|
||||||
|
width: 80vw;
|
||||||
|
height: 80vw;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 500px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 4px solid #0014FC;
|
border: 4px solid #0014FC;
|
||||||
@@ -178,6 +182,17 @@
|
|||||||
transform: scaleX(-1);
|
transform: scaleX(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Овал — безопасная зона: лицо должно помещаться в него целиком */
|
||||||
|
.med-scan-oval {
|
||||||
|
position: absolute;
|
||||||
|
inset: 12%;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.4);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.med-scan-progress { color: #fff; margin-top: 20px; font-size: 16px; text-align: center; }
|
.med-scan-progress { color: #fff; margin-top: 20px; font-size: 16px; text-align: center; }
|
||||||
|
|
||||||
.med-scan-progress-bar {
|
.med-scan-progress-bar {
|
||||||
|
|||||||
+36
-11
@@ -20,6 +20,29 @@
|
|||||||
return USE_PROXY ? PROXY_URL + path : API_URL + path;
|
return USE_PROXY ? PROXY_URL + path : API_URL + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Экранирование текста перед вставкой в innerHTML: сообщение об ошибке
|
||||||
|
// может прийти из ответа сервера (HTML-страница nginx/PHP и т.п.).
|
||||||
|
function escapeHtml(text) {
|
||||||
|
return String(text === undefined || text === null ? '' : text)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Безопасный разбор ответа: если сервер вернул не JSON (например,
|
||||||
|
// HTML-страницу ошибки), не падаем с "Unexpected token '<'".
|
||||||
|
async function parseJson(response) {
|
||||||
|
var ct = (response.headers.get('content-type') || '').toLowerCase();
|
||||||
|
if (ct.indexOf('application/json') !== -1) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
// HTML-ответ превращаем в короткое текстовое сообщение
|
||||||
|
var text = (await response.text()).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
|
if (text.length > 300) text = text.slice(0, 300) + '…';
|
||||||
|
return { error: text || ('Ошибка сервера (HTTP ' + response.status + ')') };
|
||||||
|
}
|
||||||
|
|
||||||
var uploadArea = document.getElementById('uploadArea');
|
var uploadArea = document.getElementById('uploadArea');
|
||||||
var fileInput = document.getElementById('fileInput');
|
var fileInput = document.getElementById('fileInput');
|
||||||
var analyzeBtn = document.getElementById('analyzeBtn');
|
var analyzeBtn = document.getElementById('analyzeBtn');
|
||||||
@@ -85,14 +108,14 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
var err = await response.json();
|
var err = await parseJson(response);
|
||||||
var e = new Error(err.error || 'Ошибка анализа');
|
var e = new Error(err.error || 'Ошибка анализа');
|
||||||
e.videoUrl = err.video_url;
|
e.videoUrl = err.video_url;
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
setProgress(90, 'Формирование результата...');
|
setProgress(90, 'Формирование результата...');
|
||||||
var data = await response.json();
|
var data = await parseJson(response);
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
showError(data.error);
|
showError(data.error);
|
||||||
@@ -130,7 +153,7 @@
|
|||||||
function showError(msg, videoUrl) {
|
function showError(msg, videoUrl) {
|
||||||
progressContainer.style.display = 'none';
|
progressContainer.style.display = 'none';
|
||||||
resultCard.style.display = 'block';
|
resultCard.style.display = 'block';
|
||||||
var html = '<div class="med-error-box"><strong>Ошибка:</strong> ' + msg + '</div>';
|
var html = '<div class="med-error-box"><strong>Ошибка:</strong> ' + escapeHtml(msg) + '</div>';
|
||||||
if (videoUrl) {
|
if (videoUrl) {
|
||||||
html += '<div style="text-align:center; margin-top:15px">' +
|
html += '<div style="text-align:center; margin-top:15px">' +
|
||||||
'<a class="med-btn-download" href="' + downloadUrl(videoUrl) + '">🎬 Скачать записанное видео</a>' +
|
'<a class="med-btn-download" href="' + downloadUrl(videoUrl) + '">🎬 Скачать записанное видео</a>' +
|
||||||
@@ -257,7 +280,7 @@
|
|||||||
video: {
|
video: {
|
||||||
facingMode: 'user',
|
facingMode: 'user',
|
||||||
width: { ideal: 1280 },
|
width: { ideal: 1280 },
|
||||||
height: { ideal: 720 }
|
height: { ideal: 1280 }
|
||||||
},
|
},
|
||||||
audio: false
|
audio: false
|
||||||
});
|
});
|
||||||
@@ -277,11 +300,11 @@
|
|||||||
scanLastFrameTime = 0;
|
scanLastFrameTime = 0;
|
||||||
updateScanProgress(0);
|
updateScanProgress(0);
|
||||||
|
|
||||||
// Canvas 720×720 — квадрат, лицо целиком влезает в portrait и landscape.
|
// Canvas 1080×1080 — квадрат, лицо целиком влезает в portrait и landscape.
|
||||||
// Серверное extract_frames масштабирует до 1280×720 для SDK.
|
// Серверное extract_frames масштабирует до 1280×720 для SDK.
|
||||||
scanCanvas = document.createElement('canvas');
|
scanCanvas = document.createElement('canvas');
|
||||||
scanCanvas.width = 720;
|
scanCanvas.width = 1080;
|
||||||
scanCanvas.height = 720;
|
scanCanvas.height = 1080;
|
||||||
scanCtx = scanCanvas.getContext('2d');
|
scanCtx = scanCanvas.getContext('2d');
|
||||||
|
|
||||||
var captureStream;
|
var captureStream;
|
||||||
@@ -320,16 +343,18 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ts - scanLastFrameTime >= FRAME_INTERVAL) {
|
if (ts - scanLastFrameTime >= FRAME_INTERVAL) {
|
||||||
// Квадратный center-crop: берём центральную квадратную область из видео.
|
// Квадратный center-crop с запасом 10%: берём чуть меньший
|
||||||
|
// квадрат, чтобы голова гарантированно помещалась целиком
|
||||||
|
// (иначе SDK не находит лицо из-за обрезанной головы).
|
||||||
var vw = scanVideo.videoWidth;
|
var vw = scanVideo.videoWidth;
|
||||||
var vh = scanVideo.videoHeight;
|
var vh = scanVideo.videoHeight;
|
||||||
if (vw && vh) {
|
if (vw && vh) {
|
||||||
var side = Math.min(vw, vh);
|
var side = Math.min(vw, vh) * 0.9;
|
||||||
var sx = (vw - side) / 2;
|
var sx = (vw - side) / 2;
|
||||||
var sy = (vh - side) / 2;
|
var sy = (vh - side) / 2;
|
||||||
scanCtx.drawImage(scanVideo, sx, sy, side, side, 0, 0, 720, 720);
|
scanCtx.drawImage(scanVideo, sx, sy, side, side, 0, 0, 1080, 1080);
|
||||||
} else {
|
} else {
|
||||||
scanCtx.drawImage(scanVideo, 0, 0, 720, 720);
|
scanCtx.drawImage(scanVideo, 0, 0, 1080, 1080);
|
||||||
}
|
}
|
||||||
scanFrameCount++;
|
scanFrameCount++;
|
||||||
scanLastFrameTime = ts;
|
scanLastFrameTime = ts;
|
||||||
|
|||||||
+97
-26
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* Plugin Name: Med Diagnostics
|
* Plugin Name: Med Diagnostics
|
||||||
* Description: Интеграция сервиса «Диагностика по видео» (med.my-dpr.ru) в WordPress. Выводит вёрстку сервиса через шорткод [med_diagnostics].
|
* Description: Интеграция сервиса «Диагностика по видео» (med.my-dpr.ru) в WordPress. Выводит вёрстку сервиса через шорткод [med_diagnostics].
|
||||||
* Version: 1.1.0
|
* Version: 1.2.0
|
||||||
* Author: apuc
|
* Author: apuc
|
||||||
* License: GPL-2.0-or-later
|
* License: GPL-2.0-or-later
|
||||||
* Text Domain: med-diagnostics
|
* Text Domain: med-diagnostics
|
||||||
@@ -18,7 +18,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
exit; // Прямой доступ запрещён
|
exit; // Прямой доступ запрещён
|
||||||
}
|
}
|
||||||
|
|
||||||
define( 'MED_DIAG_VERSION', '1.1.0' );
|
define( 'MED_DIAG_VERSION', '1.2.2' );
|
||||||
define( 'MED_DIAG_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
define( 'MED_DIAG_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,6 +51,22 @@ function med_diag_mode() {
|
|||||||
return apply_filters( 'med_diag_mode', in_array( $mode, array( 'proxy', 'direct' ), true ) ? $mode : 'proxy' );
|
return apply_filters( 'med_diag_mode', in_array( $mode, array( 'proxy', 'direct' ), true ) ? $mode : 'proxy' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Максимальный размер видео — как на бэкенде (Flask: MAX_CONTENT_LENGTH = 100 МБ).
|
||||||
|
*/
|
||||||
|
function med_diag_max_upload_bytes() {
|
||||||
|
return 100 * 1024 * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ошибка в формате JSON: {"error": "..."} — как во Flask-бэкенде.
|
||||||
|
* Фронтенд читает именно поле error (и video_url), а WP_Error отдаёт структуру
|
||||||
|
* {code, message, data} — из-за этого текст ошибки не доходил до пользователя.
|
||||||
|
*/
|
||||||
|
function med_diag_json_error( $message, $status = 500, $extra = array() ) {
|
||||||
|
return new WP_REST_Response( array_merge( array( 'error' => $message ), $extra ), $status );
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Регистрация ассетов ─────────────────────────────────── */
|
/* ─── Регистрация ассетов ─────────────────────────────────── */
|
||||||
|
|
||||||
add_action( 'wp_enqueue_scripts', 'med_diag_register_assets' );
|
add_action( 'wp_enqueue_scripts', 'med_diag_register_assets' );
|
||||||
@@ -98,22 +114,56 @@ function med_diag_register_routes() {
|
|||||||
|
|
||||||
function med_diag_proxy_analyze( $request ) {
|
function med_diag_proxy_analyze( $request ) {
|
||||||
$files = $request->get_file_params();
|
$files = $request->get_file_params();
|
||||||
|
|
||||||
if ( empty( $files['video'] ) ) {
|
if ( empty( $files['video'] ) ) {
|
||||||
return new WP_Error( 'med_diag_no_video', 'Файл video не получен', array( 'status' => 400 ) );
|
// Если тело запроса превысило post_max_size, PHP отбрасывает его целиком,
|
||||||
|
// и $_FILES пуст — сообщаем о превышении размера (413), как это делает
|
||||||
|
// бэкенд в своём обработчике 413, а не «файл не получен».
|
||||||
|
$content_length = isset( $_SERVER['CONTENT_LENGTH'] ) ? (int) $_SERVER['CONTENT_LENGTH'] : 0;
|
||||||
|
if ( $content_length > med_diag_max_upload_bytes() ) {
|
||||||
|
return med_diag_json_error( 'Видео слишком большое (максимум 100 МБ)', 413 );
|
||||||
}
|
}
|
||||||
|
return med_diag_json_error( 'Видео не загружено', 400 );
|
||||||
|
}
|
||||||
|
|
||||||
$file = $files['video'];
|
$file = $files['video'];
|
||||||
if ( $file['error'] !== UPLOAD_ERR_OK ) {
|
$error = isset( $file['error'] ) ? (int) $file['error'] : UPLOAD_ERR_NO_FILE;
|
||||||
return new WP_Error( 'med_diag_upload_error', 'Ошибка загрузки файла (код ' . $file['error'] . ')', array( 'status' => 400 ) );
|
|
||||||
|
if ( UPLOAD_ERR_INI_SIZE === $error || UPLOAD_ERR_FORM_SIZE === $error ) {
|
||||||
|
return med_diag_json_error( 'Видео слишком большое (максимум 100 МБ)', 413 );
|
||||||
}
|
}
|
||||||
if ( $file['size'] > 100 * 1024 * 1024 ) {
|
if ( UPLOAD_ERR_OK !== $error ) {
|
||||||
return new WP_Error( 'med_diag_too_large', 'Видео больше 100 МБ', array( 'status' => 413 ) );
|
return med_diag_json_error( 'Ошибка загрузки файла (код ' . $error . ')', 400 );
|
||||||
|
}
|
||||||
|
if ( isset( $file['size'] ) && (int) $file['size'] > med_diag_max_upload_bytes() ) {
|
||||||
|
return med_diag_json_error( 'Видео слишком большое (максимум 100 МБ)', 413 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем расширение тем же списком, что и бэкенд (иначе анализ
|
||||||
|
// вернёт ошибку уже после загрузки всего файла).
|
||||||
|
$allowed = array( 'mp4', 'avi', 'mov', 'mkv', 'webm', 'm4v' );
|
||||||
|
$ext = strtolower( pathinfo( isset( $file['name'] ) ? $file['name'] : '', PATHINFO_EXTENSION ) );
|
||||||
|
if ( ! in_array( $ext, $allowed, true ) ) {
|
||||||
|
return med_diag_json_error( 'Неподдерживаемый формат. Разрешены: ' . implode( ', ', $allowed ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $file['tmp_name'] ) || ! is_readable( $file['tmp_name'] ) ) {
|
||||||
|
return med_diag_json_error( 'Не удалось прочитать загруженный файл', 500 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Имя файла только из ASCII: в multipart-заголовке не должно быть
|
||||||
|
// неэкранированных кавычек/кириллицы, из-за которых multipart ломается.
|
||||||
|
$filename = 'med-diag-upload.' . $ext;
|
||||||
|
$content = file_get_contents( $file['tmp_name'] );
|
||||||
|
if ( false === $content || '' === $content ) {
|
||||||
|
return med_diag_json_error( 'Не удалось прочитать загруженный файл', 500 );
|
||||||
}
|
}
|
||||||
|
|
||||||
$boundary = wp_generate_password( 24, false );
|
$boundary = wp_generate_password( 24, false );
|
||||||
$body = '--' . $boundary . "\r\n";
|
$body = '--' . $boundary . "\r\n";
|
||||||
$body .= 'Content-Disposition: form-data; name="video"; filename="' . basename( $file['name'] ) . '"' . "\r\n";
|
$body .= 'Content-Disposition: form-data; name="video"; filename="' . $filename . '"' . "\r\n";
|
||||||
$body .= 'Content-Type: ' . $file['type'] . "\r\n\r\n";
|
$body .= 'Content-Type: ' . ( ! empty( $file['type'] ) ? $file['type'] : 'application/octet-stream' ) . "\r\n\r\n";
|
||||||
$body .= file_get_contents( $file['tmp_name'] ) . "\r\n";
|
$body .= $content . "\r\n";
|
||||||
$body .= '--' . $boundary . "--\r\n";
|
$body .= '--' . $boundary . "--\r\n";
|
||||||
|
|
||||||
$response = wp_remote_post( med_diag_internal_url() . '/analyze', array(
|
$response = wp_remote_post( med_diag_internal_url() . '/analyze', array(
|
||||||
@@ -123,42 +173,62 @@ function med_diag_proxy_analyze( $request ) {
|
|||||||
) );
|
) );
|
||||||
|
|
||||||
if ( is_wp_error( $response ) ) {
|
if ( is_wp_error( $response ) ) {
|
||||||
return new WP_Error( 'med_diag_proxy_error', 'Бэкенд недоступен: ' . $response->get_error_message(), array( 'status' => 502 ) );
|
return med_diag_json_error( 'Бэкенд недоступен: ' . $response->get_error_message(), 502 );
|
||||||
}
|
}
|
||||||
|
|
||||||
$status = wp_remote_retrieve_response_code( $response );
|
$status = (int) wp_remote_retrieve_response_code( $response );
|
||||||
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
|
||||||
return new WP_Error( 'med_diag_bad_response', 'Некорректный ответ бэкенда', array( 'status' => 502 ) );
|
// Бэкенд (или nginx) вернул не JSON — например, HTML-страницу ошибки.
|
||||||
|
// Отдаём понятную ошибку в JSON, чтобы фронтенд не падал на response.json()
|
||||||
|
// с «Unexpected token '<'».
|
||||||
|
if ( ! is_array( $data ) ) {
|
||||||
|
return med_diag_json_error( 'Бэкенд вернул некорректный ответ (HTTP ' . $status . ')', 502 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Прокидываем ошибку бэкенда вместе с video_url (ссылка на запись сканирования).
|
||||||
|
if ( $status >= 400 && empty( $data['error'] ) ) {
|
||||||
|
$data['error'] = 'Ошибка анализа (HTTP ' . $status . ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WP_REST_Response( $data, $status );
|
return new WP_REST_Response( $data, $status );
|
||||||
}
|
}
|
||||||
|
|
||||||
function med_diag_proxy_download( $request ) {
|
function med_diag_proxy_download( $request ) {
|
||||||
$id = rawurlencode( $request['id'] );
|
$raw_id = (string) $request->get_param( 'id' );
|
||||||
$type = $request['type'];
|
$id = rawurlencode( $raw_id );
|
||||||
|
$type = $request->get_param( 'type' );
|
||||||
|
|
||||||
$response = wp_remote_get( med_diag_internal_url() . '/download/' . $id . '/' . $type, array(
|
$response = wp_remote_get( med_diag_internal_url() . '/download/' . $id . '/' . $type, array(
|
||||||
'timeout' => 300,
|
'timeout' => 300,
|
||||||
) );
|
) );
|
||||||
|
|
||||||
if ( is_wp_error( $response ) ) {
|
if ( is_wp_error( $response ) ) {
|
||||||
return new WP_Error( 'med_diag_proxy_error', 'Бэкенд недоступен', array( 'status' => 502 ) );
|
return med_diag_json_error( 'Бэкенд недоступен: ' . $response->get_error_message(), 502 );
|
||||||
}
|
}
|
||||||
|
|
||||||
$status = wp_remote_retrieve_response_code( $response );
|
$status = (int) wp_remote_retrieve_response_code( $response );
|
||||||
if ( $status !== 200 ) {
|
if ( 200 !== $status ) {
|
||||||
return new WP_Error( 'med_diag_download_error', 'Ошибка скачивания (HTTP ' . $status . ')', array( 'status' => $status ) );
|
// Бэкенд отдаёт ошибки в JSON — прокидываем текст, если он есть.
|
||||||
|
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||||
|
$msg = ( is_array( $data ) && ! empty( $data['error'] ) )
|
||||||
|
? $data['error']
|
||||||
|
: 'Ошибка скачивания (HTTP ' . $status . ')';
|
||||||
|
return med_diag_json_error( $msg, $status > 0 ? $status : 502 );
|
||||||
}
|
}
|
||||||
|
|
||||||
$body = wp_remote_retrieve_body( $response );
|
$body = wp_remote_retrieve_body( $response );
|
||||||
$headers = wp_remote_retrieve_headers( $response );
|
$content_type = wp_remote_retrieve_header( $response, 'content-type' );
|
||||||
$content_type = isset( $headers['content-type'] ) ? $headers['content-type'] : 'application/octet-stream';
|
$disposition = wp_remote_retrieve_header( $response, 'content-disposition' );
|
||||||
|
|
||||||
$filename = 'diagnostic_' . $request['id'] . '.' . ( 'video' === $type ? 'webm' : $type );
|
// Имя файла берём из ответа бэкенда: запись сканирования может быть .mp4 или .webm.
|
||||||
|
if ( $disposition && preg_match( '/filename\*?=(?:UTF-8\'\')?"?([^";]+)"?/i', $disposition, $matches ) ) {
|
||||||
|
$filename = rawurldecode( trim( $matches[1] ) );
|
||||||
|
} else {
|
||||||
|
$filename = 'diagnostic_' . $raw_id . '.' . ( 'video' === $type ? 'webm' : $type );
|
||||||
|
}
|
||||||
|
|
||||||
header( 'Content-Type: ' . $content_type );
|
header( 'Content-Type: ' . ( $content_type ? $content_type : 'application/octet-stream' ) );
|
||||||
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
|
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
|
||||||
header( 'Content-Length: ' . strlen( $body ) );
|
header( 'Content-Length: ' . strlen( $body ) );
|
||||||
echo $body;
|
echo $body;
|
||||||
@@ -198,7 +268,7 @@ function med_diag_shortcode( $atts ) {
|
|||||||
<div class="med-upload-area" id="uploadArea">
|
<div class="med-upload-area" id="uploadArea">
|
||||||
<div class="med-icon">📹</div>
|
<div class="med-icon">📹</div>
|
||||||
<p><strong>Нажмите или перетащите видео сюда</strong></p>
|
<p><strong>Нажмите или перетащите видео сюда</strong></p>
|
||||||
<p class="med-hint">MP4, AVI, MOV, MKV • до 100 МБ • 10–15 секунд • лицо анфас</p>
|
<p class="med-hint">MP4, AVI, MOV, MKV • до 100 МБ • 10–15 секунд • горизонтальное видео • лицо анфас</p>
|
||||||
<input type="file" id="fileInput" accept="video/*">
|
<input type="file" id="fileInput" accept="video/*">
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:center">
|
<div style="text-align:center">
|
||||||
@@ -226,6 +296,7 @@ function med_diag_shortcode( $atts ) {
|
|||||||
<div class="med-scan-modal" id="scanModal">
|
<div class="med-scan-modal" id="scanModal">
|
||||||
<div class="med-scan-video-wrap">
|
<div class="med-scan-video-wrap">
|
||||||
<video class="med-scan-video" id="scanVideo" autoplay playsinline muted></video>
|
<video class="med-scan-video" id="scanVideo" autoplay playsinline muted></video>
|
||||||
|
<div class="med-scan-oval"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="med-scan-progress">
|
<div class="med-scan-progress">
|
||||||
<div id="scanProgressText">Кадров: 0 / 300 (0%)</div>
|
<div id="scanProgressText">Кадров: 0 / 300 (0%)</div>
|
||||||
@@ -234,7 +305,7 @@ function med_diag_shortcode( $atts ) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="med-scan-cancel" id="scanCancelBtn">✖ Отмена</button>
|
<button class="med-scan-cancel" id="scanCancelBtn">✖ Отмена</button>
|
||||||
<div class="med-scan-hint">Держите лицо в круге, смотрите прямо в камеру, не двигайтесь</div>
|
<div class="med-scan-hint">Поместите лицо в овал целиком: лоб, глаза и подбородок — с запасом. Если голова не помещается — отодвиньте телефон</div>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
return ob_get_clean();
|
return ob_get_clean();
|
||||||
|
|||||||
Reference in new issue
Block a user