File indexing completed on 2024-05-19 05:54:13

0001 /*  This file was part of the KDE libraries
0002 
0003     SPDX-FileCopyrightText: 2021 Tomaz Canabrava <tcanabrava@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "sshmanagerpluginwidget.h"
0009 
0010 #include "ProcessInfo.h"
0011 #include "konsoledebug.h"
0012 #include "session/Session.h"
0013 #include "session/SessionController.h"
0014 
0015 #include "sshmanagermodel.h"
0016 #include "terminalDisplay/TerminalDisplay.h"
0017 
0018 #include "profile/ProfileModel.h"
0019 
0020 #include "sshmanagerfiltermodel.h"
0021 #include "sshmanagerplugin.h"
0022 #include "ui_sshwidget.h"
0023 
0024 #include <KLocalizedString>
0025 #include <KMessageBox>
0026 #include <kwidgetsaddons_version.h>
0027 
0028 #include <QAction>
0029 #include <QFileDialog>
0030 #include <QIntValidator>
0031 #include <QItemSelectionModel>
0032 #include <QMenu>
0033 #include <QPoint>
0034 #include <QRegularExpression>
0035 #include <QRegularExpressionValidator>
0036 
0037 #include <QSettings>
0038 #include <QSortFilterProxyModel>
0039 
0040 #include <QStandardPaths>
0041 
0042 struct SSHManagerTreeWidget::Private {
0043     SSHManagerModel *model = nullptr;
0044     SSHManagerFilterModel *filterModel = nullptr;
0045     Konsole::SessionController *controller = nullptr;
0046     bool isSetup = false;
0047 };
0048 
0049 SSHManagerTreeWidget::SSHManagerTreeWidget(QWidget *parent)
0050     : QWidget(parent)
0051     , ui(std::make_unique<Ui::SSHTreeWidget>())
0052     , d(std::make_unique<SSHManagerTreeWidget::Private>())
0053 {
0054     ui->setupUi(this);
0055     ui->errorPanel->hide();
0056 
0057     d->filterModel = new SSHManagerFilterModel(this);
0058 
0059     // https://stackoverflow.com/questions/1418423/the-hostname-regex
0060     const auto hostnameRegex =
0061         QRegularExpression(QStringLiteral(R"(^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$)"));
0062 
0063     const auto *hostnameValidator = new QRegularExpressionValidator(hostnameRegex, this);
0064     ui->hostname->setValidator(hostnameValidator);
0065 
0066     // System and User ports see:
0067     // https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
0068     const auto *portValidator = new QIntValidator(0, 49151, this);
0069     ui->port->setValidator(portValidator);
0070 
0071     connect(ui->newSSHConfig, &QPushButton::clicked, this, &SSHManagerTreeWidget::showInfoPane);
0072     connect(ui->btnCancel, &QPushButton::clicked, this, &SSHManagerTreeWidget::clearSshInfo);
0073     connect(ui->btnEdit, &QPushButton::clicked, this, &SSHManagerTreeWidget::editSshInfo);
0074     connect(ui->btnDelete, &QPushButton::clicked, this, &SSHManagerTreeWidget::triggerDelete);
0075     connect(ui->btnInvertFilter, &QPushButton::clicked, d->filterModel, &SSHManagerFilterModel::setInvertFilter);
0076 
0077     connect(ui->btnFindSshKey, &QPushButton::clicked, this, [this] {
0078         const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
0079         const QString sshFile = QFileDialog::getOpenFileName(this, i18n("SSH Key"), homeFolder + QStringLiteral("/.ssh"));
0080         if (sshFile.isEmpty()) {
0081             return;
0082         }
0083         ui->sshkey->setText(sshFile);
0084     });
0085 
0086     connect(ui->filterText, &QLineEdit::textChanged, this, [this] {
0087         d->filterModel->setFilterRegularExpression(ui->filterText->text());
0088         d->filterModel->invalidate();
0089     });
0090 
0091     connect(Konsole::ProfileModel::instance(), &Konsole::ProfileModel::rowsRemoved, this, &SSHManagerTreeWidget::updateProfileList);
0092     connect(Konsole::ProfileModel::instance(), &Konsole::ProfileModel::rowsInserted, this, &SSHManagerTreeWidget::updateProfileList);
0093     updateProfileList();
0094 
0095     ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu);
0096 
0097     connect(ui->treeView, &QTreeView::customContextMenuRequested, [this](const QPoint &pos) {
0098         QModelIndex idx = ui->treeView->indexAt(pos);
0099         if (!idx.isValid()) {
0100             return;
0101         }
0102 
0103         if (idx.data(Qt::DisplayRole) == i18n("SSH Config")) {
0104             return;
0105         }
0106 
0107         auto sourceIdx = d->filterModel->mapToSource(idx);
0108         const bool isParent = sourceIdx.parent() == d->model->invisibleRootItem()->index();
0109         if (!isParent) {
0110             const auto item = d->model->itemFromIndex(sourceIdx);
0111             const auto data = item->data(SSHManagerModel::SSHRole).value<SSHConfigurationData>();
0112             if (data.importedFromSshConfig) {
0113                 return;
0114             }
0115         }
0116 
0117         QMenu *menu = new QMenu(this);
0118         auto action = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action:inmenu", "Delete"), ui->treeView);
0119         menu->addAction(action);
0120 
0121         connect(action, &QAction::triggered, this, &SSHManagerTreeWidget::triggerDelete);
0122 
0123         menu->popup(ui->treeView->viewport()->mapToGlobal(pos));
0124     });
0125 
0126     connect(ui->treeView, &QTreeView::doubleClicked, this, [this](const QModelIndex &idx) {
0127         SSHManagerPlugin::requestConnection(d->filterModel, d->model, d->controller, idx);
0128     });
0129 
0130     connect(ui->treeView, &SshTreeView::mouseButtonClicked, this, &SSHManagerTreeWidget::handleTreeClick);
0131 
0132     ui->treeView->setModel(d->filterModel);
0133 
0134     // We have nothing selected, so there's nothing to edit.
0135     ui->btnEdit->setEnabled(false);
0136 
0137     clearSshInfo();
0138 
0139     QSettings settings;
0140     settings.beginGroup(QStringLiteral("plugins"));
0141     settings.beginGroup(QStringLiteral("sshplugin"));
0142 
0143     const QKeySequence def(Qt::CTRL | Qt::ALT | Qt::Key_H);
0144     const QString defText = def.toString();
0145     const QString entry = settings.value(QStringLiteral("ssh_shortcut"), defText).toString();
0146     const QKeySequence shortcutEntry(entry);
0147 
0148     connect(ui->keySequenceEdit, &QKeySequenceEdit::keySequenceChanged, this, [this] {
0149         auto shortcut = ui->keySequenceEdit->keySequence();
0150         Q_EMIT quickAccessShortcutChanged(shortcut);
0151     });
0152     ui->keySequenceEdit->setKeySequence(shortcutEntry);
0153 }
0154 
0155 SSHManagerTreeWidget::~SSHManagerTreeWidget() = default;
0156 
0157 void SSHManagerTreeWidget::updateProfileList()
0158 {
0159     ui->profile->clear();
0160     ui->profile->addItem(i18n("Don't Change"));
0161     auto model = Konsole::ProfileModel::instance();
0162     for (int i = 0, end = model->rowCount(QModelIndex()); i < end; i++) {
0163         const int column = Konsole::ProfileModel::Column::PROFILE;
0164         const int role = Qt::DisplayRole;
0165         const QModelIndex currIdx = model->index(i, column);
0166         const auto profileName = model->data(currIdx, role).toString();
0167         ui->profile->addItem(profileName);
0168     }
0169 }
0170 
0171 void SSHManagerTreeWidget::addSshInfo()
0172 {
0173     SSHConfigurationData data;
0174     auto [error, errorString] = checkFields();
0175     if (error) {
0176         ui->errorPanel->setText(errorString);
0177         ui->errorPanel->show();
0178         return;
0179     }
0180 
0181     d->model->addChildItem(info(), ui->folder->currentText());
0182     clearSshInfo();
0183 }
0184 
0185 void SSHManagerTreeWidget::saveEdit()
0186 {
0187     //    SSHConfigurationData data; (not used?)
0188     auto [error, errorString] = checkFields();
0189     if (error) {
0190         ui->errorPanel->setText(errorString);
0191         ui->errorPanel->show();
0192         return;
0193     }
0194 
0195     auto selection = ui->treeView->selectionModel()->selectedIndexes();
0196     auto sourceIdx = d->filterModel->mapToSource(selection.at(0));
0197     d->model->editChildItem(info(), sourceIdx);
0198 
0199     clearSshInfo();
0200 }
0201 
0202 void SSHManagerTreeWidget::requestImport()
0203 {
0204     d->model->startImportFromSshConfig();
0205 }
0206 
0207 SSHConfigurationData SSHManagerTreeWidget::info() const
0208 {
0209     SSHConfigurationData data;
0210     data.host = ui->hostname->text().trimmed();
0211     data.name = ui->name->text().trimmed();
0212     data.port = ui->port->text().trimmed();
0213     data.sshKey = ui->sshkey->text().trimmed();
0214     data.profileName = ui->profile->currentText().trimmed();
0215     data.username = ui->username->text().trimmed();
0216     data.useSshConfig = ui->useSshConfig->checkState() == Qt::Checked;
0217     // if ui->username is enabled then we were not imported!
0218     data.importedFromSshConfig = !ui->username->isEnabled();
0219     return data;
0220 }
0221 
0222 void SSHManagerTreeWidget::triggerDelete()
0223 {
0224     auto selection = ui->treeView->selectionModel()->selectedIndexes();
0225     if (selection.empty()) {
0226         return;
0227     }
0228 
0229     const QString text = selection.at(0).data(Qt::DisplayRole).toString();
0230     const QString dialogMessage = ui->treeView->model()->rowCount(selection.at(0))
0231         ? i18n("You are about to delete the folder %1,\n with multiple SSH Configurations, are you sure?", text)
0232         : i18n("You are about to delete %1, are you sure?", text);
0233 
0234     const QString dontAskAgainKey =
0235         ui->treeView->model()->rowCount(selection.at(0)) ? QStringLiteral("remove_ssh_folder") : QStringLiteral("remove_ssh_config");
0236 
0237     int result = KMessageBox::warningTwoActions(this,
0238                                                 dialogMessage,
0239                                                 i18nc("@title:window", "Delete SSH Configurations"),
0240                                                 KStandardGuiItem::del(),
0241                                                 KStandardGuiItem::cancel(),
0242                                                 dontAskAgainKey);
0243 
0244     if (result == KMessageBox::ButtonCode::SecondaryAction) {
0245         return;
0246     }
0247 
0248     const auto sourceIdx = d->filterModel->mapToSource(selection.at(0));
0249     d->model->removeIndex(sourceIdx);
0250 }
0251 
0252 void SSHManagerTreeWidget::editSshInfo()
0253 {
0254     auto selection = ui->treeView->selectionModel()->selectedIndexes();
0255     if (selection.empty()) {
0256         return;
0257     }
0258 
0259     clearSshInfo();
0260     showInfoPane();
0261 
0262     const auto sourceIdx = d->filterModel->mapToSource(selection.at(0));
0263     const auto item = d->model->itemFromIndex(sourceIdx);
0264     const auto data = item->data(SSHManagerModel::SSHRole).value<SSHConfigurationData>();
0265 
0266     ui->hostname->setText(data.host);
0267     ui->name->setText(data.name);
0268     ui->port->setText(data.port);
0269     ui->sshkey->setText(data.sshKey);
0270     if (data.profileName.isEmpty()) {
0271         ui->profile->setCurrentIndex(0);
0272     } else {
0273         ui->profile->setCurrentText(data.profileName);
0274     }
0275     ui->username->setText(data.username);
0276     ui->useSshConfig->setCheckState(data.useSshConfig ? Qt::Checked : Qt::Unchecked);
0277 
0278     // This is just for add. To edit the folder, the user will drag & drop.
0279     ui->folder->setCurrentText(QStringLiteral("not-used-here"));
0280     ui->folderLabel->hide();
0281     ui->folder->hide();
0282     ui->btnAdd->setText(tr("Update"));
0283     disconnect(ui->btnAdd, nullptr, this, nullptr);
0284     connect(ui->btnAdd, &QPushButton::clicked, this, &SSHManagerTreeWidget::saveEdit);
0285 
0286     handleImportedData(data.importedFromSshConfig);
0287 }
0288 
0289 void SSHManagerTreeWidget::handleImportedData(bool isImported)
0290 {
0291     QList<QWidget *> elements = {ui->hostname, ui->port, ui->username, ui->sshkey, ui->useSshConfig};
0292     if (isImported) {
0293         ui->errorPanel->setText(QStringLiteral("Imported SSH Profile <br/> Some settings are read only."));
0294         ui->errorPanel->show();
0295     }
0296 
0297     for (auto *element : elements) {
0298         element->setEnabled(!isImported);
0299     }
0300 }
0301 
0302 void SSHManagerTreeWidget::setEditComponentsEnabled(bool enabled)
0303 {
0304     ui->hostname->setEnabled(enabled);
0305     ui->name->setEnabled(enabled);
0306     ui->port->setEnabled(enabled);
0307     ui->sshkey->setEnabled(enabled);
0308     ui->profile->setEnabled(enabled);
0309     ui->username->setEnabled(enabled);
0310     ui->useSshConfig->setEnabled(enabled);
0311 }
0312 
0313 void SSHManagerTreeWidget::clearSshInfo()
0314 {
0315     hideInfoPane();
0316     ui->name->setText({});
0317     ui->hostname->setText({});
0318     ui->port->setText(QStringLiteral("22"));
0319     ui->sshkey->setText({});
0320     ui->treeView->setEnabled(true);
0321 }
0322 
0323 void SSHManagerTreeWidget::hideInfoPane()
0324 {
0325     ui->newSSHConfig->show();
0326     ui->btnDelete->show();
0327     ui->btnEdit->show();
0328     ui->sshInfoPane->hide();
0329     ui->btnAdd->hide();
0330     ui->btnCancel->hide();
0331     ui->errorPanel->hide();
0332 }
0333 
0334 void SSHManagerTreeWidget::showInfoPane()
0335 {
0336     ui->newSSHConfig->hide();
0337     ui->btnDelete->hide();
0338     ui->btnEdit->hide();
0339     ui->sshInfoPane->show();
0340     ui->btnAdd->show();
0341     ui->btnCancel->show();
0342     ui->folder->show();
0343     ui->folderLabel->show();
0344 
0345     ui->sshkey->setText({});
0346 
0347     ui->folder->clear();
0348     ui->folder->addItems(d->model->folders());
0349 
0350     setEditComponentsEnabled(true);
0351     ui->btnAdd->setText(tr("Add"));
0352     disconnect(ui->btnAdd, nullptr, this, nullptr);
0353     connect(ui->btnAdd, &QPushButton::clicked, this, &SSHManagerTreeWidget::addSshInfo);
0354 
0355     // Disable the tree view when in edit mode.
0356     // This is important so the user don't click around
0357     // losing the configuration he did.
0358     // this will be enabled again when the user closes the panel.
0359     ui->treeView->setEnabled(false);
0360 }
0361 
0362 void SSHManagerTreeWidget::setModel(SSHManagerModel *model)
0363 {
0364     d->model = model;
0365     d->filterModel->setSourceModel(model);
0366     ui->folder->addItems(d->model->folders());
0367     ui->btnManageProfile->setChecked(d->model->getManageProfile());
0368     connect(ui->btnManageProfile, &QPushButton::clicked, d->model, &SSHManagerModel::setManageProfile);
0369 }
0370 
0371 void SSHManagerTreeWidget::setCurrentController(Konsole::SessionController *controller)
0372 {
0373     qCDebug(KonsoleDebug) << "Controller changed to" << controller;
0374 
0375     d->controller = controller;
0376     d->model->setSessionController(controller);
0377 }
0378 
0379 std::pair<bool, QString> SSHManagerTreeWidget::checkFields() const
0380 {
0381     bool error = false;
0382     QString errorString = QStringLiteral("<ul>");
0383     const QString li = QStringLiteral("<li>");
0384     const QString il = QStringLiteral("</li>");
0385 
0386     if (ui->hostname->text().isEmpty()) {
0387         error = true;
0388         errorString += li + i18n("Missing Hostname") + il;
0389     }
0390 
0391     if (ui->name->text().isEmpty()) {
0392         error = true;
0393         errorString += li + i18n("Missing Name") + il;
0394     }
0395 
0396     if (ui->useSshConfig->checkState() == Qt::Checked) {
0397         // If ui->username is not enabled then this was an autopopulated entry and we should not complain
0398         if (ui->username->isEnabled() && (ui->sshkey->text().length() || ui->username->text().length())) {
0399             error = true;
0400             errorString += li + i18n("If Use Ssh Config is set, do not specify sshkey or username.") + il;
0401         }
0402     } else {
0403         if (ui->sshkey->text().isEmpty() && ui->username->text().isEmpty()) {
0404             error = true;
0405             errorString += li + i18n("At least Username or SSHKey must be set") + il;
0406         }
0407     }
0408 
0409     if (ui->folder->currentText().isEmpty()) {
0410         error = true;
0411         errorString += li + i18n("Missing Folder") + il;
0412     }
0413 
0414     if (ui->profile->currentText().isEmpty()) {
0415         error = true;
0416         errorString += li + i18n("An SSH session must have a profile") + il;
0417     }
0418     errorString += QStringLiteral("</ul>");
0419 
0420     return {error, errorString};
0421 }
0422 
0423 void SSHManagerTreeWidget::handleTreeClick(Qt::MouseButton btn, const QModelIndex idx)
0424 {
0425     if (!d->controller) {
0426         return;
0427     }
0428     auto sourceIdx = d->filterModel->mapToSource(idx);
0429 
0430     ui->treeView->setCurrentIndex(idx);
0431     ui->treeView->selectionModel()->setCurrentIndex(idx, QItemSelectionModel::SelectionFlag::Rows);
0432 
0433     if (btn == Qt::LeftButton || btn == Qt::RightButton) {
0434         const bool isParent = sourceIdx.parent() == d->model->invisibleRootItem()->index();
0435 
0436         if (isParent) {
0437             setEditComponentsEnabled(false);
0438             if (sourceIdx.data(Qt::DisplayRole).toString() == i18n("SSH Config")) {
0439                 ui->btnDelete->setEnabled(false);
0440                 ui->btnDelete->setToolTip(i18n("Cannot delete this folder"));
0441             } else {
0442                 ui->btnDelete->setEnabled(true);
0443                 ui->btnDelete->setToolTip(i18n("Delete folder and all of its contents"));
0444             }
0445             ui->btnEdit->setEnabled(false);
0446             if (ui->sshInfoPane->isVisible()) {
0447                 ui->errorPanel->setText(i18n("Double click to change the folder name."));
0448             }
0449         } else {
0450             const auto item = d->model->itemFromIndex(sourceIdx);
0451             const auto data = item->data(SSHManagerModel::SSHRole).value<SSHConfigurationData>();
0452             ui->btnEdit->setEnabled(true);
0453             ui->btnDelete->setEnabled(!data.importedFromSshConfig);
0454             ui->btnDelete->setToolTip(data.importedFromSshConfig ? i18n("You can't delete an automatically added entry.") : i18n("Delete selected entry"));
0455             if (ui->sshInfoPane->isVisible()) {
0456                 handleImportedData(data.importedFromSshConfig);
0457                 editSshInfo();
0458             }
0459         }
0460         return;
0461     }
0462 
0463     if (btn == Qt::MiddleButton) {
0464         if (sourceIdx.parent() == d->model->invisibleRootItem()->index()) {
0465             return;
0466         }
0467 
0468         Q_EMIT requestNewTab();
0469         SSHManagerPlugin::requestConnection(d->filterModel, d->model, d->controller, sourceIdx);
0470     }
0471 }
0472 
0473 void SSHManagerTreeWidget::showEvent(QShowEvent *)
0474 {
0475     if (!d->isSetup) {
0476         ui->treeView->expandAll();
0477         d->isSetup = true;
0478     }
0479 }
0480 
0481 #include "moc_sshmanagerpluginwidget.cpp"