1 /* This file was part of the KDE libraries
2
3 SPDX-FileCopyrightText: 2021 Tomaz Canabrava <tcanabrava@kde.org>
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8 #include "sshmanagermodel.h"
9
10 #include <QStandardItem>
11
12 #include <KLocalizedString>
13
14 #include <KConfig>
15 #include <KConfigGroup>
16
17 #include <QDebug>
18 #include <QFile>
19 #include <QLoggingCategory>
20 #include <QStandardPaths>
21 #include <QTextStream>
22
23 #include "sshconfigurationdata.h"
24
25 Q_LOGGING_CATEGORY(SshManagerPlugin, "org.kde.konsole.plugin.sshmanager")
26
27 namespace
28 {
29 const QString SshDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + QStringLiteral("/.ssh/");
30 }
31
SSHManagerModel(QObject * parent)32 SSHManagerModel::SSHManagerModel(QObject *parent)
33 : QStandardItemModel(parent)
34 {
35 load();
36 if (invisibleRootItem()->rowCount() == 0) {
37 addTopLevelItem(i18n("Default"));
38 }
39 }
40
~SSHManagerModel()41 SSHManagerModel::~SSHManagerModel() noexcept
42 {
43 save();
44 }
45
addTopLevelItem(const QString & name)46 QStandardItem *SSHManagerModel::addTopLevelItem(const QString &name)
47 {
48 for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
49 if (invisibleRootItem()->child(i)->text() == name) {
50 return nullptr;
51 }
52 }
53
54 auto *newItem = new QStandardItem();
55 newItem->setText(name);
56 invisibleRootItem()->appendRow(newItem);
57 invisibleRootItem()->sortChildren(0);
58 return newItem;
59 }
60
addChildItem(const SSHConfigurationData & config,const QString & parentName)61 void SSHManagerModel::addChildItem(const SSHConfigurationData &config, const QString &parentName)
62 {
63 QStandardItem *item = nullptr;
64 for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
65 if (invisibleRootItem()->child(i)->text() == parentName) {
66 item = invisibleRootItem()->child(i);
67 break;
68 }
69 }
70
71 if (!item) {
72 item = addTopLevelItem(parentName);
73 }
74
75 auto newChild = new QStandardItem();
76 newChild->setData(QVariant::fromValue(config), SSHRole);
77 newChild->setData(config.name, Qt::DisplayRole);
78 item->appendRow(newChild);
79 item->sortChildren(0);
80 }
81
setData(const QModelIndex & index,const QVariant & value,int role)82 bool SSHManagerModel::setData(const QModelIndex &index, const QVariant &value, int role)
83 {
84 const bool ret = QStandardItemModel::setData(index, value, role);
85 invisibleRootItem()->sortChildren(0);
86 return ret;
87 }
88
editChildItem(const SSHConfigurationData & config,const QModelIndex & idx)89 void SSHManagerModel::editChildItem(const SSHConfigurationData &config, const QModelIndex &idx)
90 {
91 QStandardItem *item = itemFromIndex(idx);
92 item->setData(QVariant::fromValue(config), SSHRole);
93 item->setData(config.name, Qt::DisplayRole);
94 item->parent()->sortChildren(0);
95 }
96
folders() const97 QStringList SSHManagerModel::folders() const
98 {
99 QStringList retList;
100 for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
101 retList.push_back(invisibleRootItem()->child(i)->text());
102 }
103 return retList;
104 }
105
load()106 void SSHManagerModel::load()
107 {
108 auto config = KConfig(QStringLiteral("konsolesshconfig"), KConfig::OpenFlag::SimpleConfig);
109 for (const QString &groupName : config.groupList()) {
110 KConfigGroup group = config.group(groupName);
111 addTopLevelItem(groupName);
112 for (const QString &sessionName : group.groupList()) {
113 SSHConfigurationData data;
114 KConfigGroup sessionGroup = group.group(sessionName);
115 data.host = sessionGroup.readEntry("hostname");
116 data.name = sessionGroup.readEntry("identifier");
117 data.port = sessionGroup.readEntry("port");
118 data.profileName = sessionGroup.readEntry("profilename");
119 data.sshKey = sessionGroup.readEntry("sshkey");
120 data.useSshConfig = sessionGroup.readEntry<bool>("useSshConfig", false);
121 data.importedFromSshConfig = sessionGroup.readEntry<bool>("importedFromSshConfig", false);
122 addChildItem(data, groupName);
123 }
124 }
125 }
126
save()127 void SSHManagerModel::save()
128 {
129 auto config = KConfig(QStringLiteral("konsolesshconfig"), KConfig::OpenFlag::SimpleConfig);
130 for (const QString &groupName : config.groupList()) {
131 config.deleteGroup(groupName);
132 }
133
134 for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
135 QStandardItem *groupItem = invisibleRootItem()->child(i);
136 const QString groupName = groupItem->text();
137 KConfigGroup baseGroup = config.group(groupName);
138 for (int e = 0, rend = groupItem->rowCount(); e < rend; e++) {
139 QStandardItem *sshElement = groupItem->child(e);
140 const auto data = sshElement->data(SSHRole).value<SSHConfigurationData>();
141 KConfigGroup sshGroup = baseGroup.group(data.name.trimmed());
142 sshGroup.writeEntry("hostname", data.host.trimmed());
143 sshGroup.writeEntry("identifier", data.name.trimmed());
144 sshGroup.writeEntry("port", data.port.trimmed());
145 sshGroup.writeEntry("profileName", data.profileName.trimmed());
146 sshGroup.writeEntry("sshkey", data.sshKey.trimmed());
147 sshGroup.writeEntry("useSshConfig", data.useSshConfig);
148 sshGroup.writeEntry("importedFromSshConfig", data.importedFromSshConfig);
149 sshGroup.sync();
150 }
151 baseGroup.sync();
152 }
153 config.sync();
154 }
155
flags(const QModelIndex & index) const156 Qt::ItemFlags SSHManagerModel::flags(const QModelIndex &index) const
157 {
158 if (indexFromItem(invisibleRootItem()) == index.parent()) {
159 return QStandardItemModel::flags(index);
160 } else {
161 return QStandardItemModel::flags(index) & ~Qt::ItemIsEditable;
162 }
163 }
164
removeIndex(const QModelIndex & idx)165 void SSHManagerModel::removeIndex(const QModelIndex &idx)
166 {
167 removeRow(idx.row(), idx.parent());
168 }
169
startImportFromSshConfig()170 void SSHManagerModel::startImportFromSshConfig()
171 {
172 for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
173 QStandardItem *groupItem = invisibleRootItem()->child(i);
174 if (groupItem->data(Qt::DisplayRole).toString() == tr("SSH Config")) {
175 removeIndex(indexFromItem(groupItem));
176 break;
177 }
178 }
179
180 importFromSshConfigFile(SshDir + QStringLiteral("config"));
181 }
182
importFromSshConfigFile(const QString & file)183 void SSHManagerModel::importFromSshConfigFile(const QString &file)
184 {
185 QFile sshConfig(file);
186 if (!sshConfig.open(QIODevice::ReadOnly)) {
187 qCDebug(SshManagerPlugin) << "Can't open config file";
188 }
189 QTextStream stream(&sshConfig);
190 QString line;
191
192 SSHConfigurationData data;
193
194 // If we hit a *, we ignore till the next Host.
195 bool ignoreEntry = false;
196 while (stream.readLineInto(&line)) {
197 // ignore comments
198 if (line.startsWith(QStringLiteral("#"))) {
199 continue;
200 }
201
202 QStringList lists = line.split(QLatin1Char(' '), Qt::SkipEmptyParts);
203 // ignore lines that are not "Type Value"
204 if (lists.count() != 2) {
205 continue;
206 }
207
208 if (lists.at(0) == QStringLiteral("Import")) {
209 if (lists.at(1).contains(QLatin1Char('*'))) {
210 // TODO: We don't handle globbing yet.
211 continue;
212 }
213
214 importFromSshConfigFile(SshDir + lists.at(1));
215 continue;
216 }
217
218 if (lists.at(0) == QStringLiteral("Host")) {
219 if (line.contains(QLatin1Char('*'))) {
220 // Panic, ignore everything untill the next Host appears.
221 ignoreEntry = true;
222 continue;
223 } else {
224 ignoreEntry = false;
225 }
226
227 if (!data.host.isEmpty()) {
228 if (data.name.isEmpty()) {
229 data.name = data.host;
230 }
231 data.useSshConfig = true;
232 data.importedFromSshConfig = true;
233 addChildItem(data, tr("SSH Config"));
234 data = {};
235 }
236
237 data.host = lists.at(1);
238 }
239
240 if (ignoreEntry) {
241 continue;
242 }
243
244 if (lists.at(0) == QStringLiteral("HostName")) {
245 // hostname is always after Host, so this will be true.
246 const QString currentHost = data.host;
247 data.host = lists.at(1).trimmed();
248 data.name = currentHost.trimmed();
249 } else if (lists.at(0) == QStringLiteral("IdentityFile")) {
250 data.sshKey = lists.at(1).trimmed();
251 } else if (lists.at(0) == QStringLiteral("Port")) {
252 data.port = lists.at(1).trimmed();
253 } else if (lists.at(0) == QStringLiteral("User")) {
254 data.username = lists.at(1).trimmed();
255 }
256 }
257
258 // the last possible read
259 if (data.host.count()) {
260 if (data.name.isEmpty()) {
261 data.name = data.host.trimmed();
262 }
263 data.useSshConfig = true;
264 data.importedFromSshConfig = true;
265 addChildItem(data, tr("SSH Config"));
266 }
267 }
268