tdesktop/Telegram/SourceFiles/passport/passport_form_controller.cpp

2763 lines
78 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "passport/passport_form_controller.h"
#include "passport/passport_encryption.h"
#include "passport/passport_panel_controller.h"
#include "passport/passport_panel_edit_document.h"
#include "ui/boxes/confirm_box.h"
#include "boxes/passcode_box.h"
#include "lang/lang_keys.h"
#include "lang/lang_hardcoded.h"
#include "base/random.h"
#include "base/qthelp_url.h"
#include "base/unixtime.h"
#include "base/call_delayed.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "mainwindow.h"
#include "window/window_session_controller.h"
#include "core/click_handler_types.h"
#include "ui/toast/toast.h"
#include "ui/widgets/sent_code_field.h"
#include "main/main_session.h"
#include "storage/localimageloader.h"
#include "storage/localstorage.h"
#include "storage/file_upload.h"
#include "storage/file_download_mtproto.h"
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
namespace Passport {
namespace {
constexpr auto kDocumentScansLimit = 20;
constexpr auto kTranslationScansLimit = 20;
constexpr auto kShortPollTimeout = crl::time(3000);
constexpr auto kRememberCredentialsDelay = crl::time(1800 * 1000);
bool ForwardServiceErrorRequired(const QString &error) {
return (error == u"BOT_INVALID"_q)
|| (error == u"PUBLIC_KEY_REQUIRED"_q)
|| (error == u"PUBLIC_KEY_INVALID"_q)
|| (error == u"SCOPE_EMPTY"_q)
|| (error == u"PAYLOAD_EMPTY"_q);
}
bool SaveErrorRequiresRestart(const QString &error) {
return (error == u"PASSWORD_REQUIRED"_q)
|| (error == u"SECURE_SECRET_REQUIRED"_q)
|| (error == u"SECURE_SECRET_INVALID"_q);
}
bool AcceptErrorRequiresRestart(const QString &error) {
return (error == u"PASSWORD_REQUIRED"_q)
|| (error == u"SECURE_SECRET_REQUIRED"_q)
|| (error == u"SECURE_VALUE_EMPTY"_q)
|| (error == u"SECURE_VALUE_HASH_INVALID"_q);
}
std::map<QString, QString> GetTexts(const ValueMap &map) {
auto result = std::map<QString, QString>();
for (const auto &[key, value] : map.fields) {
result[key] = value.text;
}
return result;
}
QImage ReadImage(bytes::const_span buffer) {
return Images::Read({
.content = QByteArray::fromRawData(
reinterpret_cast<const char*>(buffer.data()),
buffer.size()),
.forceOpaque = true,
}).image;
}
Value::Type ConvertType(const MTPSecureValueType &type) {
using Type = Value::Type;
switch (type.type()) {
case mtpc_secureValueTypePersonalDetails:
return Type::PersonalDetails;
case mtpc_secureValueTypePassport:
return Type::Passport;
case mtpc_secureValueTypeDriverLicense:
return Type::DriverLicense;
case mtpc_secureValueTypeIdentityCard:
return Type::IdentityCard;
case mtpc_secureValueTypeInternalPassport:
return Type::InternalPassport;
case mtpc_secureValueTypeAddress:
return Type::Address;
case mtpc_secureValueTypeUtilityBill:
return Type::UtilityBill;
case mtpc_secureValueTypeBankStatement:
return Type::BankStatement;
case mtpc_secureValueTypeRentalAgreement:
return Type::RentalAgreement;
case mtpc_secureValueTypePassportRegistration:
return Type::PassportRegistration;
case mtpc_secureValueTypeTemporaryRegistration:
return Type::TemporaryRegistration;
case mtpc_secureValueTypePhone:
return Type::Phone;
case mtpc_secureValueTypeEmail:
return Type::Email;
}
Unexpected("Type in secureValueType type.");
};
MTPSecureValueType ConvertType(Value::Type type) {
using Type = Value::Type;
switch (type) {
case Type::PersonalDetails:
return MTP_secureValueTypePersonalDetails();
case Type::Passport:
return MTP_secureValueTypePassport();
case Type::DriverLicense:
return MTP_secureValueTypeDriverLicense();
case Type::IdentityCard:
return MTP_secureValueTypeIdentityCard();
case Type::InternalPassport:
return MTP_secureValueTypeInternalPassport();
case Type::Address:
return MTP_secureValueTypeAddress();
case Type::UtilityBill:
return MTP_secureValueTypeUtilityBill();
case Type::BankStatement:
return MTP_secureValueTypeBankStatement();
case Type::RentalAgreement:
return MTP_secureValueTypeRentalAgreement();
case Type::PassportRegistration:
return MTP_secureValueTypePassportRegistration();
case Type::TemporaryRegistration:
return MTP_secureValueTypeTemporaryRegistration();
case Type::Phone:
return MTP_secureValueTypePhone();
case Type::Email:
return MTP_secureValueTypeEmail();
}
Unexpected("Type in FormController::submit.");
}
void CollectToRequestedRow(
RequestedRow &row,
const MTPSecureRequiredType &data) {
data.match([&](const MTPDsecureRequiredType &data) {
row.values.emplace_back(ConvertType(data.vtype()));
auto &value = row.values.back();
value.selfieRequired = data.is_selfie_required();
value.translationRequired = data.is_translation_required();
value.nativeNames = data.is_native_names();
}, [&](const MTPDsecureRequiredTypeOneOf &data) {
row.values.reserve(row.values.size() + data.vtypes().v.size());
for (const auto &one : data.vtypes().v) {
CollectToRequestedRow(row, one);
}
});
}
void ApplyDataChanges(ValueData &data, ValueMap &&changes) {
data.parsedInEdit = data.parsed;
for (auto &[key, value] : changes.fields) {
data.parsedInEdit.fields[key] = std::move(value);
}
}
RequestedRow CollectRequestedRow(const MTPSecureRequiredType &data) {
auto result = RequestedRow();
CollectToRequestedRow(result, data);
return result;
}
QJsonObject GetJSONFromMap(
const std::map<QString, bytes::const_span> &map) {
auto result = QJsonObject();
for (const auto &[key, value] : map) {
const auto raw = QByteArray::fromRawData(
reinterpret_cast<const char*>(value.data()),
value.size());
result.insert(key, QString::fromUtf8(raw.toBase64()));
}
return result;
}
QJsonObject GetJSONFromFile(const File &file) {
return GetJSONFromMap({
{ "file_hash", file.hash },
{ "secret", file.secret }
});
}
FormRequest PreprocessRequest(const FormRequest &request) {
auto result = request;
result.publicKey.replace("\r\n", "\n");
return result;
}
QString ValueCredentialsKey(Value::Type type) {
using Type = Value::Type;
switch (type) {
case Type::PersonalDetails: return "personal_details";
case Type::Passport: return "passport";
case Type::DriverLicense: return "driver_license";
case Type::IdentityCard: return "identity_card";
case Type::InternalPassport: return "internal_passport";
case Type::Address: return "address";
case Type::UtilityBill: return "utility_bill";
case Type::BankStatement: return "bank_statement";
case Type::RentalAgreement: return "rental_agreement";
case Type::PassportRegistration: return "passport_registration";
case Type::TemporaryRegistration: return "temporary_registration";
case Type::Phone:
case Type::Email: return QString();
}
Unexpected("Type in ValueCredentialsKey.");
}
QString SpecialScanCredentialsKey(FileType type) {
switch (type) {
case FileType::FrontSide: return "front_side";
case FileType::ReverseSide: return "reverse_side";
case FileType::Selfie: return "selfie";
}
Unexpected("Type in SpecialScanCredentialsKey.");
}
QString ValidateUrl(const QString &url) {
const auto result = qthelp::validate_url(url);
return result.startsWith("tg://", Qt::CaseInsensitive)
? QString()
: result;
}
auto ParseConfig(const QByteArray &json) {
auto languagesByCountryCode = std::map<QString, QString>();
auto error = QJsonParseError{ 0, QJsonParseError::NoError };
const auto document = QJsonDocument::fromJson(json, &error);
if (error.error != QJsonParseError::NoError) {
LOG(("API Error: Failed to parse passport config, error: %1."
).arg(error.errorString()));
return languagesByCountryCode;
} else if (!document.isObject()) {
LOG(("API Error: Not an object received in passport config."));
return languagesByCountryCode;
}
const auto object = document.object();
for (auto i = object.constBegin(); i != object.constEnd(); ++i) {
const auto countryCode = i.key();
const auto language = i.value();
if (!language.isString()) {
LOG(("API Error: Not a string in passport config item."));
continue;
}
languagesByCountryCode.emplace(
countryCode,
language.toString());
}
return languagesByCountryCode;
}
} // namespace
QString NonceNameByScope(const QString &scope) {
return (scope.startsWith('{') && scope.endsWith('}'))
? u"nonce"_q
: u"payload"_q;
}
bool ValueChanged(not_null<const Value*> value, const ValueMap &data) {
const auto FileChanged = [](const EditFile &file) {
if (file.uploadData) {
return !file.deleted;
}
return file.deleted;
};
for (const auto &scan : value->filesInEdit(FileType::Scan)) {
if (FileChanged(scan)) {
return true;
}
}
for (const auto &scan : value->filesInEdit(FileType::Translation)) {
if (FileChanged(scan)) {
return true;
}
}
for (const auto &[type, scan] : value->specialScansInEdit) {
if (FileChanged(scan)) {
return true;
}
}
const auto &existing = value->data.parsed.fields;
for (const auto &[key, value] : data.fields) {
const auto i = existing.find(key);
if (i != existing.end()) {
if (i->second.text != value.text) {
return true;
}
} else if (!value.text.isEmpty()) {
return true;
}
}
return false;
}
FormRequest::FormRequest(
UserId botId,
const QString &scope,
const QString &callbackUrl,
const QString &publicKey,
const QString &nonce)
: botId(botId)
, scope(scope)
, callbackUrl(ValidateUrl(callbackUrl))
, publicKey(publicKey)
, nonce(nonce) {
}
EditFile::EditFile(
not_null<Main::Session*> session,
not_null<const Value*> value,
FileType type,
const File &fields,
std::unique_ptr<UploadScanData> &&uploadData)
: value(value)
, type(type)
, fields(std::move(fields))
, uploadData(session, std::move(uploadData))
, guard(std::make_shared<bool>(true)) {
}
UploadScanDataPointer::UploadScanDataPointer(
not_null<Main::Session*> session,
std::unique_ptr<UploadScanData> &&value)
: _session(session)
, _value(std::move(value)) {
}
UploadScanDataPointer::UploadScanDataPointer(
UploadScanDataPointer &&other) = default;
UploadScanDataPointer &UploadScanDataPointer::operator=(
UploadScanDataPointer &&other) = default;
UploadScanDataPointer::~UploadScanDataPointer() {
if (const auto value = _value.get()) {
if (const auto fullId = value->fullId) {
_session->uploader().cancel(fullId);
}
}
}
UploadScanData *UploadScanDataPointer::get() const {
return _value.get();
}
UploadScanDataPointer::operator UploadScanData*() const {
return _value.get();
}
UploadScanDataPointer::operator bool() const {
return _value.get();
}
UploadScanData *UploadScanDataPointer::operator->() const {
return _value.get();
}
RequestedValue::RequestedValue(Value::Type type) : type(type) {
}
Value::Value(Type type) : type(type) {
}
bool Value::requiresScan(FileType type) const {
if (type == FileType::Scan) {
return (this->type == Type::UtilityBill)
|| (this->type == Type::BankStatement)
|| (this->type == Type::RentalAgreement)
|| (this->type == Type::PassportRegistration)
|| (this->type == Type::TemporaryRegistration);
} else if (type == FileType::Translation) {
return translationRequired;
} else {
return requiresSpecialScan(type);
}
}
bool Value::requiresSpecialScan(FileType type) const {
switch (type) {
case FileType::FrontSide:
return (this->type == Type::Passport)
|| (this->type == Type::DriverLicense)
|| (this->type == Type::IdentityCard)
|| (this->type == Type::InternalPassport);
case FileType::ReverseSide:
return (this->type == Type::DriverLicense)
|| (this->type == Type::IdentityCard);
case FileType::Selfie:
return selfieRequired;
}
Unexpected("Special scan type in requiresSpecialScan.");
}
void Value::fillDataFrom(Value &&other) {
const auto savedSelfieRequired = selfieRequired;
const auto savedTranslationRequired = translationRequired;
const auto savedNativeNames = nativeNames;
const auto savedEditScreens = editScreens;
*this = std::move(other);
selfieRequired = savedSelfieRequired;
translationRequired = savedTranslationRequired;
nativeNames = savedNativeNames;
editScreens = savedEditScreens;
}
bool Value::scansAreFilled() const {
return (whatNotFilled() == 0);
}
int Value::whatNotFilled() const {
const auto noRequiredSpecialScan = [&](FileType type) {
return requiresSpecialScan(type)
&& (specialScans.find(type) == end(specialScans));
};
if (requiresScan(FileType::Scan) && _scans.empty()) {
return kNothingFilled;
} else if (noRequiredSpecialScan(FileType::FrontSide)) {
return kNothingFilled;
}
auto result = 0;
if (requiresScan(FileType::Translation) && _translations.empty()) {
result |= kNoTranslationFilled;
}
if (noRequiredSpecialScan(FileType::ReverseSide)
|| noRequiredSpecialScan(FileType::Selfie)) {
result |= kNoSelfieFilled;
}
return result;
}
void Value::saveInEdit(not_null<Main::Session*> session) {
const auto saveList = [&](FileType type) {
filesInEdit(type) = ranges::views::all(
files(type)
) | ranges::views::transform([=](const File &file) {
return EditFile(session, this, type, file, nullptr);
}) | ranges::to_vector;
};
saveList(FileType::Scan);
saveList(FileType::Translation);
specialScansInEdit.clear();
for (const auto &[type, scan] : specialScans) {
specialScansInEdit.emplace(type, EditFile(
session,
this,
type,
scan,
nullptr));
}
data.parsedInEdit = data.parsed;
}
void Value::clearEditData() {
filesInEdit(FileType::Scan).clear();
filesInEdit(FileType::Translation).clear();
specialScansInEdit.clear();
data.encryptedSecretInEdit.clear();
data.hashInEdit.clear();
data.parsedInEdit = ValueMap();
}
bool Value::uploadingScan() const {
const auto uploading = [](const EditFile &file) {
return file.uploadData
&& file.uploadData->fullId
&& !file.deleted;
};
const auto uploadingInList = [&](FileType type) {
const auto &list = filesInEdit(type);
return ranges::any_of(list, uploading);
};
if (uploadingInList(FileType::Scan)
|| uploadingInList(FileType::Translation)) {
return true;
}
if (ranges::any_of(specialScansInEdit, [&](const auto &pair) {
return uploading(pair.second);
})) {
return true;
}
return false;
}
bool Value::saving() const {
return (saveRequestId != 0)
|| (verification.requestId != 0)
|| (verification.codeLength != 0)
|| uploadingScan();
}
std::vector<File> &Value::files(FileType type) {
switch (type) {
case FileType::Scan: return _scans;
case FileType::Translation: return _translations;
}
Unexpected("Type in Value::files().");
}
const std::vector<File> &Value::files(FileType type) const {
switch (type) {
case FileType::Scan: return _scans;
case FileType::Translation: return _translations;
}
Unexpected("Type in Value::files() const.");
}
QString &Value::fileMissingError(FileType type) {
switch (type) {
case FileType::Scan: return _scanMissingError;
case FileType::Translation: return _translationMissingError;
}
Unexpected("Type in Value::fileMissingError().");
}
const QString &Value::fileMissingError(FileType type) const {
switch (type) {
case FileType::Scan: return _scanMissingError;
case FileType::Translation: return _translationMissingError;
}
Unexpected("Type in Value::fileMissingError() const.");
}
std::vector<EditFile> &Value::filesInEdit(FileType type) {
switch (type) {
case FileType::Scan: return _scansInEdit;
case FileType::Translation: return _translationsInEdit;
}
Unexpected("Type in Value::filesInEdit().");
}
const std::vector<EditFile> &Value::filesInEdit(FileType type) const {
switch (type) {
case FileType::Scan: return _scansInEdit;
case FileType::Translation: return _translationsInEdit;
}
Unexpected("Type in Value::filesInEdit() const.");
}
EditFile &Value::fileInEdit(FileType type, std::optional<int> fileIndex) {
switch (type) {
case FileType::Scan:
case FileType::Translation: {
auto &list = filesInEdit(type);
Assert(fileIndex.has_value());
Assert(*fileIndex >= 0 && *fileIndex < list.size());
return list[*fileIndex];
} break;
}
const auto i = specialScansInEdit.find(type);
Assert(!fileIndex.has_value());
Assert(i != end(specialScansInEdit));
return i->second;
}
const EditFile &Value::fileInEdit(
FileType type,
std::optional<int> fileIndex) const {
switch (type) {
case FileType::Scan:
case FileType::Translation: {
auto &list = filesInEdit(type);
Assert(fileIndex.has_value());
Assert(*fileIndex >= 0 && *fileIndex < list.size());
return list[*fileIndex];
} break;
}
const auto i = specialScansInEdit.find(type);
Assert(!fileIndex.has_value());
Assert(i != end(specialScansInEdit));
return i->second;
}
std::vector<EditFile> Value::takeAllFilesInEdit() {
auto result = base::take(filesInEdit(FileType::Scan));
auto &translation = filesInEdit(FileType::Translation);
auto &special = specialScansInEdit;
result.reserve(result.size() + translation.size() + special.size());
for (auto &scan : base::take(translation)) {
result.push_back(std::move(scan));
}
for (auto &[type, scan] : base::take(special)) {
result.push_back(std::move(scan));
}
return result;
}
FormController::FormController(
not_null<Window::SessionController*> controller,
const FormRequest &request)
: _controller(controller)
, _api(&_controller->session().mtp())
, _request(PreprocessRequest(request))
, _shortPollTimer([=] { reloadPassword(); })
, _view(std::make_unique<PanelController>(this)) {
}
Main::Session &FormController::session() const {
return _controller->session();
}
void FormController::show() {
requestForm();
requestPassword();
}
UserData *FormController::bot() const {
return _bot;
}
QString FormController::privacyPolicyUrl() const {
return _form.privacyPolicyUrl;
}
bytes::vector FormController::passwordHashForAuth(
bytes::const_span password) const {
return Core::ComputeCloudPasswordHash(_password.request.algo, password);
}
auto FormController::prepareFinalData() -> FinalData {
auto errors = std::vector<not_null<const Value*>>();
auto hashes = QVector<MTPSecureValueHash>();
auto secureData = QJsonObject();
const auto addValueToJSON = [&](
const QString &key,
not_null<const Value*> value) {
auto object = QJsonObject();
if (!value->data.parsed.fields.empty()) {
object.insert("data", GetJSONFromMap({
{ "data_hash", value->data.hash },
{ "secret", value->data.secret }
}));
}
const auto addList = [&](
const QString &key,
const std::vector<File> &list) {
if (!list.empty()) {
auto files = QJsonArray();
for (const auto &scan : list) {
files.append(GetJSONFromFile(scan));
}
object.insert(key, files);
}
};
addList("files", value->files(FileType::Scan));
if (value->translationRequired) {
addList("translation", value->files(FileType::Translation));
}
for (const auto &[type, scan] : value->specialScans) {
if (value->requiresSpecialScan(type)) {
object.insert(
SpecialScanCredentialsKey(type),
GetJSONFromFile(scan));
}
}
secureData.insert(key, object);
};
const auto addValue = [&](not_null<const Value*> value) {
hashes.push_back(MTP_secureValueHash(
ConvertType(value->type),
MTP_bytes(value->submitHash)));
const auto key = ValueCredentialsKey(value->type);
if (!key.isEmpty()) {
addValueToJSON(key, value);
}
};
const auto scopes = ComputeScopes(_form);
for (const auto &scope : scopes) {
const auto row = ComputeScopeRow(scope);
if (row.ready.isEmpty() || !row.error.isEmpty()) {
errors.push_back(scope.details
? scope.details
: scope.documents[0].get());
continue;
}
if (scope.details) {
addValue(scope.details);
}
if (!scope.documents.empty()) {
for (const auto &document : scope.documents) {
if (document->scansAreFilled()) {
addValue(document);
break;
}
}
}
}
auto json = QJsonObject();
if (errors.empty()) {
json.insert("secure_data", secureData);
json.insert(NonceNameByScope(_request.scope), _request.nonce);
}
return {
hashes,
QJsonDocument(json).toJson(QJsonDocument::Compact),
errors
};
}
std::vector<not_null<const Value*>> FormController::submitGetErrors() {
if (_submitRequestId || _submitSuccess|| _cancelled) {
return {};
}
const auto prepared = prepareFinalData();
if (!prepared.errors.empty()) {
return prepared.errors;
}
const auto credentialsEncryptedData = EncryptData(
bytes::make_span(prepared.credentials));
const auto credentialsEncryptedSecret = EncryptCredentialsSecret(
credentialsEncryptedData.secret,
bytes::make_span(_request.publicKey.toUtf8()));
_submitRequestId = _api.request(MTPaccount_AcceptAuthorization(
MTP_long(_request.botId.bare),
MTP_string(_request.scope),
MTP_string(_request.publicKey),
MTP_vector<MTPSecureValueHash>(prepared.hashes),
MTP_secureCredentialsEncrypted(
MTP_bytes(credentialsEncryptedData.bytes),
MTP_bytes(credentialsEncryptedData.hash),
MTP_bytes(credentialsEncryptedSecret))
)).done([=] {
_submitRequestId = 0;
_submitSuccess = true;
_view->showToast(tr::lng_passport_success(tr::now));
base::call_delayed(
(st::defaultToast.durationFadeIn
+ Ui::Toast::kDefaultDuration
+ st::defaultToast.durationFadeOut),
this,
[=] { cancel(); });
}).fail([=](const MTP::Error &error) {
_submitRequestId = 0;
if (handleAppUpdateError(error.type())) {
} else if (AcceptErrorRequiresRestart(error.type())) {
suggestRestart();
} else {
_view->show(Ui::MakeInformBox(
Lang::Hard::SecureAcceptError() + "\n" + error.type()));
}
}).send();
return {};
}
void FormController::checkPasswordHash(
mtpRequestId &guard,
bytes::vector hash,
PasswordCheckCallback callback) {
_passwordCheckHash = std::move(hash);
_passwordCheckCallback = std::move(callback);
if (_password.request.id) {
passwordChecked();
} else {
requestPasswordData(guard);
}
}
void FormController::passwordChecked() {
if (!_password.request || !_password.request.id) {
return passwordServerError();
}
const auto check = Core::ComputeCloudPasswordCheck(
_password.request,
_passwordCheckHash);
if (!check) {
return passwordServerError();
}
_password.request.id = 0;
_passwordCheckCallback(check);
}
void FormController::requestPasswordData(mtpRequestId &guard) {
if (!_passwordCheckCallback) {
return passwordServerError();
}
_api.request(base::take(guard)).cancel();
guard = _api.request(
MTPaccount_GetPassword()
).done([=, &guard](const MTPaccount_Password &result) {
guard = 0;
result.match([&](const MTPDaccount_password &data) {
_password.request = Core::ParseCloudPasswordCheckRequest(data);
passwordChecked();
});
}).send();
}
void FormController::submitPassword(const QByteArray &password) {
Expects(!!_password.request);
const auto submitSaved = !base::take(_savedPasswordValue).isEmpty();
if (_passwordCheckRequestId) {
return;
} else if (password.isEmpty()) {
_passwordError.fire(QString());
return;
}
const auto callback = [=](const Core::CloudPasswordResult &check) {
submitPassword(check, password, submitSaved);
};
checkPasswordHash(
_passwordCheckRequestId,
passwordHashForAuth(bytes::make_span(password)),
callback);
}
void FormController::submitPassword(
const Core::CloudPasswordResult &check,
const QByteArray &password,
bool submitSaved) {
_passwordCheckRequestId = _api.request(MTPaccount_GetPasswordSettings(
check.result
)).handleFloodErrors(
).done([=](const MTPaccount_PasswordSettings &result) {
Expects(result.type() == mtpc_account_passwordSettings);
_passwordCheckRequestId = 0;
_savedPasswordValue = QByteArray();
const auto &data = result.c_account_passwordSettings();
_password.confirmedEmail = qs(data.vemail().value_or_empty());
if (const auto wrapped = data.vsecure_settings()) {
const auto &settings = wrapped->c_secureSecretSettings();
const auto algo = Core::ParseSecureSecretAlgo(
settings.vsecure_algo());
if (v::is_null(algo)) {
_view->showUpdateAppBox();
return;
}
const auto hashForSecret = Core::ComputeSecureSecretHash(
algo,
bytes::make_span(password));
validateSecureSecret(
bytes::make_span(settings.vsecure_secret().v),
hashForSecret,
bytes::make_span(password),
settings.vsecure_secret_id().v);
if (!_secret.empty()) {
auto saved = SavedCredentials();
saved.hashForAuth = base::take(_passwordCheckHash);
saved.hashForSecret = hashForSecret;
saved.secretId = _secretId;
session().data().rememberPassportCredentials(
std::move(saved),
kRememberCredentialsDelay);
}
} else {
validateSecureSecret(
bytes::const_span(), // secure_secret
bytes::const_span(), // hash for secret
bytes::make_span(password),
0); // secure_secret_id
}
}).fail([=](const MTP::Error &error) {
_passwordCheckRequestId = 0;
if (error.type() == u"SRP_ID_INVALID"_q) {
handleSrpIdInvalid(_passwordCheckRequestId);
} else if (submitSaved) {
// Force reload and show form.
_password = PasswordSettings();
reloadPassword();
} else if (MTP::IsFloodError(error)) {
_passwordError.fire(tr::lng_flood_error(tr::now));
} else if (error.type() == u"PASSWORD_HASH_INVALID"_q
|| error.type() == u"SRP_PASSWORD_CHANGED"_q) {
_passwordError.fire(tr::lng_passport_password_wrong(tr::now));
} else {
_passwordError.fire_copy(error.type());
}
}).send();
}
bool FormController::handleSrpIdInvalid(mtpRequestId &guard) {
const auto now = crl::now();
if (_lastSrpIdInvalidTime > 0
&& now - _lastSrpIdInvalidTime < Core::kHandleSrpIdInvalidTimeout) {
_password.request.id = 0;
_passwordError.fire(Lang::Hard::ServerError());
return false;
} else {
_lastSrpIdInvalidTime = now;
requestPasswordData(guard);
return true;
}
}
void FormController::passwordServerError() {
_view->showCriticalError(Lang::Hard::ServerError());
}
void FormController::checkSavedPasswordSettings(
const SavedCredentials &credentials) {
const auto callback = [=](const Core::CloudPasswordResult &check) {
checkSavedPasswordSettings(check, credentials);
};
checkPasswordHash(
_passwordCheckRequestId,
credentials.hashForAuth,
callback);
}
void FormController::checkSavedPasswordSettings(
const Core::CloudPasswordResult &check,
const SavedCredentials &credentials) {
_passwordCheckRequestId = _api.request(MTPaccount_GetPasswordSettings(
check.result
)).done([=](const MTPaccount_PasswordSettings &result) {
Expects(result.type() == mtpc_account_passwordSettings);
_passwordCheckRequestId = 0;
const auto &data = result.c_account_passwordSettings();
if (const auto wrapped = data.vsecure_settings()) {
const auto &settings = wrapped->c_secureSecretSettings();
const auto algo = Core::ParseSecureSecretAlgo(
settings.vsecure_algo());
if (v::is_null(algo)) {
_view->showUpdateAppBox();
return;
} else if (!settings.vsecure_secret().v.isEmpty()
&& settings.vsecure_secret_id().v == credentials.secretId) {
_password.confirmedEmail = qs(data.vemail().value_or_empty());
validateSecureSecret(
bytes::make_span(settings.vsecure_secret().v),
credentials.hashForSecret,
{},
settings.vsecure_secret_id().v);
}
}
if (_secret.empty()) {
session().data().forgetPassportCredentials();
showForm();
}
}).fail([=](const MTP::Error &error) {
_passwordCheckRequestId = 0;
if (error.type() != u"SRP_ID_INVALID"_q
|| !handleSrpIdInvalid(_passwordCheckRequestId)) {
} else {
session().data().forgetPassportCredentials();
showForm();
}
}).send();
}
void FormController::recoverPassword() {
if (!_password.hasRecovery) {
_view->show(Ui::MakeInformBox(tr::lng_signin_no_email_forgot()));
return;
} else if (_recoverRequestId) {
return;
}
_recoverRequestId = _api.request(MTPauth_RequestPasswordRecovery(
)).done([=](const MTPauth_PasswordRecovery &result) {
Expects(result.type() == mtpc_auth_passwordRecovery);
_recoverRequestId = 0;
const auto &data = result.c_auth_passwordRecovery();
const auto pattern = qs(data.vemail_pattern());
auto fields = PasscodeBox::CloudFields{
.mtp = PasscodeBox::CloudFields::Mtp {
.newAlgo = _password.newAlgo,
.newSecureSecretAlgo = _password.newSecureAlgo,
},
.hasRecovery = _password.hasRecovery,
.pendingResetDate = _password.pendingResetDate,
};
// MSVC x64 (non-LTO) Release build fails with a linker error:
// - unresolved external variant::variant(variant const &)
// It looks like a MSVC bug and this works like a workaround.
const auto force = fields.mtp.newSecureSecretAlgo;
const auto box = _view->show(Box<RecoverBox>(
&_controller->session().mtp(),
&_controller->session(),
pattern,
fields));
box->newPasswordSet(
) | rpl::start_with_next([=](const QByteArray &password) {
if (password.isEmpty()) {
reloadPassword();
} else {
reloadAndSubmitPassword(password);
}
}, box->lifetime());
box->recoveryExpired(
) | rpl::start_with_next([=] {
box->closeBox();
}, box->lifetime());
}).fail([=](const MTP::Error &error) {
_recoverRequestId = 0;
_view->show(Ui::MakeInformBox(Lang::Hard::ServerError()
+ '\n'
+ error.type()));
}).send();
}
void FormController::reloadPassword() {
requestPassword();
}
void FormController::reloadAndSubmitPassword(const QByteArray &password) {
_savedPasswordValue = password;
requestPassword();
}
void FormController::cancelPassword() {
if (_passwordRequestId) {
return;
}
_passwordRequestId = _api.request(MTPaccount_CancelPasswordEmail(
)).done([=] {
_passwordRequestId = 0;
reloadPassword();
}).fail([=] {
_passwordRequestId = 0;
reloadPassword();
}).send();
}
void FormController::validateSecureSecret(
bytes::const_span encryptedSecret,
bytes::const_span passwordHashForSecret,
bytes::const_span passwordBytes,
uint64 serverSecretId) {
Expects(!passwordBytes.empty() || !passwordHashForSecret.empty());
if (!passwordHashForSecret.empty() && !encryptedSecret.empty()) {
_secret = DecryptSecureSecret(
encryptedSecret,
passwordHashForSecret);
if (_secret.empty()) {
_secretId = 0;
LOG(("API Error: Failed to decrypt secure secret."));
if (!passwordBytes.empty()) {
suggestReset(bytes::make_vector(passwordBytes));
}
return;
} else if (CountSecureSecretId(_secret) != serverSecretId) {
_secret.clear();
_secretId = 0;
LOG(("API Error: Wrong secure secret id."));
if (!passwordBytes.empty()) {
suggestReset(bytes::make_vector(passwordBytes));
}
return;
} else {
_secretId = serverSecretId;
decryptValues();
}
}
if (_secret.empty()) {
generateSecret(passwordBytes);
}
_secretReady.fire({});
}
void FormController::suggestReset(bytes::vector password) {
for (auto &[type, value] : _form.values) {
// if (!value.data.original.isEmpty()) {
resetValue(value);
// }
}
_view->suggestReset([=] {
const auto callback = [=](const Core::CloudPasswordResult &check) {
resetSecret(check, password);
};
checkPasswordHash(
_saveSecretRequestId,
passwordHashForAuth(bytes::make_span(password)),
callback);
_secretReady.fire({});
});
}
void FormController::resetSecret(
const Core::CloudPasswordResult &check,
const bytes::vector &password) {
using Flag = MTPDaccount_passwordInputSettings::Flag;
_saveSecretRequestId = _api.request(MTPaccount_UpdatePasswordSettings(
check.result,
MTP_account_passwordInputSettings(
MTP_flags(Flag::f_new_secure_settings),
MTPPasswordKdfAlgo(), // new_algo
MTPbytes(), // new_password_hash
MTPstring(), // hint
MTPstring(), // email
MTP_secureSecretSettings(
MTP_securePasswordKdfAlgoUnknown(), // secure_algo
MTP_bytes(), // secure_secret
MTP_long(0))) // secure_secret_id
)).done([=] {
_saveSecretRequestId = 0;
generateSecret(password);
}).fail([=](const MTP::Error &error) {
_saveSecretRequestId = 0;
if (error.type() != u"SRP_ID_INVALID"_q
|| !handleSrpIdInvalid(_saveSecretRequestId)) {
formFail(error.type());
}
}).send();
}
void FormController::decryptValues() {
Expects(!_secret.empty());
for (auto &[type, value] : _form.values) {
decryptValue(value);
}
fillErrors();
fillNativeFromFallback();
}
void FormController::fillErrors() {
const auto find = [&](const MTPSecureValueType &type) -> Value* {
const auto i = _form.values.find(ConvertType(type));
if (i != end(_form.values)) {
return &i->second;
}
LOG(("API Error: Value not found for error type."));
return nullptr;
};
const auto scan = [&](
Value &value,
FileType type,
bytes::const_span hash) -> File* {
auto &list = value.files(type);
const auto i = ranges::find_if(list, [&](const File &scan) {
return !bytes::compare(hash, scan.hash);
});
if (i != end(list)) {
return &*i;
}
LOG(("API Error: File not found for error value."));
return nullptr;
};
const auto setSpecialScanError = [&](FileType type, auto &&data) {
if (const auto value = find(data.vtype())) {
if (value->requiresSpecialScan(type)) {
const auto i = value->specialScans.find(type);
if (i != value->specialScans.end()) {
i->second.error = qs(data.vtext());
} else {
LOG(("API Error: "
"Special scan %1 not found for error value."
).arg(int(type)));
}
}
}
};
for (const auto &error : std::as_const(_form.pendingErrors)) {
error.match([&](const MTPDsecureValueError &data) {
if (const auto value = find(data.vtype())) {
if (CanHaveErrors(value->type)) {
value->error = qs(data.vtext());
}
}
}, [&](const MTPDsecureValueErrorData &data) {
if (const auto value = find(data.vtype())) {
const auto key = qs(data.vfield());
if (CanHaveErrors(value->type)
&& !SkipFieldCheck(value, key)) {
value->data.parsed.fields[key].error = qs(data.vtext());
}
}
}, [&](const MTPDsecureValueErrorFile &data) {
const auto hash = bytes::make_span(data.vfile_hash().v);
if (const auto value = find(data.vtype())) {
if (const auto file = scan(*value, FileType::Scan, hash)) {
if (value->requiresScan(FileType::Scan)) {
file->error = qs(data.vtext());
}
}
}
}, [&](const MTPDsecureValueErrorFiles &data) {
if (const auto value = find(data.vtype())) {
if (value->requiresScan(FileType::Scan)) {
value->fileMissingError(FileType::Scan)
= qs(data.vtext());
}
}
}, [&](const MTPDsecureValueErrorTranslationFile &data) {
const auto hash = bytes::make_span(data.vfile_hash().v);
if (const auto value = find(data.vtype())) {
const auto file = scan(*value, FileType::Translation, hash);
if (file && value->requiresScan(FileType::Translation)) {
file->error = qs(data.vtext());
}
}
}, [&](const MTPDsecureValueErrorTranslationFiles &data) {
if (const auto value = find(data.vtype())) {
if (value->requiresScan(FileType::Translation)) {
value->fileMissingError(FileType::Translation)
= qs(data.vtext());
}
}
}, [&](const MTPDsecureValueErrorFrontSide &data) {
setSpecialScanError(FileType::FrontSide, data);
}, [&](const MTPDsecureValueErrorReverseSide &data) {
setSpecialScanError(FileType::ReverseSide, data);
}, [&](const MTPDsecureValueErrorSelfie &data) {
setSpecialScanError(FileType::Selfie, data);
});
}
}
rpl::producer<EditDocumentCountry> FormController::preferredLanguage(
const QString &countryCode) {
const auto findLang = [=] {
if (countryCode.isEmpty()) {
return QString();
}
auto &langs = _passportConfig.languagesByCountryCode;
const auto i = langs.find(countryCode);
return (i == end(langs)) ? QString() : i->second;
};
return [=](auto consumer) {
const auto hash = _passportConfig.hash;
if (hash) {
consumer.put_next({ countryCode, findLang() });
consumer.put_done();
return rpl::lifetime() ;
}
_api.request(MTPhelp_GetPassportConfig(
MTP_int(hash)
)).done([=](const MTPhelp_PassportConfig &result) {
result.match([&](const MTPDhelp_passportConfig &data) {
_passportConfig.hash = data.vhash().v;
_passportConfig.languagesByCountryCode = ParseConfig(
data.vcountries_langs().c_dataJSON().vdata().v);
}, [](const MTPDhelp_passportConfigNotModified &data) {
});
consumer.put_next({ countryCode, findLang() });
consumer.put_done();
}).fail([=] {
consumer.put_next({ countryCode, QString() });
consumer.put_done();
}).send();
return rpl::lifetime();
};
}
void FormController::fillNativeFromFallback() {
// Check if additional values (*_name_native) were requested.
const auto i = _form.values.find(Value::Type::PersonalDetails);
if (i == end(_form.values) || !i->second.nativeNames) {
return;
}
auto values = i->second.data.parsed;
// Check if additional values should be copied from fallback values.
const auto scheme = GetDocumentScheme(
Scope::Type::PersonalDetails,
std::nullopt,
true,
[=](const QString &code) { return preferredLanguage(code); });
const auto dependencyIt = values.fields.find(
scheme.additionalDependencyKey);
const auto dependency = (dependencyIt == end(values.fields))
? QString()
: dependencyIt->second.text;
// Copy additional values from fallback if they're not filled yet.
using Scheme = EditDocumentScheme;
scheme.preferredLanguage(
dependency
) | rpl::map(
scheme.additionalShown
) | rpl::take(
1
) | rpl::start_with_next([=](Scheme::AdditionalVisibility v) {
if (v != Scheme::AdditionalVisibility::OnlyIfError) {
return;
}
auto values = i->second.data.parsed;
auto changed = false;
for (const auto &row : scheme.rows) {
if (row.valueClass == Scheme::ValueClass::Additional) {
const auto nativeIt = values.fields.find(row.key);
const auto native = (nativeIt == end(values.fields))
? QString()
: nativeIt->second.text;
if (!native.isEmpty()
|| (nativeIt != end(values.fields)
&& !nativeIt->second.error.isEmpty())) {
return;
}
const auto latinIt = values.fields.find(
row.additionalFallbackKey);
const auto latin = (latinIt == end(values.fields))
? QString()
: latinIt->second.text;
if (row.error(latin).has_value()) {
return;
} else if (native != latin) {
values.fields[row.key].text = latin;
changed = true;
}
}
}
if (changed) {
startValueEdit(&i->second);
saveValueEdit(&i->second, std::move(values));
}
}, _lifetime);
}
void FormController::decryptValue(Value &value) const {
Expects(!_secret.empty());
if (!validateValueSecrets(value)) {
resetValue(value);
return;
}
if (!value.data.original.isEmpty()) {
const auto decrypted = DecryptData(
bytes::make_span(value.data.original),
value.data.hash,
value.data.secret);
if (decrypted.empty()) {
LOG(("API Error: Could not decrypt value fields."));
resetValue(value);
return;
}
const auto fields = DeserializeData(decrypted);
value.data.parsed.fields.clear();
for (const auto &[key, text] : fields) {
value.data.parsed.fields[key] = { text };
}
}
}
bool FormController::validateValueSecrets(Value &value) const {
if (!value.data.original.isEmpty()) {
value.data.secret = DecryptValueSecret(
value.data.encryptedSecret,
_secret,
value.data.hash);
if (value.data.secret.empty()) {
LOG(("API Error: Could not decrypt data secret."));
return false;
}
}
const auto validateFileSecret = [&](File &file) {
file.secret = DecryptValueSecret(
file.encryptedSecret,
_secret,
file.hash);
if (file.secret.empty()) {
LOG(("API Error: Could not decrypt file secret."));
return false;
}
return true;
};
for (auto &scan : value.files(FileType::Scan)) {
if (!validateFileSecret(scan)) {
return false;
}
}
for (auto &scan : value.files(FileType::Translation)) {
if (!validateFileSecret(scan)) {
return false;
}
}
for (auto &[type, scan] : value.specialScans) {
if (!validateFileSecret(scan)) {
return false;
}
}
return true;
}
void FormController::resetValue(Value &value) const {
value.fillDataFrom(Value(value.type));
}
rpl::producer<QString> FormController::passwordError() const {
return _passwordError.events();
}
const PasswordSettings &FormController::passwordSettings() const {
return _password;
}
void FormController::uploadScan(
not_null<const Value*> value,
FileType type,
QByteArray &&content) {
if (!canAddScan(value, type)) {
_view->showToast(tr::lng_passport_scans_limit_reached(tr::now));
return;
}
const auto nonconst = findValue(value);
const auto fileIndex = [&]() -> std::optional<int> {
auto scanInEdit = EditFile(
&session(),
nonconst,
type,
File(),
nullptr);
if (type == FileType::Scan || type == FileType::Translation) {
auto &list = nonconst->filesInEdit(type);
list.push_back(std::move(scanInEdit));
return list.size() - 1;
}
auto i = nonconst->specialScansInEdit.find(type);
if (i != nonconst->specialScansInEdit.end()) {
i->second = std::move(scanInEdit);
} else {
i = nonconst->specialScansInEdit.emplace(
type,
std::move(scanInEdit)).first;
}
return std::nullopt;
}();
auto &scan = nonconst->fileInEdit(type, fileIndex);
encryptFile(scan, std::move(content), [=](UploadScanData &&result) {
uploadEncryptedFile(
nonconst->fileInEdit(type, fileIndex),
std::move(result));
});
}
void FormController::deleteScan(
not_null<const Value*> value,
FileType type,
std::optional<int> fileIndex) {
scanDeleteRestore(value, type, fileIndex, true);
}
void FormController::restoreScan(
not_null<const Value*> value,
FileType type,
std::optional<int> fileIndex) {
scanDeleteRestore(value, type, fileIndex, false);
}
void FormController::prepareFile(
EditFile &file,
const QByteArray &content) {
const auto fileId = base::RandomValue<uint64>();
file.fields.size = content.size();
file.fields.id = fileId;
file.fields.dcId = _controller->session().mainDcId();
file.fields.secret = GenerateSecretBytes();
file.fields.date = base::unixtime::now();
file.fields.image = ReadImage(bytes::make_span(content));
file.fields.downloadStatus.set(LoadStatus::Status::Done);
_scanUpdated.fire(&file);
}
void FormController::encryptFile(
EditFile &file,
QByteArray &&content,
Fn<void(UploadScanData &&result)> callback) {
prepareFile(file, content);
const auto weak = std::weak_ptr<bool>(file.guard);
crl::async([
=,
fileId = file.fields.id,
bytes = std::move(content),
fileSecret = file.fields.secret
] {
auto data = EncryptData(
bytes::make_span(bytes),
fileSecret);
auto result = UploadScanData();
result.fileId = fileId;
result.hash = std::move(data.hash);
result.bytes = std::move(data.bytes);
result.md5checksum.resize(32);
hashMd5Hex(
result.bytes.data(),
result.bytes.size(),
result.md5checksum.data());
crl::on_main([=, encrypted = std::move(result)]() mutable {
if (weak.lock()) {
callback(std::move(encrypted));
}
});
});
}
void FormController::scanDeleteRestore(
not_null<const Value*> value,
FileType type,
std::optional<int> fileIndex,
bool deleted) {
const auto nonconst = findValue(value);
auto &scan = nonconst->fileInEdit(type, fileIndex);
if (scan.deleted && !deleted) {
if (!canAddScan(value, type)) {
_view->showToast(tr::lng_passport_scans_limit_reached(tr::now));
return;
}
}
scan.deleted = deleted;
_scanUpdated.fire(&scan);
}
bool FormController::canAddScan(
not_null<const Value*> value,
FileType type) const {
const auto limit = (type == FileType::Scan)
? kDocumentScansLimit
: (type == FileType::Translation)
? kTranslationScansLimit
: -1;
if (limit < 0) {
return true;
}
const auto scansCount = ranges::count_if(
value->filesInEdit(type),
[](const EditFile &scan) { return !scan.deleted; });
return (scansCount < limit);
}
void FormController::subscribeToUploader() {
if (_uploaderSubscriptions) {
return;
}
using namespace Storage;
session().uploader().secureReady(
) | rpl::start_with_next([=](const UploadSecureDone &data) {
scanUploadDone(data);
}, _uploaderSubscriptions);
session().uploader().secureProgress(
) | rpl::start_with_next([=](const UploadSecureProgress &data) {
scanUploadProgress(data);
}, _uploaderSubscriptions);
session().uploader().secureFailed(
) | rpl::start_with_next([=](const FullMsgId &fullId) {
scanUploadFail(fullId);
}, _uploaderSubscriptions);
}
void FormController::uploadEncryptedFile(
EditFile &file,
UploadScanData &&data) {
subscribeToUploader();
file.uploadData = UploadScanDataPointer(
&session(),
std::make_unique<UploadScanData>(std::move(data)));
auto prepared = std::make_shared<FileLoadResult>(
TaskId(),
file.uploadData->fileId,
FileLoadTo(PeerId(), Api::SendOptions(), FullReplyTo(), MsgId()),
TextWithTags(),
false,
std::shared_ptr<SendingAlbum>(nullptr));
prepared->type = SendMediaType::Secure;
prepared->content = QByteArray::fromRawData(
reinterpret_cast<char*>(file.uploadData->bytes.data()),
file.uploadData->bytes.size());
prepared->setFileData(prepared->content);
prepared->filemd5 = file.uploadData->md5checksum;
file.uploadData->fullId = FullMsgId(
session().userPeerId(),
session().data().nextLocalMessageId());
file.uploadData->status.set(LoadStatus::Status::InProgress, 0);
session().uploader().upload(
file.uploadData->fullId,
std::move(prepared));
}
void FormController::scanUploadDone(const Storage::UploadSecureDone &data) {
if (const auto file = findEditFile(data.fullId)) {
Assert(file->uploadData != nullptr);
Assert(file->uploadData->fileId == data.fileId);
file->uploadData->partsCount = data.partsCount;
file->fields.hash = std::move(file->uploadData->hash);
file->fields.encryptedSecret = EncryptValueSecret(
file->fields.secret,
_secret,
file->fields.hash);
file->uploadData->fullId = FullMsgId();
file->uploadData->status.set(LoadStatus::Status::Done);
_scanUpdated.fire(file);
}
}
void FormController::scanUploadProgress(
const Storage::UploadSecureProgress &data) {
if (const auto file = findEditFile(data.fullId)) {
Assert(file->uploadData != nullptr);
file->uploadData->status.set(
LoadStatus::Status::InProgress,
data.offset);
_scanUpdated.fire(file);
}
}
void FormController::scanUploadFail(const FullMsgId &fullId) {
if (const auto file = findEditFile(fullId)) {
Assert(file->uploadData != nullptr);
file->uploadData->status.set(LoadStatus::Status::Failed);
_scanUpdated.fire(file);
}
}
rpl::producer<> FormController::secretReadyEvents() const {
return _secretReady.events();
}
QString FormController::defaultEmail() const {
return _password.confirmedEmail;
}
QString FormController::defaultPhoneNumber() const {
return session().user()->phone();
}
auto FormController::scanUpdated() const
-> rpl::producer<not_null<const EditFile*>> {
return _scanUpdated.events();
}
auto FormController::valueSaveFinished() const
-> rpl::producer<not_null<const Value*>> {
return _valueSaveFinished.events();
}
auto FormController::verificationNeeded() const
-> rpl::producer<not_null<const Value*>> {
return _verificationNeeded.events();
}
auto FormController::verificationUpdate() const
-> rpl::producer<not_null<const Value*>> {
return _verificationUpdate.events();
}
void FormController::verify(
not_null<const Value*> value,
const QString &code) {
if (value->verification.requestId) {
return;
}
const auto nonconst = findValue(value);
const auto prepared = code.trimmed();
Assert(nonconst->verification.codeLength != 0);
verificationError(nonconst, QString());
if (nonconst->verification.codeLength > 0
&& nonconst->verification.codeLength != prepared.size()) {
verificationError(nonconst, tr::lng_signin_wrong_code(tr::now));
return;
} else if (prepared.isEmpty()) {
verificationError(nonconst, tr::lng_signin_wrong_code(tr::now));
return;
}
nonconst->verification.requestId = [&] {
switch (nonconst->type) {
case Value::Type::Phone:
return _api.request(MTPaccount_VerifyPhone(
MTP_string(getPhoneFromValue(nonconst)),
MTP_string(nonconst->verification.phoneCodeHash),
MTP_string(prepared)
)).done([=](const MTPBool &result) {
savePlainTextValue(nonconst);
clearValueVerification(nonconst);
}).fail([=](const MTP::Error &error) {
nonconst->verification.requestId = 0;
if (error.type() == u"PHONE_CODE_INVALID"_q) {
verificationError(
nonconst,
tr::lng_signin_wrong_code(tr::now));
} else {
verificationError(nonconst, error.type());
}
}).send();
case Value::Type::Email:
return _api.request(MTPaccount_VerifyEmail(
MTP_emailVerifyPurposePassport(),
MTP_emailVerificationCode(MTP_string(prepared))
)).done([=](const MTPaccount_EmailVerified &result) {
savePlainTextValue(nonconst);
clearValueVerification(nonconst);
}).fail([=](const MTP::Error &error) {
nonconst->verification.requestId = 0;
if (error.type() == u"CODE_INVALID"_q) {
verificationError(
nonconst,
tr::lng_signin_wrong_code(tr::now));
} else {
verificationError(nonconst, error.type());
}
}).send();
}
Unexpected("Type in FormController::verify().");
}();
}
void FormController::verificationError(
not_null<Value*> value,
const QString &text) {
value->verification.error = text;
_verificationUpdate.fire_copy(value);
}
const Form &FormController::form() const {
return _form;
}
not_null<Value*> FormController::findValue(not_null<const Value*> value) {
const auto i = _form.values.find(value->type);
Assert(i != end(_form.values));
const auto result = &i->second;
Ensures(result == value);
return result;
}
void FormController::startValueEdit(not_null<const Value*> value) {
const auto nonconst = findValue(value);
++nonconst->editScreens;
if (nonconst->saving()) {
return;
}
for (auto &scan : nonconst->files(FileType::Scan)) {
loadFile(scan);
}
if (nonconst->translationRequired) {
for (auto &scan : nonconst->files(FileType::Translation)) {
loadFile(scan);
}
}
for (auto &[type, scan] : nonconst->specialScans) {
if (nonconst->requiresSpecialScan(type)) {
loadFile(scan);
}
}
nonconst->saveInEdit(&session());
}
void FormController::loadFile(File &file) {
if (!file.image.isNull()) {
file.downloadStatus.set(LoadStatus::Status::Done);
return;
}
const auto key = FileKey{ file.id };
const auto i = _fileLoaders.find(key);
if (i != _fileLoaders.end()) {
return;
}
file.downloadStatus.set(LoadStatus::Status::InProgress, 0);
const auto &[j, ok] = _fileLoaders.emplace(
key,
std::make_unique<mtpFileLoader>(
&_controller->session(),
StorageFileLocation(
file.dcId,
session().userId(),
MTP_inputSecureFileLocation(
MTP_long(file.id),
MTP_long(file.accessHash))),
Data::FileOrigin(),
SecureFileLocation,
QString(),
file.size,
file.size,
LoadToCacheAsWell,
LoadFromCloudOrLocal,
false,
Data::kImageCacheTag));
const auto loader = j->second.get();
loader->updates(
) | rpl::start_with_next_error_done([=] {
fileLoadProgress(key, loader->currentOffset());
}, [=](FileLoader::Error error) {
fileLoadFail(key);
}, [=] {
fileLoadDone(key, loader->bytes());
}, loader->lifetime());
loader->start();
}
void FormController::fileLoadDone(FileKey key, const QByteArray &bytes) {
if (const auto &[value, file] = findFile(key); file != nullptr) {
const auto decrypted = DecryptData(
bytes::make_span(bytes),
file->hash,
file->secret);
if (decrypted.empty()) {
fileLoadFail(key);
return;
}
file->downloadStatus.set(LoadStatus::Status::Done);
file->image = ReadImage(gsl::make_span(decrypted));
if (const auto fileInEdit = findEditFile(key)) {
fileInEdit->fields.image = file->image;
fileInEdit->fields.downloadStatus = file->downloadStatus;
_scanUpdated.fire(fileInEdit);
}
}
}
void FormController::fileLoadProgress(FileKey key, int offset) {
if (const auto &[value, file] = findFile(key); file != nullptr) {
file->downloadStatus.set(LoadStatus::Status::InProgress, offset);
if (const auto fileInEdit = findEditFile(key)) {
fileInEdit->fields.downloadStatus = file->downloadStatus;
_scanUpdated.fire(fileInEdit);
}
}
}
void FormController::fileLoadFail(FileKey key) {
if (const auto &[value, file] = findFile(key); file != nullptr) {
file->downloadStatus.set(LoadStatus::Status::Failed);
if (const auto fileInEdit = findEditFile(key)) {
fileInEdit->fields.downloadStatus = file->downloadStatus;
_scanUpdated.fire(fileInEdit);
}
}
}
void FormController::cancelValueEdit(not_null<const Value*> value) {
Expects(value->editScreens > 0);
const auto nonconst = findValue(value);
--nonconst->editScreens;
clearValueEdit(nonconst);
}
void FormController::valueEditFailed(not_null<Value*> value) {
Expects(!value->saving());
if (value->editScreens == 0) {
clearValueEdit(value);
}
}
void FormController::clearValueEdit(not_null<Value*> value) {
if (value->saving()) {
return;
}
value->clearEditData();
}
void FormController::cancelValueVerification(not_null<const Value*> value) {
const auto nonconst = findValue(value);
clearValueVerification(nonconst);
if (!nonconst->saving()) {
valueEditFailed(nonconst);
}
}
void FormController::clearValueVerification(not_null<Value*> value) {
const auto was = (value->verification.codeLength != 0);
if (const auto requestId = base::take(value->verification.requestId)) {
_api.request(requestId).cancel();
}
value->verification = Verification();
if (was) {
_verificationUpdate.fire_copy(value);
}
}
bool FormController::isEncryptedValue(Value::Type type) const {
return (type != Value::Type::Phone && type != Value::Type::Email);
}
void FormController::saveValueEdit(
not_null<const Value*> value,
ValueMap &&data) {
if (value->saving() || _submitRequestId) {
return;
}
// If we didn't change anything, we don't send save request
// and we don't reset value->error/[scan|translation]MissingError.
// Otherwise we reset them after save by re-parsing the value.
const auto nonconst = findValue(value);
if (!ValueChanged(nonconst, data)) {
nonconst->saveRequestId = -1;
crl::on_main(this, [=] {
nonconst->clearEditData();
nonconst->saveRequestId = 0;
_valueSaveFinished.fire_copy(nonconst);
});
return;
}
ApplyDataChanges(nonconst->data, std::move(data));
if (isEncryptedValue(nonconst->type)) {
saveEncryptedValue(nonconst);
} else {
savePlainTextValue(nonconst);
}
}
void FormController::deleteValueEdit(not_null<const Value*> value) {
if (value->saving() || _submitRequestId) {
return;
}
const auto nonconst = findValue(value);
nonconst->saveRequestId = _api.request(MTPaccount_DeleteSecureValue(
MTP_vector<MTPSecureValueType>(1, ConvertType(nonconst->type))
)).done([=] {
resetValue(*nonconst);
_valueSaveFinished.fire_copy(value);
}).fail([=](const MTP::Error &error) {
nonconst->saveRequestId = 0;
valueSaveShowError(nonconst, error);
}).send();
}
void FormController::saveEncryptedValue(not_null<Value*> value) {
Expects(isEncryptedValue(value->type));
if (_secret.empty()) {
_secretCallbacks.push_back([=] {
saveEncryptedValue(value);
});
return;
}
const auto wrapFile = [](const EditFile &file) {
if (const auto uploadData = file.uploadData.get()) {
return MTP_inputSecureFileUploaded(
MTP_long(file.fields.id),
MTP_int(uploadData->partsCount),
MTP_bytes(uploadData->md5checksum),
MTP_bytes(file.fields.hash),
MTP_bytes(file.fields.encryptedSecret));
}
return MTP_inputSecureFile(
MTP_long(file.fields.id),
MTP_long(file.fields.accessHash));
};
const auto wrapList = [&](not_null<const Value*> value, FileType type) {
const auto &list = value->filesInEdit(type);
auto result = QVector<MTPInputSecureFile>();
result.reserve(list.size());
for (const auto &scan : value->filesInEdit(type)) {
if (scan.deleted) {
continue;
}
result.push_back(wrapFile(scan));
}
return result;
};
const auto files = wrapList(value, FileType::Scan);
const auto translations = wrapList(value, FileType::Translation);
if (value->data.secret.empty()) {
value->data.secret = GenerateSecretBytes();
}
const auto encryptedData = EncryptData(
SerializeData(GetTexts(value->data.parsedInEdit)),
value->data.secret);
value->data.hashInEdit = encryptedData.hash;
value->data.encryptedSecretInEdit = EncryptValueSecret(
value->data.secret,
_secret,
value->data.hashInEdit);
const auto hasSpecialFile = [&](FileType type) {
const auto i = value->specialScansInEdit.find(type);
return (i != end(value->specialScansInEdit) && !i->second.deleted);
};
const auto specialFile = [&](FileType type) {
const auto i = value->specialScansInEdit.find(type);
return (i != end(value->specialScansInEdit) && !i->second.deleted)
? wrapFile(i->second)
: MTPInputSecureFile();
};
const auto frontSide = specialFile(FileType::FrontSide);
const auto reverseSide = specialFile(FileType::ReverseSide);
const auto selfie = specialFile(FileType::Selfie);
const auto type = ConvertType(value->type);
const auto flags = (value->data.parsedInEdit.fields.empty()
? MTPDinputSecureValue::Flag(0)
: MTPDinputSecureValue::Flag::f_data)
| (hasSpecialFile(FileType::FrontSide)
? MTPDinputSecureValue::Flag::f_front_side
: MTPDinputSecureValue::Flag(0))
| (hasSpecialFile(FileType::ReverseSide)
? MTPDinputSecureValue::Flag::f_reverse_side
: MTPDinputSecureValue::Flag(0))
| (hasSpecialFile(FileType::Selfie)
? MTPDinputSecureValue::Flag::f_selfie
: MTPDinputSecureValue::Flag(0))
| (translations.empty()
? MTPDinputSecureValue::Flag(0)
: MTPDinputSecureValue::Flag::f_translation)
| (files.empty()
? MTPDinputSecureValue::Flag(0)
: MTPDinputSecureValue::Flag::f_files);
Assert(flags != MTPDinputSecureValue::Flags(0));
sendSaveRequest(value, MTP_inputSecureValue(
MTP_flags(flags),
type,
MTP_secureData(
MTP_bytes(encryptedData.bytes),
MTP_bytes(value->data.hashInEdit),
MTP_bytes(value->data.encryptedSecretInEdit)),
frontSide,
reverseSide,
selfie,
MTP_vector<MTPInputSecureFile>(translations),
MTP_vector<MTPInputSecureFile>(files),
MTPSecurePlainData()));
}
void FormController::savePlainTextValue(not_null<Value*> value) {
Expects(!isEncryptedValue(value->type));
const auto text = getPlainTextFromValue(value);
const auto type = [&] {
switch (value->type) {
case Value::Type::Phone: return MTP_secureValueTypePhone();
case Value::Type::Email: return MTP_secureValueTypeEmail();
}
Unexpected("Value type in savePlainTextValue().");
}();
const auto plain = [&] {
switch (value->type) {
case Value::Type::Phone: return MTP_securePlainPhone;
case Value::Type::Email: return MTP_securePlainEmail;
}
Unexpected("Value type in savePlainTextValue().");
}();
sendSaveRequest(value, MTP_inputSecureValue(
MTP_flags(MTPDinputSecureValue::Flag::f_plain_data),
type,
MTPSecureData(),
MTPInputSecureFile(),
MTPInputSecureFile(),
MTPInputSecureFile(),
MTPVector<MTPInputSecureFile>(),
MTPVector<MTPInputSecureFile>(),
plain(MTP_string(text))));
}
void FormController::sendSaveRequest(
not_null<Value*> value,
const MTPInputSecureValue &data) {
Expects(value->saveRequestId == 0);
value->saveRequestId = _api.request(MTPaccount_SaveSecureValue(
data,
MTP_long(_secretId)
)).done([=](const MTPSecureValue &result) {
auto scansInEdit = value->takeAllFilesInEdit();
auto refreshed = parseValue(result, scansInEdit);
decryptValue(refreshed);
value->fillDataFrom(std::move(refreshed));
_valueSaveFinished.fire_copy(value);
}).fail([=](const MTP::Error &error) {
value->saveRequestId = 0;
const auto code = error.type();
if (handleAppUpdateError(code)) {
} else if (code == u"PHONE_VERIFICATION_NEEDED"_q) {
if (value->type == Value::Type::Phone) {
startPhoneVerification(value);
return;
}
} else if (code == u"PHONE_NUMBER_INVALID"_q) {
if (value->type == Value::Type::Phone) {
value->data.parsedInEdit.fields["value"].error
= tr::lng_bad_phone(tr::now);
valueSaveFailed(value);
return;
}
} else if (code == u"EMAIL_VERIFICATION_NEEDED"_q) {
if (value->type == Value::Type::Email) {
startEmailVerification(value);
return;
}
} else if (code == u"EMAIL_INVALID"_q) {
if (value->type == Value::Type::Email) {
value->data.parsedInEdit.fields["value"].error
= tr::lng_cloud_password_bad_email(tr::now);
valueSaveFailed(value);
return;
}
}
if (SaveErrorRequiresRestart(code)) {
suggestRestart();
} else {
valueSaveShowError(value, error);
}
}).send();
}
QString FormController::getPhoneFromValue(
not_null<const Value*> value) const {
Expects(value->type == Value::Type::Phone);
return getPlainTextFromValue(value);
}
QString FormController::getEmailFromValue(
not_null<const Value*> value) const {
Expects(value->type == Value::Type::Email);
return getPlainTextFromValue(value);
}
QString FormController::getPlainTextFromValue(
not_null<const Value*> value) const {
Expects(value->type == Value::Type::Phone
|| value->type == Value::Type::Email);
const auto i = value->data.parsedInEdit.fields.find("value");
Assert(i != end(value->data.parsedInEdit.fields));
return i->second.text;
}
void FormController::startPhoneVerification(not_null<Value*> value) {
value->verification.requestId = _api.request(MTPaccount_SendVerifyPhoneCode(
MTP_string(getPhoneFromValue(value)),
MTP_codeSettings(
MTP_flags(0),
MTPVector<MTPbytes>(),
MTPstring(),
MTPBool())
)).done([=](const MTPauth_SentCode &result) {
result.match([&](const MTPDauth_sentCode &data) {
const auto next = data.vnext_type();
const auto timeout = data.vtimeout();
value->verification.requestId = 0;
value->verification.phoneCodeHash = qs(data.vphone_code_hash());
value->verification.fragmentUrl = QString();
const auto bad = [](const char *type) {
LOG(("API Error: Should not be '%1' "
"in FormController::startPhoneVerification.").arg(type));
};
data.vtype().match([&](const MTPDauth_sentCodeTypeApp &) {
LOG(("API Error: sentCodeTypeApp not expected "
"in FormController::startPhoneVerification."));
}, [&](const MTPDauth_sentCodeTypeCall &data) {
value->verification.codeLength = (data.vlength().v > 0)
? data.vlength().v
: -1;
value->verification.call = std::make_unique<Ui::SentCodeCall>(
[=] { requestPhoneCall(value); },
[=] { _verificationUpdate.fire_copy(value); });
value->verification.call->setStatus(
{ Ui::SentCodeCall::State::Called, 0 });
if (next) {
LOG(("API Error: next_type is not supported for calls."));
}
}, [&](const MTPDauth_sentCodeTypeSms &data) {
value->verification.codeLength = (data.vlength().v > 0)
? data.vlength().v
: -1;
if (next && next->type() == mtpc_auth_codeTypeCall) {
value->verification.call = std::make_unique<Ui::SentCodeCall>(
[=] { requestPhoneCall(value); },
[=] { _verificationUpdate.fire_copy(value); });
value->verification.call->setStatus({
Ui::SentCodeCall::State::Waiting,
timeout.value_or(60),
});
}
}, [&](const MTPDauth_sentCodeTypeFragmentSms &data) {
value->verification.codeLength = data.vlength().v;
value->verification.fragmentUrl = qs(data.vurl());
value->verification.call = nullptr;
}, [&](const MTPDauth_sentCodeTypeFlashCall &) {
bad("FlashCall");
}, [&](const MTPDauth_sentCodeTypeMissedCall &) {
bad("MissedCall");
}, [&](const MTPDauth_sentCodeTypeFirebaseSms &) {
bad("FirebaseSms");
}, [&](const MTPDauth_sentCodeTypeEmailCode &) {
bad("EmailCode");
}, [&](const MTPDauth_sentCodeTypeSetUpEmailRequired &) {
bad("SetUpEmailRequired");
});
_verificationNeeded.fire_copy(value);
}, [](const MTPDauth_sentCodeSuccess &) {
LOG(("API Error: Unexpected auth.sentCodeSuccess "
"(FormController::startPhoneVerification)."));
});
}).fail([=](const MTP::Error &error) {
value->verification.requestId = 0;
valueSaveShowError(value, error);
}).send();
}
void FormController::startEmailVerification(not_null<Value*> value) {
value->verification.requestId = _api.request(
MTPaccount_SendVerifyEmailCode(
MTP_emailVerifyPurposePassport(),
MTP_string(getEmailFromValue(value)))
).done([=](const MTPaccount_SentEmailCode &result) {
Expects(result.type() == mtpc_account_sentEmailCode);
value->verification.requestId = 0;
const auto &data = result.c_account_sentEmailCode();
value->verification.codeLength = (data.vlength().v > 0)
? data.vlength().v
: -1;
_verificationNeeded.fire_copy(value);
}).fail([=](const MTP::Error &error) {
valueSaveShowError(value, error);
}).send();
}
void FormController::requestPhoneCall(not_null<Value*> value) {
Expects(value->verification.call != nullptr);
value->verification.call->setStatus(
{ Ui::SentCodeCall::State::Calling, 0 });
_api.request(MTPauth_ResendCode(
MTP_string(getPhoneFromValue(value)),
MTP_string(value->verification.phoneCodeHash)
)).done([=] {
value->verification.call->callDone();
}).send();
}
void FormController::valueSaveShowError(
not_null<Value*> value,
const MTP::Error &error) {
_view->show(Ui::MakeInformBox(
Lang::Hard::SecureSaveError() + "\n" + error.type()));
valueSaveFailed(value);
}
void FormController::valueSaveFailed(not_null<Value*> value) {
valueEditFailed(value);
_valueSaveFinished.fire_copy(value);
}
void FormController::generateSecret(bytes::const_span password) {
Expects(!password.empty());
if (_saveSecretRequestId) {
return;
}
auto secret = GenerateSecretBytes();
auto saved = SavedCredentials();
saved.hashForAuth = _passwordCheckHash;
saved.hashForSecret = Core::ComputeSecureSecretHash(
_password.newSecureAlgo,
password);
saved.secretId = CountSecureSecretId(secret);
const auto callback = [=](const Core::CloudPasswordResult &check) {
saveSecret(check, saved, secret);
};
checkPasswordHash(_saveSecretRequestId, saved.hashForAuth, callback);
}
void FormController::saveSecret(
const Core::CloudPasswordResult &check,
const SavedCredentials &saved,
const bytes::vector &secret) {
const auto encryptedSecret = EncryptSecureSecret(
secret,
saved.hashForSecret);
using Flag = MTPDaccount_passwordInputSettings::Flag;
_saveSecretRequestId = _api.request(MTPaccount_UpdatePasswordSettings(
check.result,
MTP_account_passwordInputSettings(
MTP_flags(Flag::f_new_secure_settings),
MTPPasswordKdfAlgo(), // new_algo
MTPbytes(), // new_password_hash
MTPstring(), // hint
MTPstring(), // email
MTP_secureSecretSettings(
Core::PrepareSecureSecretAlgo(_password.newSecureAlgo),
MTP_bytes(encryptedSecret),
MTP_long(saved.secretId)))
)).done([=] {
session().data().rememberPassportCredentials(
std::move(saved),
kRememberCredentialsDelay);
_saveSecretRequestId = 0;
_secret = secret;
_secretId = saved.secretId;
//_password.salt = newPasswordSaltFull;
for (const auto &callback : base::take(_secretCallbacks)) {
callback();
}
}).fail([=](const MTP::Error &error) {
_saveSecretRequestId = 0;
if (error.type() != u"SRP_ID_INVALID"_q
|| !handleSrpIdInvalid(_saveSecretRequestId)) {
suggestRestart();
}
}).send();
}
void FormController::suggestRestart() {
_suggestingRestart = true;
_view->show(Ui::MakeConfirmBox({
.text = tr::lng_passport_restart_sure(),
.confirmed = [=] { _controller->showPassportForm(_request); },
.cancelled = [=] { cancel(); },
.confirmText = tr::lng_passport_restart(),
}));
}
void FormController::requestForm() {
if (_request.nonce.isEmpty()) {
_formRequestId = -1;
formFail(NonceNameByScope(_request.scope).toUpper() + "_EMPTY");
return;
}
_formRequestId = _api.request(MTPaccount_GetAuthorizationForm(
MTP_long(_request.botId.bare),
MTP_string(_request.scope),
MTP_string(_request.publicKey)
)).done([=](const MTPaccount_AuthorizationForm &result) {
_formRequestId = 0;
formDone(result);
}).fail([=](const MTP::Error &error) {
formFail(error.type());
}).send();
}
auto FormController::parseFiles(
const QVector<MTPSecureFile> &data,
const std::vector<EditFile> &editData) const
-> std::vector<File> {
auto result = std::vector<File>();
result.reserve(data.size());
for (const auto &file : data) {
if (auto normal = parseFile(file, editData)) {
result.push_back(std::move(*normal));
}
}
return result;
}
auto FormController::parseFile(
const MTPSecureFile &data,
const std::vector<EditFile> &editData) const
-> std::optional<File> {
switch (data.type()) {
case mtpc_secureFileEmpty:
return std::nullopt;
case mtpc_secureFile: {
const auto &fields = data.c_secureFile();
auto result = File();
result.id = fields.vid().v;
result.accessHash = fields.vaccess_hash().v;
result.size = fields.vsize().v;
result.date = fields.vdate().v;
result.dcId = fields.vdc_id().v;
result.hash = bytes::make_vector(fields.vfile_hash().v);
result.encryptedSecret = bytes::make_vector(fields.vsecret().v);
fillDownloadedFile(result, editData);
return result;
} break;
}
Unexpected("Type in FormController::parseFile.");
}
void FormController::fillDownloadedFile(
File &destination,
const std::vector<EditFile> &source) const {
const auto i = ranges::find(
source,
destination.hash,
[](const EditFile &file) { return file.fields.hash; });
if (i == source.end()) {
return;
}
destination.image = i->fields.image;
destination.downloadStatus = i->fields.downloadStatus;
if (!i->uploadData) {
return;
}
const auto &bytes = i->uploadData->bytes;
if (bytes.size() > Storage::kMaxFileInMemory) {
return;
}
session().data().cache().put(
Data::DocumentCacheKey(destination.dcId, destination.id),
Storage::Cache::Database::TaggedValue(
QByteArray(
reinterpret_cast<const char*>(bytes.data()),
bytes.size()),
Data::kImageCacheTag));
}
auto FormController::parseValue(
const MTPSecureValue &value,
const std::vector<EditFile> &editData) const -> Value {
Expects(value.type() == mtpc_secureValue);
const auto &data = value.c_secureValue();
const auto type = ConvertType(data.vtype());
auto result = Value(type);
result.submitHash = bytes::make_vector(data.vhash().v);
if (const auto secureData = data.vdata()) {
secureData->match([&](const MTPDsecureData &data) {
result.data.original = data.vdata().v;
result.data.hash = bytes::make_vector(data.vdata_hash().v);
result.data.encryptedSecret = bytes::make_vector(data.vsecret().v);
});
}
if (const auto files = data.vfiles()) {
result.files(FileType::Scan) = parseFiles(files->v, editData);
}
if (const auto translation = data.vtranslation()) {
result.files(FileType::Translation) = parseFiles(
translation->v,
editData);
}
const auto parseSpecialScan = [&](
FileType type,
const MTPSecureFile &file) {
if (auto parsed = parseFile(file, editData)) {
result.specialScans.emplace(type, std::move(*parsed));
}
};
if (const auto side = data.vfront_side()) {
parseSpecialScan(FileType::FrontSide, *side);
}
if (const auto side = data.vreverse_side()) {
parseSpecialScan(FileType::ReverseSide, *side);
}
if (const auto selfie = data.vselfie()) {
parseSpecialScan(FileType::Selfie, *selfie);
}
if (const auto plain = data.vplain_data()) {
plain->match([&](const MTPDsecurePlainPhone &data) {
result.data.parsed.fields["value"].text = qs(data.vphone());
}, [&](const MTPDsecurePlainEmail &data) {
result.data.parsed.fields["value"].text = qs(data.vemail());
});
}
return result;
}
template <typename Condition>
EditFile *FormController::findEditFileByCondition(Condition &&condition) {
for (auto &pair : _form.values) {
auto &value = pair.second;
const auto foundInList = [&](FileType type) -> EditFile* {
for (auto &scan : value.filesInEdit(type)) {
if (condition(scan)) {
return &scan;
}
}
return nullptr;
};
if (const auto result = foundInList(FileType::Scan)) {
return result;
} else if (const auto other = foundInList(FileType::Translation)) {
return other;
}
for (auto &[special, scan] : value.specialScansInEdit) {
if (condition(scan)) {
return &scan;
}
}
}
return nullptr;
}
EditFile *FormController::findEditFile(const FullMsgId &fullId) {
return findEditFileByCondition([&](const EditFile &file) {
return (file.uploadData && file.uploadData->fullId == fullId);
});
}
EditFile *FormController::findEditFile(const FileKey &key) {
return findEditFileByCondition([&](const EditFile &file) {
return (file.fields.id == key.id);
});
}
auto FormController::findFile(const FileKey &key)
-> std::pair<Value*, File*> {
const auto found = [&](const File &file) {
return (file.id == key.id);
};
for (auto &pair : _form.values) {
auto &value = pair.second;
const auto foundInList = [&](FileType type) -> File* {
for (auto &scan : value.files(type)) {
if (found(scan)) {
return &scan;
}
}
return nullptr;
};
if (const auto result = foundInList(FileType::Scan)) {
return { &value, result };
} else if (const auto other = foundInList(FileType::Translation)) {
return { &value, other };
}
for (auto &[special, scan] : value.specialScans) {
if (found(scan)) {
return { &value, &scan };
}
}
}
return { nullptr, nullptr };
}
void FormController::formDone(const MTPaccount_AuthorizationForm &result) {
if (!parseForm(result)) {
_view->showCriticalError(tr::lng_passport_form_error(tr::now));
} else {
showForm();
}
}
bool FormController::parseForm(const MTPaccount_AuthorizationForm &result) {
Expects(result.type() == mtpc_account_authorizationForm);
const auto &data = result.c_account_authorizationForm();
session().data().processUsers(data.vusers());
for (const auto &value : data.vvalues().v) {
auto parsed = parseValue(value);
const auto type = parsed.type;
const auto alreadyIt = _form.values.find(type);
if (alreadyIt != _form.values.end()) {
LOG(("API Error: Two values for type %1 in authorization form"
"%1").arg(int(type)));
return false;
}
_form.values.emplace(type, std::move(parsed));
}
if (const auto url = data.vprivacy_policy_url()) {
_form.privacyPolicyUrl = qs(*url);
}
for (const auto &required : data.vrequired_types().v) {
const auto row = CollectRequestedRow(required);
for (const auto &requested : row.values) {
const auto type = requested.type;
const auto &[i, ok] = _form.values.emplace(type, Value(type));
auto &value = i->second;
value.translationRequired = requested.translationRequired;
value.selfieRequired = requested.selfieRequired;
value.nativeNames = requested.nativeNames;
}
_form.request.push_back(row.values
| ranges::views::transform([](const RequestedValue &value) {
return value.type;
}) | ranges::to_vector);
}
if (!ValidateForm(_form)) {
return false;
}
_bot = session().data().userLoaded(_request.botId);
_form.pendingErrors = data.verrors().v;
return true;
}
void FormController::formFail(const QString &error) {
_savedPasswordValue = QByteArray();
_serviceErrorText = error;
if (!handleAppUpdateError(error)) {
_view->showCriticalError(
tr::lng_passport_form_error(tr::now) + "\n" + error);
}
}
bool FormController::handleAppUpdateError(const QString &error) {
if (error == u"APP_VERSION_OUTDATED"_q) {
_view->showUpdateAppBox();
return true;
}
return false;
}
void FormController::requestPassword() {
if (_passwordRequestId) {
return;
}
_passwordRequestId = _api.request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
_passwordRequestId = 0;
passwordDone(result);
}).fail([=](const MTP::Error &error) {
formFail(error.type());
}).send();
}
void FormController::passwordDone(const MTPaccount_Password &result) {
Expects(result.type() == mtpc_account_password);
const auto changed = applyPassword(result.c_account_password());
if (changed) {
showForm();
}
shortPollEmailConfirmation();
}
void FormController::shortPollEmailConfirmation() {
if (_password.unconfirmedPattern.isEmpty()) {
_shortPollTimer.cancel();
return;
}
_shortPollTimer.callOnce(kShortPollTimeout);
}
void FormController::showForm() {
if (_formRequestId || _passwordRequestId) {
return;
} else if (!_bot) {
formFail(Lang::Hard::NoAuthorizationBot());
return;
}
if (_password.unknownAlgo
|| v::is_null(_password.newAlgo)
|| v::is_null(_password.newSecureAlgo)) {
_view->showUpdateAppBox();
return;
} else if (_password.request) {
if (!_savedPasswordValue.isEmpty()) {
submitPassword(base::duplicate(_savedPasswordValue));
} else if (const auto saved = session().data().passportCredentials()) {
checkSavedPasswordSettings(*saved);
} else {
_view->showAskPassword();
}
} else {
_view->showNoPassword();
}
}
bool FormController::applyPassword(const MTPDaccount_password &result) {
auto settings = PasswordSettings();
settings.hint = qs(result.vhint().value_or_empty());
settings.hasRecovery = result.is_has_recovery();
settings.notEmptyPassport = result.is_has_secure_values();
settings.request = Core::ParseCloudPasswordCheckRequest(result);
settings.unknownAlgo = result.vcurrent_algo() && !settings.request;
settings.unconfirmedPattern =
qs(result.vemail_unconfirmed_pattern().value_or_empty());
settings.newAlgo = Core::ValidateNewCloudPasswordAlgo(
Core::ParseCloudPasswordAlgo(result.vnew_algo()));
settings.newSecureAlgo = Core::ValidateNewSecureSecretAlgo(
Core::ParseSecureSecretAlgo(result.vnew_secure_algo()));
settings.pendingResetDate = result.vpending_reset_date().value_or_empty();
base::RandomAddSeed(bytes::make_span(result.vsecure_random().v));
return applyPassword(std::move(settings));
}
bool FormController::applyPassword(PasswordSettings &&settings) {
if (_password != settings) {
_password = std::move(settings);
return true;
}
return false;
}
void FormController::cancel() {
if (!_submitSuccess && _serviceErrorText.isEmpty()) {
_view->show(Ui::MakeConfirmBox({
.text = tr::lng_passport_stop_sure(),
.confirmed = [=] { cancelSure(); },
.cancelled = [=](Fn<void()> close) { cancelAbort(); close(); },
.confirmText = tr::lng_passport_stop(),
}));
} else {
cancelSure();
}
}
void FormController::cancelAbort() {
if (_cancelled || _submitSuccess) {
return;
} else if (_suggestingRestart) {
suggestRestart();
}
}
void FormController::cancelSure() {
if (!_cancelled) {
_cancelled = true;
if (!_request.callbackUrl.isEmpty()
&& (_serviceErrorText.isEmpty()
|| ForwardServiceErrorRequired(_serviceErrorText))) {
const auto url = qthelp::url_append_query_or_hash(
_request.callbackUrl,
(_submitSuccess
? "tg_passport=success"
: (_serviceErrorText.isEmpty()
? "tg_passport=cancel"
: "tg_passport=error&error=" + _serviceErrorText)));
UrlClickHandler::Open(url);
}
const auto timeout = _view->closeGetDuration();
base::call_delayed(timeout, this, [=] {
_controller->clearPassportForm();
});
}
}
rpl::lifetime &FormController::lifetime() {
return _lifetime;
}
FormController::~FormController() = default;
} // namespace Passport