////////////////////////////////////////////////////////////////////////////////
// Fordewind Cert / Cert4U — Модуль УКЭП-подписания (штатный CAdES-BES).
//
// НАЗНАЧЕНИЕ:
//   Локальное подписание документа усиленной квалифицированной подписью (УКЭП)
//   штатными средствами платформы 1С:Предприятие (МенеджерКриптографии,
//   НачатьПодписаниеДанныхCAdES) — БЕЗ COM-объектов CAdESCOM, кросс-платформенно.
//
// ВАЖНО ПРО СБОРКУ:
//   Это НЕ отдельный общий модуль. Содержимое ЭТОГО файла нужно вставить
//   как ОТДЕЛЬНУЮ ОБЛАСТЬ в МОДУЛЬ ФОРМЫ «ФормаОсновная» (сразу после
//   области HTTPТранспорт из Form_MainForm_Module.bsl). Тогда процедуры
//   подписания используют единый HTTP-слой формы (X-Api-Key, таймауты,
//   идемпотентность) и не дублируют транспорт.
//
//   Хардкод адреса/ключа и вызовы несуществующей «ВыполнитьPOSTЗапросAPI»
//   УДАЛЕНЫ. Отправка подписанного документа идёт через общий метод формы
//   ЗагрузитьПодписанныйНаСервере (endpoint api_1c_upload_signed).
//
// ПЛАТФОРМА:
//   НачатьПодписаниеДанныхCAdES доступна с 8.3.21. Требуется расширение работы
//   с криптографией (толстый клиент — ставится вместе с платформой; веб-клиент —
//   «1С:Расширение для работы с криптографией») и установленный КриптоПро CSP.
//
// ФОРМАТ ПОДПИСИ:
//   CAdES-BES. Формат берётся из настройки сервиса «Формат подписи УКЭП»
//   (GET api_1c_settings → sig_attached): открепленная (detached) — .sig без
//   документа; прикреплённая (attached) — документ внутри .sig. Сервер сам
//   определяет формат по подписи; для прикреплённой сверяет вложенный документ
//   с переданным файлом (должны совпадать побайтно). Результат — ДвоичныеДанные подписи
//   (DER, PKCS#7/CAdES). Сервер (api_1c_upload_signed) читает файл .sig и
//   выполняет base64_decode(), поэтому подпись кодируется в Base64Строка()
//   и отправляется как base64-ТЕКСТ. См. УКЭП_ПослеПодписания ниже.
////////////////////////////////////////////////////////////////////////////////

#Область УКЭППодписание

// Точка входа: подписать выбранный документ УКЭП и загрузить на сервер.
// Полностью асинхронный сценарий (совместим с РежимИспользованияМодальности =
// НеИспользовать и с веб-клиентом).
//
&НаКлиенте
Процедура УКЭП_ПодписатьИЗагрузить(Команда)

	Если Не ПроверитьНастройкиПодключенияИнтерактивно() Тогда
		Возврат;
	КонецЕсли;

	Если Не ЗначениеЗаполнено(НазваниеДокумента) Тогда
		ПоказатьПредупреждение(, "Укажите название документа (вкладка «Загрузка»).");
		Возврат;
	КонецЕсли;

	// 1. Асинхронно выбрать файл документа и поместить во временное хранилище.
	ОписаниеОповещения = Новый ОписаниеОповещения("УКЭП_ПослеВыбораФайла", ЭтотОбъект);

	ДиалогВыбора = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.Открытие);
	ДиалогВыбора.Фильтр = "Документы (*.pdf;*.doc;*.docx;*.xlsx)|*.pdf;*.doc;*.docx;*.xlsx|Все файлы (*.*)|*.*";
	ДиалогВыбора.Заголовок = "Выберите документ для подписания УКЭП";
	ДиалогВыбора.МножественныйВыбор = Ложь;

	НачатьПомещениеФайлов(ОписаниеОповещения, , ДиалогВыбора, Истина, УникальныйИдентификатор);

КонецПроцедуры

// Оповещение о завершении помещения файла во временное хранилище.
// ПомещённыеФайлы — Массив из ОписаниеПереданногоФайла.
//
&НаКлиенте
Процедура УКЭП_ПослеВыбораФайла(ПомещённыеФайлы, ДопПараметры) Экспорт

	Если ПомещённыеФайлы = Неопределено ИЛИ ПомещённыеФайлы.Количество() = 0 Тогда
		Возврат; // Пользователь отменил выбор.
	КонецЕсли;

	ОписаниеФайла = ПомещённыеФайлы[0];

	Контекст = Новый Структура;
	Контекст.Вставить("АдресДокумента", ОписаниеФайла.Хранение);
	Контекст.Вставить("ИмяФайла", ИмяФайлаБезПути(ОписаниеФайла.Имя));
	// Формат подписи — из настройки сервиса (Истина = прикреплённая, Ложь = открепленная).
	Контекст.Вставить("Прикрепленная", УКЭП_ПрикрепленнаяПодписьНаСервере(АдресСервера, КлючAPI, ПутьAPI));

	// 2. Выбрать сертификат из личного хранилища (штатный диалог, асинхронно).
	ОписаниеОповещения = Новый ОписаниеОповещения("УКЭП_ПослеВыбораСертификата", ЭтотОбъект, Контекст);
	НачатьВыборСертификата(ОписаниеОповещения);

КонецПроцедуры

// Оповещение о выборе сертификата подписи.
// ДанныеВыбора — отпечаток (Строка Base64) или объект сертификата.
//
&НаКлиенте
Процедура УКЭП_ПослеВыбораСертификата(ДанныеВыбора, Контекст) Экспорт

	Если ДанныеВыбора = Неопределено Тогда
		ПоказатьПредупреждение(, "Сертификат не выбран. Подписание отменено.");
		Возврат;
	КонецЕсли;

	Отпечаток = "";
	Если ТипЗнч(ДанныеВыбора) = Тип("Строка") Тогда
		Отпечаток = ДанныеВыбора;
	Иначе
		Попытка
			Отпечаток = Base64Строка(ДанныеВыбора.Отпечаток);
		Исключение
			Отпечаток = Строка(ДанныеВыбора);
		КонецПопытки;
	КонецЕсли;

	Контекст.Вставить("ОтпечатокСертификата", Отпечаток);

	// 3. Получить сертификаты личного хранилища (асинхронно), затем найти по отпечатку.
	ОписаниеОповещения = Новый ОписаниеОповещения("УКЭП_ПослеПолученияСертификата", ЭтотОбъект, Контекст,
		"УКЭП_ОшибкаКриптографии", ЭтотОбъект);
	НачатьПолучениеСертификатовКриптографии(ОписаниеОповещения, РасположениеХранилищаСертификатов.Личное);

КонецПроцедуры

// Оповещение со списком сертификатов личного хранилища.
//
&НаКлиенте
Процедура УКЭП_ПослеПолученияСертификата(МассивСертификатов, Контекст) Экспорт

	ИскомыйСертификат = Неопределено;
	Для Каждого Серт Из МассивСертификатов Цикл
		Если Base64Строка(Серт.Отпечаток) = Контекст.ОтпечатокСертификата Тогда
			ИскомыйСертификат = Серт;
			Прервать;
		КонецЕсли;
	КонецЦикла;

	Если ИскомыйСертификат = Неопределено Тогда
		ПоказатьПредупреждение(, "Не удалось получить выбранный сертификат из личного хранилища.");
		Возврат;
	КонецЕсли;

	Контекст.Вставить("Сертификат", ИскомыйСертификат);

	// 4. Инициализировать менеджер криптографии (асинхронно).
	ОписаниеОповещения = Новый ОписаниеОповещения("УКЭП_ПослеСозданияМенеджера", ЭтотОбъект, Контекст,
		"УКЭП_ОшибкаКриптографии", ЭтотОбъект);
	НачатьСозданиеМенеджераКриптографии(ОписаниеОповещения);

КонецПроцедуры

// Оповещение о создании МенеджераКриптографии.
//
&НаКлиенте
Процедура УКЭП_ПослеСозданияМенеджера(МенеджерКриптографии, Контекст) Экспорт

	Если МенеджерКриптографии = Неопределено Тогда
		ПоказатьПредупреждение(, "Не удалось инициализировать криптографию."
			+ " Установите криптопровайдер (КриптоПро CSP) и расширение работы с криптографией.");
		Возврат;
	КонецЕсли;

	ДанныеДокумента = ПолучитьИзВременногоХранилища(Контекст.АдресДокумента);

	// 5. Подписать CAdES-BES: открепленная или прикреплённая — по настройке сервиса.
	//    PIN запросит криптопровайдер сам.
	ОписаниеОповещения = Новый ОписаниеОповещения("УКЭП_ПослеПодписания", ЭтотОбъект, Контекст,
		"УКЭП_ОшибкаКриптографии", ЭтотОбъект);

	МенеджерКриптографии.НачатьПодписаниеДанныхCAdES(
		ОписаниеОповещения,
		ДанныеДокумента,
		Контекст.Сертификат,
		ТипПодписиCAdES.BES,
		Не Контекст.Прикрепленная); // Истина = отсоединённая (detached), Ложь = прикреплённая (attached).

КонецПроцедуры

// Оповещение о завершении подписания.
// ПодписьДвоичные — ДвоичныеДанные подписи (DER, PKCS#7/CAdES).
//
&НаКлиенте
Процедура УКЭП_ПослеПодписания(ПодписьДвоичные, Контекст) Экспорт

	Если ПодписьДвоичные = Неопределено Тогда
		ПоказатьПредупреждение(, "Подпись не была создана.");
		Возврат;
	КонецЕсли;

	// Сервер (api_1c_upload_signed) читает .sig и делает base64_decode(),
	// поэтому передаём подпись как Base64-ТЕКСТ DER.
	ПодписьBase64Текст = Base64Строка(ПодписьДвоичные);
	ПодписьТекстДД = ПолучитьДвоичныеДанныеИзСтроки(ПодписьBase64Текст, "UTF-8", Ложь);
	АдресПодписиВХранилище = ПоместитьВоВременноеХранилище(ПодписьТекстДД, УникальныйИдентификатор);

	// «Мягкие» подсказки cert_info (сервер доверяет только самой .sig,
	// но должность/e-mail может использовать как подсказку).
	ПодсказкаCertInfo = ПодсказкаСведенийСертификата(Контекст.Сертификат);
	IDКлиента = ТекущийIDКлиентаИзТаблицы();

	Состояние("Загрузка подписанного документа на сервер...");

	// 6. Отправить на сервер через ЕДИНЫЙ HTTP-слой формы (метод Form_MainForm_Module).
	Результат = ЗагрузитьПодписанныйНаСервере(
		АдресСервера, КлючAPI, ПутьAPI,
		Контекст.АдресДокумента, Контекст.ИмяФайла,
		АдресПодписиВХранилище,
		НазваниеДокумента, ОписаниеДокумента,
		IDКлиента, ПодсказкаCertInfo,
		ИННПодписанта, СНИЛСПодписанта);

	Если Результат.Успех Тогда
		ИННПодписанта = ""; СНИЛСПодписанта = ""; // чтобы не перенести на следующий документ
		Текст = "Документ подписан УКЭП и загружен на сервер." + Символы.ПС
			+ "ID документа: " + Результат.IDДокумента + Символы.ПС
			+ "Криптопроверка сервером: " + ?(Результат.КриптоПроверка, "ПОДТВЕРЖДЕНА", "не подтверждена") + Символы.ПС
			+ "Формат подписи: " + ?(Контекст.Прикрепленная, "прикреплённая (документ внутри .sig)", "открепленная");
		Если ЗначениеЗаполнено(Результат.ОшибкаПроверки) Тогда
			Текст = Текст + Символы.ПС + Результат.ОшибкаПроверки;
		КонецЕсли;
		ПоказатьПредупреждение(, Текст);
		ОбновитьСпискиДокументовИКлиентов();
	Иначе
		ПоказатьПредупреждение(, "Ошибка загрузки подписанного документа: " + Результат.Ошибка);
	КонецЕсли;

КонецПроцедуры

// Единый обработчик ошибок криптоопераций (ОбработкаОшибки в ОписанииОповещения).
//
&НаКлиенте
Процедура УКЭП_ОшибкаКриптографии(ИнформацияОбОшибке, СтандартнаяОбработка, Контекст) Экспорт

	СтандартнаяОбработка = Ложь;
	ЗаписатьВЖурналРегистрацииКлиент("Cert4U.УКЭП: " + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке));
	ПоказатьПредупреждение(, "Ошибка криптографии: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке)
		+ Символы.ПС + "Проверьте, что установлен КриптоПро CSP и расширение работы с криптографией.");

КонецПроцедуры

#КонецОбласти

#Область УКЭПСлужебное

// Формат подписи из настройки сервиса (GET api_1c_settings → sig_attached).
// При ошибке связи или на старой версии сервера — Ложь (открепленная, как раньше).
//
&НаСервереБезКонтекста
Функция УКЭП_ПрикрепленнаяПодписьНаСервере(Адрес, Ключ, Путь)

	Ответ = ВыполнитьGETЗапрос(Адрес, Ключ, Путь, "api_1c_settings");
	Если Не Ответ.Успех Тогда
		Возврат Ложь;
	КонецЕсли;

	Данные = РазобратьJSON(Ответ.Тело);
	Если Данные <> Неопределено И Данные.Свойство("sig_attached") И Данные.sig_attached = Истина Тогда
		Возврат Истина;
	КонецЕсли;
	Возврат Ложь;

КонецФункции

// Формирует «мягкие» подсказки сведений о сертификате (должность/e-mail).
//
&НаКлиенте
Функция ПодсказкаСведенийСертификата(Сертификат)

	Подсказка = Новый Структура;
	Попытка
		Субъект = Сертификат.Субъект; // Соответствие полей субъекта.
		Если Субъект <> Неопределено Тогда
			Если Субъект.Свойство("T") Тогда
				Подсказка.Вставить("subject_title", Строка(Субъект["T"]));
			КонецЕсли;
			Если Субъект.Свойство("E") Тогда
				Подсказка.Вставить("subject_email", Строка(Субъект["E"]));
			КонецЕсли;
		КонецЕсли;
	Исключение
		ЗаписатьВЖурналРегистрацииКлиент("Cert4U.УКЭП: не удалось прочитать субъект сертификата: "
			+ ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
	КонецПопытки;
	Возврат Подсказка;

КонецФункции

// ID клиента из текущей строки таблицы клиентов (или пустая строка).
//
&НаКлиенте
Функция ТекущийIDКлиентаИзТаблицы()

	ТекДанные = Элементы.ТаблицаКлиентов.ТекущиеДанные;
	Если ТекДанные <> Неопределено Тогда
		Возврат ТекДанные.IDКлиента;
	КонецЕсли;
	Возврат "";

КонецФункции

#КонецОбласти
