304 lines
11 KiB
C++
304 lines
11 KiB
C++
/*
|
|
* Kaidan - A user-friendly XMPP client for every device!
|
|
*
|
|
* Copyright (C) 2016-2021 Kaidan developers and contributors
|
|
* (see the LICENSE file for a full list of copyright authors)
|
|
*
|
|
* Kaidan is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* In addition, as a special exception, the author of Kaidan gives
|
|
* permission to link the code of its release with the OpenSSL
|
|
* project's "OpenSSL" library (or with modified versions of it that
|
|
* use the same license as the "OpenSSL" library), and distribute the
|
|
* linked executables. You must obey the GNU General Public License in
|
|
* all respects for all of the code used other than "OpenSSL". If you
|
|
* modify this file, you may extend this exception to your version of
|
|
* the file, but you are not obligated to do so. If you do not wish to
|
|
* do so, delete this exception statement from your version.
|
|
*
|
|
* Kaidan is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with Kaidan. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "RegistrationManager.h"
|
|
|
|
// QXmpp
|
|
#include <QXmppBitsOfBinaryDataList.h>
|
|
#include <QXmppRegistrationManager.h>
|
|
// Kaidan
|
|
#include "AccountManager.h"
|
|
#include "BitsOfBinaryImageProvider.h"
|
|
#include "ClientWorker.h"
|
|
#include "Globals.h"
|
|
#include "Kaidan.h"
|
|
#include "RegistrationDataFormModel.h"
|
|
#include "ServerFeaturesCache.h"
|
|
|
|
RegistrationManager::RegistrationManager(ClientWorker *clientWorker, QXmppClient *client, QObject *parent)
|
|
: QObject(parent),
|
|
m_clientWorker(clientWorker),
|
|
m_client(client),
|
|
m_manager(new QXmppRegistrationManager),
|
|
m_dataFormModel(new RegistrationDataFormModel())
|
|
{
|
|
client->addExtension(m_manager);
|
|
|
|
connect(m_manager, &QXmppRegistrationManager::supportedByServerChanged, this, &RegistrationManager::handleInBandRegistrationSupportedChanged);
|
|
|
|
// account creation
|
|
connect(this, &RegistrationManager::sendRegistrationFormRequested,
|
|
this, &RegistrationManager::sendRegistrationForm);
|
|
connect(m_manager, &QXmppRegistrationManager::registrationFormReceived, this, &RegistrationManager::handleRegistrationFormReceived);
|
|
connect(m_manager, &QXmppRegistrationManager::registrationSucceeded, this, &RegistrationManager::handleRegistrationSucceeded);
|
|
connect(m_manager, &QXmppRegistrationManager::registrationFailed, this, &RegistrationManager::handleRegistrationFailed);
|
|
|
|
// account deletion
|
|
connect(m_manager, &QXmppRegistrationManager::accountDeletionFailed, m_clientWorker, &ClientWorker::handleAccountDeletionFromServerFailed);
|
|
connect(m_manager, &QXmppRegistrationManager::accountDeleted, m_clientWorker, &ClientWorker::handleAccountDeletedFromServer);
|
|
|
|
// password change
|
|
connect(this, &RegistrationManager::changePasswordRequested,
|
|
this, &RegistrationManager::changePassword);
|
|
connect(m_manager, &QXmppRegistrationManager::passwordChanged, this, &RegistrationManager::handlePasswordChanged);
|
|
connect(m_manager, &QXmppRegistrationManager::passwordChangeFailed, this, &RegistrationManager::handlePasswordChangeFailed);
|
|
}
|
|
|
|
void RegistrationManager::setRegisterOnConnectEnabled(bool registerOnConnect)
|
|
{
|
|
m_manager->setRegisterOnConnectEnabled(registerOnConnect);
|
|
}
|
|
|
|
void RegistrationManager::deleteAccount()
|
|
{
|
|
m_manager->deleteAccount();
|
|
}
|
|
|
|
void RegistrationManager::sendRegistrationForm()
|
|
{
|
|
if (m_dataFormModel->isFakeForm()) {
|
|
QXmppRegisterIq iq;
|
|
iq.setUsername(m_dataFormModel->extractUsername());
|
|
iq.setPassword(m_dataFormModel->extractPassword());
|
|
iq.setEmail(m_dataFormModel->extractEmail());
|
|
|
|
m_manager->setRegistrationFormToSend(iq);
|
|
} else {
|
|
m_manager->setRegistrationFormToSend(m_dataFormModel->form());
|
|
}
|
|
|
|
m_clientWorker->connectToRegister();
|
|
}
|
|
|
|
void RegistrationManager::changePassword(const QString &newPassword)
|
|
{
|
|
m_clientWorker->startTask(
|
|
[=] () {
|
|
m_manager->changePassword(newPassword);
|
|
}
|
|
);
|
|
}
|
|
|
|
void RegistrationManager::handleInBandRegistrationSupportedChanged()
|
|
{
|
|
if (m_client->isConnected()) {
|
|
m_clientWorker->caches()->serverFeaturesCache->setInBandRegistrationSupported(m_manager->supportedByServer());
|
|
}
|
|
}
|
|
|
|
void RegistrationManager::handleRegistrationFormReceived(const QXmppRegisterIq &iq)
|
|
{
|
|
m_client->disconnectFromServer();
|
|
|
|
bool isFakeForm;
|
|
QXmppDataForm newDataForm = extractFormFromRegisterIq(iq, isFakeForm);
|
|
|
|
// If there is no registration data form, try to use an out-of-band URL.
|
|
if (newDataForm.fields().isEmpty()) {
|
|
#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 5, 0)
|
|
// If there is a standardized out-of-band URL, use that.
|
|
if (!iq.outOfBandUrl().isEmpty()) {
|
|
emit Kaidan::instance()->registrationOutOfBandUrlReceived(iq.outOfBandUrl());
|
|
return;
|
|
}
|
|
#endif
|
|
// Try to find an out-of-band URL within the instructions element.
|
|
// Most servers include a text with a link to the website.
|
|
const auto words = iq.instructions().split(u' ');
|
|
for (const auto &instructionPart : words) {
|
|
if (instructionPart.startsWith(u"https://")) {
|
|
emit Kaidan::instance()->registrationOutOfBandUrlReceived(instructionPart);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If no URL has been found in the instructions, there is a
|
|
// problem with the server.
|
|
emit m_clientWorker->connectionErrorChanged(ClientWorker::RegistrationUnsupported);
|
|
return;
|
|
}
|
|
|
|
copyUserDefinedValuesToNewForm(m_dataFormModel->form(), newDataForm);
|
|
cleanUpLastForm();
|
|
|
|
m_dataFormModel = new RegistrationDataFormModel(newDataForm);
|
|
m_dataFormModel->setIsFakeForm(isFakeForm);
|
|
// Move to the main thread, so QML can connect signals to the model.
|
|
m_dataFormModel->moveToThread(Kaidan::instance()->thread());
|
|
|
|
// Add the attached Bits of Binary data to the corresponding image provider.
|
|
const auto bobDataList = iq.bitsOfBinaryData();
|
|
for (const auto &bobData : bobDataList) {
|
|
BitsOfBinaryImageProvider::instance()->addImage(bobData);
|
|
m_contentIdsToRemove << bobData.cid();
|
|
}
|
|
|
|
emit Kaidan::instance()->registrationFormReceived(m_dataFormModel);
|
|
}
|
|
|
|
void RegistrationManager::handleRegistrationSucceeded()
|
|
{
|
|
AccountManager::instance()->setJid(m_dataFormModel->extractUsername().append('@').append(m_client->configuration().domain()));
|
|
AccountManager::instance()->setPassword(m_dataFormModel->extractPassword());
|
|
|
|
m_client->disconnectFromServer();
|
|
setRegisterOnConnectEnabled(false);
|
|
m_clientWorker->logIn();
|
|
|
|
cleanUpLastForm();
|
|
m_dataFormModel = new RegistrationDataFormModel();
|
|
}
|
|
|
|
void RegistrationManager::handleRegistrationFailed(const QXmppStanza::Error &error)
|
|
{
|
|
m_client->disconnectFromServer();
|
|
setRegisterOnConnectEnabled(false);
|
|
|
|
RegistrationError registrationError = RegistrationError::UnknownError;
|
|
|
|
switch(error.type()) {
|
|
case QXmppStanza::Error::Cancel:
|
|
if (error.condition() == QXmppStanza::Error::FeatureNotImplemented)
|
|
registrationError = RegistrationError::InBandRegistrationNotSupported;
|
|
else if (error.condition() == QXmppStanza::Error::Conflict)
|
|
registrationError = RegistrationError::UsernameConflict;
|
|
else if (error.condition() == QXmppStanza::Error::NotAllowed && error.text().contains("captcha", Qt::CaseInsensitive))
|
|
registrationError = RegistrationError::CaptchaVerificationFailed;
|
|
break;
|
|
case QXmppStanza::Error::Modify:
|
|
if (error.condition() == QXmppStanza::Error::NotAcceptable) {
|
|
// TODO: Check error text in English (needs QXmpp change)
|
|
if (error.text().contains("password", Qt::CaseInsensitive) && (error.text().contains("weak", Qt::CaseInsensitive) || error.text().contains("short", Qt::CaseInsensitive)))
|
|
registrationError = RegistrationError::PasswordTooWeak;
|
|
else if (error.text().contains("ip", Qt::CaseInsensitive) || error.text().contains("quickly", Qt::CaseInsensitive)
|
|
)
|
|
registrationError = RegistrationError::TemporarilyBlocked;
|
|
else
|
|
registrationError = RegistrationError::RequiredInformationMissing;
|
|
} else if (error.condition() == QXmppStanza::Error::BadRequest && error.text().contains("captcha", Qt::CaseInsensitive)) {
|
|
registrationError = RegistrationError::CaptchaVerificationFailed;
|
|
}
|
|
break;
|
|
default:
|
|
#if QXMPP_VERSION == QT_VERSION_CHECK(1, 2, 0)
|
|
// Workaround: Catch an error which is wrongly emitted by QXmpp
|
|
// v1.2.0 although the registration was successful.
|
|
if (error.text().isEmpty())
|
|
return;
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
emit Kaidan::instance()->registrationFailed(quint8(registrationError), error.text());
|
|
}
|
|
|
|
void RegistrationManager::handlePasswordChanged(const QString &newPassword)
|
|
{
|
|
AccountManager::instance()->setPassword(newPassword);
|
|
AccountManager::instance()->storePassword();
|
|
AccountManager::instance()->setHasNewCredentials(false);
|
|
m_clientWorker->finishTask();
|
|
emit Kaidan::instance()->passwordChangeSucceeded();
|
|
}
|
|
|
|
void RegistrationManager::handlePasswordChangeFailed(const QXmppStanza::Error &error)
|
|
{
|
|
emit Kaidan::instance()->passwordChangeFailed(error.text());
|
|
m_clientWorker->finishTask();
|
|
}
|
|
|
|
QXmppDataForm RegistrationManager::extractFormFromRegisterIq(const QXmppRegisterIq& iq, bool &isFakeForm)
|
|
{
|
|
QXmppDataForm newDataForm = iq.form();
|
|
if (newDataForm.fields().isEmpty()) {
|
|
// This is a hack, so we only need to implement one way of registering in QML.
|
|
// A 'fake' data form model is created with a username and password field.
|
|
isFakeForm = true;
|
|
|
|
if (!iq.username().isNull()) {
|
|
QXmppDataForm::Field field;
|
|
field.setKey(QStringLiteral("username"));
|
|
field.setRequired(true);
|
|
field.setType(QXmppDataForm::Field::TextSingleField);
|
|
newDataForm.fields().append(field);
|
|
}
|
|
|
|
if (!iq.password().isNull()) {
|
|
QXmppDataForm::Field field;
|
|
field.setKey(QStringLiteral("password"));
|
|
field.setRequired(true);
|
|
field.setType(QXmppDataForm::Field::TextPrivateField);
|
|
newDataForm.fields().append(field);
|
|
}
|
|
|
|
if (!iq.email().isNull()) {
|
|
QXmppDataForm::Field field;
|
|
field.setKey(QStringLiteral("email"));
|
|
field.setRequired(true);
|
|
field.setType(QXmppDataForm::Field::TextPrivateField);
|
|
newDataForm.fields().append(field);
|
|
}
|
|
} else {
|
|
isFakeForm = false;
|
|
}
|
|
|
|
return newDataForm;
|
|
}
|
|
|
|
void RegistrationManager::copyUserDefinedValuesToNewForm(const QXmppDataForm &oldForm, QXmppDataForm& newForm)
|
|
{
|
|
// Copy values from the last form.
|
|
const QList<QXmppDataForm::Field> oldFields = oldForm.fields();
|
|
for (const auto &field : oldFields) {
|
|
// Only copy fields which:
|
|
// * are required,
|
|
// * are visible to the user
|
|
// * do not have a media element (e.g. CAPTCHA)
|
|
if (field.isRequired() && field.type() != QXmppDataForm::Field::HiddenField &&
|
|
field.mediaSources().isEmpty()) {
|
|
for (auto &fieldFromNewForm : newForm.fields()) {
|
|
if (fieldFromNewForm.key() == field.key()) {
|
|
fieldFromNewForm.setValue(field.value());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void RegistrationManager::cleanUpLastForm()
|
|
{
|
|
delete m_dataFormModel;
|
|
|
|
// Remove content IDs from the last form.
|
|
for (const auto &cid : qAsConst(m_contentIdsToRemove))
|
|
BitsOfBinaryImageProvider::instance()->removeImage(cid);
|
|
}
|