'https://...');
*/
function med_diag_api_url() {
$url = get_option( 'med_diag_api_url', 'https://med.my-dpr.ru' );
return apply_filters( 'med_diag_api_url', untrailingslashit( $url ) );
}
/**
* Внутренний URL API-бэкенда (для проксирования через PHP).
* По умолчанию — внутренний IP сервера, где стоит и WordPress, и API.
* Можно переопределить фильтром: add_filter('med_diag_internal_url', fn() => 'http://10.0.0.5:18018');
*/
function med_diag_internal_url() {
$url = get_option( 'med_diag_internal_url', 'http://127.0.0.1:18018' );
return apply_filters( 'med_diag_internal_url', untrailingslashit( $url ) );
}
/**
* Режим подключения к бэкенду:
* 'proxy' — браузер шлёт запрос на этот же WordPress, PHP проксирует на внутренний IP (по умолчанию);
* 'direct' — браузер шлёт запрос напрямую на публичный URL API (нужен CORS на бэкенде).
*/
function med_diag_mode() {
$mode = get_option( 'med_diag_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;
}
/* ─── Страница согласия на сканирование ───────────────────── */
// Слаг страницы с текстом согласия (только латиница).
define( 'MED_DIAG_CONSENT_SLUG', 'consent-to-scan' );
/**
* Содержимое страницы согласия (из официального текста).
*/
function med_diag_consent_content() {
return '
СОГЛАСИЕ НА СКАНИРОВАНИЕ
Нажимая кнопку «Начать измерение» на устройстве «Фотоплетизмографическое сканирование здоровья «Спектр»» (далее – «Устройство»), я, дееспособный гражданин Российской Федерации, даю ООО «МЦДЦ» (адрес: 121205, город Москва, тер Инновационного Центра Сколково, Большой б-р, д. 30 стр. 1, эт. 1 помещ. 247, ОГРН 1217700384857, ИНН 9731082407) согласие на разовое сканирование моих физиологических показателей.
Я уведомлен о том, что данные, полученные в результате сканирования с помощью Устройства, используются исключительно для демонстрации результата сканирования на экране Устройства, не сохраняются и не используются для идентификации моей личности.
Я уведомлен о том, что сведения, отображаемые на экране Устройства, не могут быть использованы для профилактики, диагностики и лечения заболеваний, и о необходимости обращения к врачу при обнаружении у меня каких-либо симптомов заболеваний.
';
}
/**
* URL страницы согласия (создаётся при активации плагина).
*/
function med_diag_consent_url() {
$page = get_page_by_path( MED_DIAG_CONSENT_SLUG );
if ( $page ) {
return get_permalink( $page );
}
return home_url( '/' . MED_DIAG_CONSENT_SLUG );
}
/**
* При активации плагина создаём страницу согласия, если её ещё нет.
*/
register_activation_hook( __FILE__, 'med_diag_activate' );
function med_diag_activate() {
$existing = get_page_by_path( MED_DIAG_CONSENT_SLUG );
if ( $existing ) {
return;
}
wp_insert_post( array(
'post_type' => 'page',
'post_status' => 'publish',
'post_title' => 'Согласие на сканирование',
'post_name' => MED_DIAG_CONSENT_SLUG,
'post_content' => med_diag_consent_content(),
) );
}
/**
* Ошибка в формате 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' );
function med_diag_register_assets() {
wp_register_style(
'med-diag-css',
MED_DIAG_PLUGIN_URL . 'assets/css/med-diag.css',
array(),
MED_DIAG_VERSION
);
wp_register_script(
'med-diag-js',
MED_DIAG_PLUGIN_URL . 'assets/js/med-diag.js',
array(),
MED_DIAG_VERSION,
true // в футере
);
wp_localize_script( 'med-diag-js', 'MED_DIAG', array(
'apiUrl' => med_diag_api_url(),
'proxyUrl' => rest_url( 'med-diag/v1' ),
'mode' => med_diag_mode(),
) );
}
/* ─── Прокси на внутренний IP (REST) ──────────────────────── */
add_action( 'rest_api_init', 'med_diag_register_routes' );
function med_diag_register_routes() {
// Анализ: браузер → этот WordPress → внутренний IP бэкенда
register_rest_route( 'med-diag/v1', '/analyze', array(
'methods' => 'POST',
'permission_callback' => '__return_true',
'callback' => 'med_diag_proxy_analyze',
) );
// Скачивания (PDF/DOCX/видео): /download//
register_rest_route( 'med-diag/v1', '/download/(?P[^/]+)/(?P[a-z]+)', array(
'methods' => 'GET',
'permission_callback' => '__return_true',
'callback' => 'med_diag_proxy_download',
) );
}
function med_diag_proxy_analyze( $request ) {
$files = $request->get_file_params();
if ( empty( $files['video'] ) ) {
// Если тело запроса превысило 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'];
$error = isset( $file['error'] ) ? (int) $file['error'] : UPLOAD_ERR_NO_FILE;
if ( UPLOAD_ERR_INI_SIZE === $error || UPLOAD_ERR_FORM_SIZE === $error ) {
return med_diag_json_error( 'Видео слишком большое (максимум 100 МБ)', 413 );
}
if ( UPLOAD_ERR_OK !== $error ) {
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 );
$body = '--' . $boundary . "\r\n";
$body .= 'Content-Disposition: form-data; name="video"; filename="' . $filename . '"' . "\r\n";
$body .= 'Content-Type: ' . ( ! empty( $file['type'] ) ? $file['type'] : 'application/octet-stream' ) . "\r\n\r\n";
$body .= $content . "\r\n";
$body .= '--' . $boundary . "--\r\n";
$response = wp_remote_post( med_diag_internal_url() . '/analyze', array(
'timeout' => 300,
'headers' => array( 'Content-Type' => 'multipart/form-data; boundary=' . $boundary ),
'body' => $body,
) );
if ( is_wp_error( $response ) ) {
return med_diag_json_error( 'Бэкенд недоступен: ' . $response->get_error_message(), 502 );
}
$status = (int) wp_remote_retrieve_response_code( $response );
$data = json_decode( wp_remote_retrieve_body( $response ), true );
// Бэкенд (или 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 );
}
function med_diag_proxy_download( $request ) {
$raw_id = (string) $request->get_param( 'id' );
$id = rawurlencode( $raw_id );
$type = $request->get_param( 'type' );
$response = wp_remote_get( med_diag_internal_url() . '/download/' . $id . '/' . $type, array(
'timeout' => 300,
) );
if ( is_wp_error( $response ) ) {
return med_diag_json_error( 'Бэкенд недоступен: ' . $response->get_error_message(), 502 );
}
$status = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $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 );
$content_type = wp_remote_retrieve_header( $response, 'content-type' );
$disposition = wp_remote_retrieve_header( $response, 'content-disposition' );
// Имя файла берём из ответа бэкенда: запись сканирования может быть .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 ? $content_type : 'application/octet-stream' ) );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Content-Length: ' . strlen( $body ) );
echo $body;
exit;
}
/* ─── Шорткод ─────────────────────────────────────────────── */
add_shortcode( 'med_diagnostics', 'med_diag_shortcode' );
function med_diag_shortcode( $atts ) {
$atts = shortcode_atts(
array(
'title' => '🩺 Диагностика по видео',
),
$atts,
'med_diagnostics'
);
wp_enqueue_style( 'med-diag-css' );
wp_enqueue_script( 'med-diag-js' );
ob_start();
?>
Получите оценку состояния здоровья
📷 Сканирование
Наведите камеру на лицо — мы запишем видео и проведём анализ