/*
* Copyright (c) 2012-2015 Christian Surlykke
* 2017 bwLehrpool project
*
* Based on qt-lightdm-greeter, stripped down to fit
* our specific needs.
* It is distributed under the LGPL 2.1 or later license.
* Please refer to the LICENSE file for a copy of the license.
*/
#include <QTextStream>
#include "x11util.h"
#include "loginform.h"
#include "ui_loginform.h"
#include "settings.h"
#include "global.h"
#include "namereplace.h"
#include "loginrpc.h"
#include "qrlogin.h"
#undef KeyPress
#undef KeyRelease
#undef FocusIn
#undef FocusOut
#include <QAbstractListModel>
#include <QModelIndex>
#include <QPixmap>
#include <QIcon>
#include <QMessageBox>
#include <QMenu>
#include <QListView>
#include <QSvgRenderer>
#include <QX11Info>
#include "webview.h"
#include <QSizePolicy>
#include <iostream>
void createSimpleBackground();
LoginForm::LoginForm(QWidget *parent) :
QWidget(parent),
ui(new Ui::LoginForm),
browser(nullptr),
clearMsg(false),
capsOn(-1),
pageCount(0),
qrcode(nullptr),
qrlogin(nullptr)
{
ui->setupUi(this);
origSize = shibSize = sizeHint();
if (this->parentWidget() != nullptr) {
shibSize.setWidth(qMin(1000, int(this->parentWidget()->width() * .75f) ));
shibSize.setHeight(qMin(700, int(this->parentWidget()->height() * .75f) ));
} else {
shibSize.rwidth() += 350;
shibSize.rheight() += 250;
}
if (!Settings::shibSessionEnabled() && !Settings::guestSessionEnabled()) {
origSize.rheight() -= 64;
}
initialize();
this->updateGeometry();
int port = Settings::rpcPort();
if (port != 0) {
auto *car = new LoginRpc(port, this);
connect(car, &LoginRpc::loginRequest, [this](const QString &username, const QString &password, const QString &resolution) {
if (username.isEmpty() || password.isEmpty())
return;
ui->userInput->setText(username);
ui->passwordInput->setText(password);
this->startFormBasedAuthentication();
});
}
connect(ui->loginChooser, &QStackedWidget::currentChanged, this, &LoginForm::setBrowserSize);
}
LoginForm::~LoginForm()
{
delete ui;
}
void LoginForm::setFocus(Qt::FocusReason reason)
{
if (ui->userInput->text().isEmpty()) {
ui->userInput->setFocus(reason);
} else {
ui->passwordInput->setFocus(reason);
}
}
void LoginForm::resizeEvent(QResizeEvent *e)
{
if (this->parentWidget() != nullptr) {
shibSize.setWidth(qMin(1000, int(this->parentWidget()->width() * .75f) ));
shibSize.setHeight(qMin(700, int(this->parentWidget()->height() * .75f) ));
}
const QSize *size = nullptr;
if (ui->loginChooser->currentWidget() == ui->shibPage) {
size = &shibSize;
} else {
size = &origSize;
}
if (*size != e->size()) {
e->ignore();
setMinimumSize(*size);
setFixedSize(*size);
setBaseSize(*size);
int pw = 0, ph = 0;
if (this->parentWidget() != nullptr) {
this->parentWidget()->pos();
pw = (this->parentWidget()->width() - size->width()) / 2;
ph = (this->parentWidget()->height() - size->height()) / 2;
}
setGeometry(pw, pw, size->width(), size->height());
emit resized();
setBrowserSize();
return;
}
QWidget::resizeEvent(e);
}
void LoginForm::initialize()
{
QString path = Settings::miniIconFile();
QPixmap pixmap;
if (!path.isEmpty()) {
// Try to get the default size, in case this is an SVG
QSize size = QSvgRenderer(path).defaultSize();
if (!size.isValid()) { // if not, use maximum of destination
size = ui->iconLabel->maximumSize();
} else {
size = size.boundedTo(ui->iconLabel->maximumSize()).expandedTo(ui->iconLabel->minimumSize());
}
pixmap = QIcon(path).pixmap(size);
}
if (pixmap.isNull() || pixmap.width() < 10) {
// fallback to built-in bwlp logo
pixmap = QIcon(QLatin1String(":/resources/bwlp.svg")).pixmap(ui->iconLabel->size());
}
ui->iconLabel->setPixmap(pixmap);
ui->iconLabel->setFixedSize(pixmap.size());
ui->frame->setFixedHeight(pixmap.height() + 2);
ui->leaveComboBox->setView(new QListView()); // This is required to get the stylesheet to apply
cancelLoginTimer.setInterval(20000);
cancelLoginTimer.setSingleShot(true);
connect(&cancelLoginTimer, &QTimer::timeout, this, &LoginForm::cancelLogin);
hideMessageTimer.setInterval(10000);
hideMessageTimer.setSingleShot(true);
connect(&hideMessageTimer, &QTimer::timeout, this, [this]() {
this->hideMessage();
this->capsOn = -1;
this->checkCaps();
});
// timer to reset the form to its original state
if (Settings::resetForm() > 0) {
connect(&resetFormTimer, &QTimer::timeout, [this]() {
if (ui->loginChooser->currentWidget() == ui->qrPage)
return;
int idleTime = static_cast<int>(getIdleTime(QX11Info::display()));
int remaining = Settings::resetForm() * 1000 - idleTime;
if (remaining <= 0) {
resetForm();
remaining = Settings::resetForm() * 1000;
}
resetFormTimer.start(remaining + 100);
});
resetFormTimer.start(Settings::resetForm() * 1000);
}
ui->backButton->hide();
if (Settings::guestSessionEnabled()) {
pageCount++;
if (!Settings::guestSessionButtonText().isEmpty()) {
ui->guestButton->setText(Settings::guestSessionButtonText());
}
if (!Settings::userSessionButtonText().isEmpty()) {
ui->loginButton->setText(Settings::userSessionButtonText());
}
if (!Settings::guestSessionStartText().isEmpty()) {
ui->guestStartLabel->setText(Settings::guestSessionStartText());
}
if (!Settings::guestSessionStartButtonText().isEmpty()) {
ui->guestStartButton->setText(Settings::guestSessionStartButtonText());
}
connect(ui->guestButton, &QAbstractButton::released, this, [this]() {
ui->loginChooser->setCurrentWidget(ui->guestPage);
});
} else {
ui->guestButton->hide();
}
if (Settings::shibSessionEnabled()) {
pageCount += 2; // Fake this so we always return on timeout;
// otherwise, the browser session could expire after some time, breaking
// the login process.
if (!Settings::shibSessionButtonText().isEmpty()) {
ui->shibButton->setText(Settings::shibSessionButtonText());
}
connect(ui->shibButton, &QAbstractButton::released, this, &LoginForm::showShibWindow);
// Reduce minimum size of hostname/icon bar
ui->frame->setMinimumSize(10, 30);
ui->frame->setMaximumSize(99999, ui->iconLabel->height());
ui->frame->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
ui->loginChooser->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
} else {
ui->shibButton->hide();
}
if (Settings::qrSessionEnabled()) {
pageCount += 2; // Fake this so we always return on timeout;
// otherwise, the qr code could expire after some time, breaking
// the login process.
if (!Settings::qrSessionButtonText().isEmpty()) {
ui->qrButton->setText(Settings::qrSessionButtonText());
}
connect(ui->qrButton, &QAbstractButton::released, this, &LoginForm::showQrWindow);
} else {
ui->qrButton->hide();
}
if (Settings::userSessionEnabled()) {
pageCount++;
} else {
ui->loginButton->hide();
}
if (pageCount > 1) {
connect(ui->backButton, &QAbstractButton::released, this, [this]() {
resetForm();
});
ui->loginChooser->setCurrentWidget(ui->welcomePage);
connect(ui->loginChooser, &QStackedWidget::currentChanged, this, [this]() {
if (ui->loginChooser->currentWidget() == ui->welcomePage) {
ui->backButton->hide();
} else {
ui->backButton->show();
}
});
connect(ui->loginButton, &QAbstractButton::released, this, [this]() {
ui->loginChooser->setCurrentWidget(ui->loginPage);
ui->userInput->setFocus();
});
} else if (Settings::shibSessionEnabled()) {
this->showShibWindow();
} else if (Settings::guestSessionEnabled()) {
ui->loginChooser->setCurrentWidget(ui->guestPage);
} else {
ui->loginChooser->setCurrentWidget(ui->loginPage);
}
if (!Global::testMode()) {
ui->hostnameLabel->setText(Global::greeter()->hostname());
if(!Settings::usernamePlaceholder().isEmpty()) {
ui->userInput->setPlaceholderText(Settings::usernamePlaceholder());
}
if(!Settings::passwordPlaceholder().isEmpty()) {
ui->passwordInput->setPlaceholderText(Settings::passwordPlaceholder());
}
addLeaveEntry(Global::power()->canShutdown(), "system-shutdown", tr("Shutdown"), "shutdown");
addLeaveEntry(Global::power()->canRestart(), "system-reboot", tr("Restart"), "restart");
connect(ui->leaveComboBox, QOverload<int>::of(&QComboBox::activated), this, &LoginForm::leaveDropDownActivated);
connect(Global::greeter(), &QLightDM::Greeter::showPrompt, this, &LoginForm::onPrompt);
connect(Global::greeter(), &QLightDM::Greeter::showMessage, this, &LoginForm::onMessage);
connect(Global::greeter(), &QLightDM::Greeter::authenticationComplete, this, &LoginForm::onAuthenticationComplete);
if (Settings::guestSessionEnabled()) {
connect(ui->guestStartButton, &QAbstractButton::released, this, []() {
if (!Global::autoLoginGuest()) {
std::cerr << "Guest login failed..." << std::endl;
// TODO warn user about it
}
});
}
}
// Load regexp for name substitution
NameReplace::loadSubs();
ui->leaveComboBox->setDisabled(ui->leaveComboBox->count() <= 1);
ui->passwordInput->clear();
this->layout()->setSizeConstraint(QLayout::SetFixedSize);
this->installEventFilter(this);
checkCaps();
}
void LoginForm::showShibWindow() {
if (browser == nullptr) {
browser = new WebView(ui->shibPage);
ui->verticalLayout_5->addWidget(browser);
connect(browser, &WebView::triggerReset, [this](const QString &message) {
this->showMessage(message, true);
if (pageCount == 1) {
showShibWindow();
} else {
ui->loginChooser->setCurrentWidget(ui->welcomePage);
}
});
connect(browser, &WebView::startAuthentication, this, &LoginForm::startAuthAs);
}
browser->reset(Settings::shibUrl());
ui->loginChooser->setCurrentWidget(ui->shibPage);
setBrowserSize();
}
void LoginForm::showQrWindow() {
if (qrcode == nullptr) {
qrcode = new QLabel(this);
ui->vlQrCode->addWidget(qrcode);
qrcode->setBackgroundRole(QPalette::Shadow);
//connect(browser, &WebView::startAuthentication, this, &LoginForm::startAuthAs);
}
if (qrlogin != nullptr) {
disconnect(qrlogin);
qrlogin->deleteLater();
}
qrlogin = new QrLogin(this);
connect(qrlogin, &QrLogin::startAuthentication, this, &LoginForm::startAuthAs);
connect(qrlogin, &QrLogin::updateStatus, this, &LoginForm::showLowPrioMessage);
connect(qrlogin, &QrLogin::triggerReset, [this](const QString &message) {
this->showMessage(message, true);
ui->loginChooser->setCurrentWidget(ui->welcomePage);
});
QPixmap pm(20, 20);
pm.fill(Qt::red);
qrcode->setPixmap(pm);
qrlogin->loadQrCode(qrcode);
ui->loginChooser->setCurrentWidget(ui->qrPage);
}
void LoginForm::checkCaps()
{
unsigned int mask = getKeyMask(QX11Info::display());
int caps = (mask & 1) == 1;
if (caps != capsOn) {
capsOn = caps;
QString message(tr("!! CAPS LOCK ACTIVE !!"));
if (caps) {
ui->messageLabel->setProperty("caps", message);
showMessage(message, false);
} else if (ui->messageLabel->property("caps").toString() == message) {
hideMessage();
}
}
}
void LoginForm::startFormBasedAuthentication()
{
QString username(ui->userInput->text().trimmed());
NameReplace::replace(username);
std::cerr << "Logging in as " << username.toStdString() << std::endl;
if (ui->userInput->text().isEmpty()) {
ui->userInput->setFocus();
return;
}
if (ui->passwordInput->text().isEmpty()) {
ui->passwordInput->setFocus();
return;
}
startAuthAs(username, ui->passwordInput->text());
ui->passwordInput->setText("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
ui->passwordInput->clear();
}
void LoginForm::startAuthAs(const QString &user, const QString &pass)
{
if (Global::testMode()) {
showMessage(QLatin1String("Test mode..."), true);
return;
}
if (Global::greeter()->inAuthentication()) {
Global::greeter()->cancelAuthentication();
}
showMessage(tr("Logging in..."), false);
clearMsg = false;
enableInputs(false);
cancelLoginTimer.start();
password = pass;
Global::greeter()->authenticate(user);
}
void LoginForm::onPrompt(QString prompt, QLightDM::Greeter::PromptType /* promptType */)
{
std::cerr << "Prompt: " << prompt.toStdString() << std::endl;
Global::greeter()->respond(password);
password = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx";
password.clear();
}
void LoginForm::leaveDropDownActivated(int index)
{
QString actionName = ui->leaveComboBox->itemData(index).toString();
if (actionName == "shutdown") Global::power()->shutdown();
else if (actionName == "restart") Global::power()->restart();
else if (actionName == "hibernate") Global::power()->hibernate();
else if (actionName == "suspend") Global::power()->suspend();
}
void LoginForm::onMessage(QString message, QLightDM::Greeter::MessageType type)
{
bool err = type == QLightDM::Greeter::MessageType::MessageTypeError;
if (err) {
ui->passwordInput->clear();
}
std::cerr << "Message: " << message.toStdString() << std::endl;
showMessage(QLatin1String("[G] ") + message, err);
clearMsg = true;
}
void LoginForm::addLeaveEntry(bool canDo, QString iconName, QString text, QString actionName)
{
if (canDo) {
ui->leaveComboBox->addItem(QIcon::fromTheme(iconName), text, actionName);
}
}
void LoginForm::onAuthenticationComplete()
{
if (Global::greeter()->isAuthenticated()) {
std::cerr << "Auth complete, start session" << std::endl;
showMessage(tr("Starting session..."), false);
createSimpleBackground();
if (Global::startSession()) {
cancelLoginTimer.stop();
} else {
showMessage(tr("Cannot open session"), true);
clearMsg = true;
}
} else {
std::cerr << "Auth failed, cancelling..." << std::endl;
cancelLogin();
}
}
void LoginForm::cancelLogin()
{
std::cerr << "Cancel login" << std::endl;
if (Global::greeter()->inAuthentication()) {
std::cerr << "Was in authentication" << std::endl;
Global::greeter()->cancelAuthentication();
}
if (ui->loginChooser->currentWidget() != ui->loginPage && pageCount > 1) {
ui->loginChooser->setCurrentWidget(ui->welcomePage);
}
cancelLoginTimer.stop();
ui->passwordInput->clear();
enableInputs(true);
if (!clearMsg) {
showMessage(tr("Login failed"), true);
} else {
ui->messageLabel->setStyleSheet("color:red");
}
ui->passwordInput->setFocus();
clearMsg = true;
}
void LoginForm::enableInputs(bool enable)
{
ui->userInput->setEnabled(enable);
ui->passwordInput->setEnabled(enable);
ui->backButton->setEnabled(enable);
ui->guestButton->setEnabled(enable);
ui->shibButton->setEnabled(enable);
ui->qrButton->setEnabled(enable);
if (browser != nullptr) {
browser->setEnabled(enable);
}
}
void LoginForm::showLowPrioMessage(QString message)
{
if (!ui->messageLabel->text().isEmpty() && !ui->messageLabel->styleSheet().isEmpty())
return;
showMessage(message, false);
}
void LoginForm::showMessage(QString message, bool error)
{
hideMessageTimer.stop();
ui->messageLabel->setText(message);
if (error) {
ui->messageLabel->setStyleSheet("color:red");
hideMessageTimer.start();
} else {
ui->messageLabel->setStyleSheet("");
}
}
void LoginForm::hideMessage()
{
hideMessageTimer.stop();
ui->messageLabel->clear();
}
void LoginForm::keyPressEvent(QKeyEvent *event)
{
if (clearMsg) {
clearMsg = false;
hideMessage();
}
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
if (!ui->userInput->isEnabled()) {
// Ignore if auth in progress
return;
}
if (ui->userInput->hasFocus()) {
ui->passwordInput->setFocus();
return;
}
if (ui->passwordInput->hasFocus()) {
startFormBasedAuthentication();
return;
}
}
if (Settings::guestSessionEnabled() && event->key() == Qt::Key_Escape) {
resetForm();
}
// Fallback: Passthrough
QWidget::keyPressEvent(event);
}
void LoginForm::resetForm()
{
std::cerr << "PageCount: " << pageCount << std::endl;
if (ui->loginChooser->currentWidget() == ui->qrPage && qrlogin != nullptr) {
qrlogin->abort();
hideMessage();
}
if (pageCount > 1) {
ui->loginChooser->setCurrentWidget(ui->welcomePage);
}
ui->passwordInput->clear();
ui->userInput->clear();
if (pageCount == 1) {
if (Settings::userSessionEnabled()) {
ui->userInput->setFocus();
}
}
}
bool LoginForm::eventFilter(QObject *object, QEvent *event)
{
if (event->type() == QEvent::KeyRelease) {
checkCaps();
}
return false;
}
void LoginForm::setBrowserSize()
{
auto s = ui->shibPage->size();
auto *f = &shibSize;
if (ui->loginChooser->currentWidget() != ui->shibPage) {
f = &origSize;
s = QSize(50, 50);
}
if (browser != nullptr) {
browser->setFixedSize(s);
browser->setGeometry(QRect(QPoint(0, 0), s));
}
QTimer::singleShot(10, [this, f]() {
int pw = 0, ph = 0;
if (this->parentWidget() != nullptr) {
this->parentWidget()->pos();
pw = (this->parentWidget()->width() - f->width()) / 2;
ph = (this->parentWidget()->height() - f->height()) / 2;
}
this->resize(*f);
this->setFixedSize(*f);
this->setGeometry(pw, ph, f->width(), f->height());
auto s = ui->shibPage->size();
std::cerr << "Delayed resize to " << s.width() << " " << s.height() << std::endl;
if (browser != nullptr) {
browser->setFixedSize(s);
browser->setGeometry(QRect(QPoint(0, 0), s));
}
emit resized();
});
}