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"