1 /*
2 SPDX-License-Identifier: GPL-2.0-or-later
3 SPDX-FileCopyrightText: 2004 Jan Schaefer <j_schaef@informatik.uni-kl.de>
4 SPDX-FileCopyrightText: 2011 Rodrigo Belem <rclbelem@gmail.com>
5 SPDX-FileCopyrightText: 2015-2020 Harald Sitter <sitter@kde.org>
6 SPDX-FileCopyrightText: 2019 Nate Graham <nate@kde.org>
7 */
8
9 #include "sambausershareplugin.h"
10
11 #include <QFileInfo>
12 #include <QDebug>
13 #include <QQmlApplicationEngine>
14 #include <QQuickWidget>
15 #include <QQuickItem>
16 #include <QMetaMethod>
17 #include <QVBoxLayout>
18 #include <KLocalizedString>
19 #include <QTimer>
20 #include <QQmlContext>
21 #include <QPushButton>
22 #include <QDBusInterface>
23 #include <QDBusConnection>
24
25 #include <KDeclarative/KDeclarative>
26 #include <KLocalizedContext>
27 #include <KMessageBox>
28 #include <KPluginFactory>
29 #include <KSambaShare>
30 #include <KSambaShareData>
31 #include <KService>
32 #include <KIO/ApplicationLauncherJob>
33
34 #include "model.h"
35 #include "usermanager.h"
36 #include "groupmanager.h"
37
38 #ifdef SAMBA_INSTALL
39 #include "sambainstaller.h"
40 #endif
41
42 K_PLUGIN_CLASS_WITH_JSON(SambaUserSharePlugin, "sambausershareplugin.json")
43
44 class ShareContext : public QObject
45 {
46 Q_OBJECT
47 Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged)
48 Q_PROPERTY(bool canEnableGuest READ canEnableGuest CONSTANT)
49 Q_PROPERTY(bool guestEnabled READ guestEnabled WRITE setGuestEnabled NOTIFY guestEnabledChanged)
50 Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
51 Q_PROPERTY(int maximumNameLength READ maximumNameLength CONSTANT)
52 public:
ShareContext(const QUrl & url,QObject * parent=nullptr)53 explicit ShareContext(const QUrl &url, QObject *parent = nullptr)
54 : QObject(parent)
55 , m_shareData(resolveShare(url))
56 , m_enabled(KSambaShare::instance()->isDirectoryShared(m_shareData.path()))
57 // url isn't a member. always use .path()!
58 {
59 }
60
enabled() const61 bool enabled() const
62 {
63 return m_enabled;
64 }
65
setEnabled(bool enabled)66 void setEnabled(bool enabled)
67 {
68 m_enabled = enabled;
69 Q_EMIT enabledChanged();
70 }
71
canEnableGuest()72 bool canEnableGuest()
73 {
74 return KSambaShare::instance()->areGuestsAllowed();
75 }
76
guestEnabled() const77 bool guestEnabled() const
78 {
79 // WTF is that enum even...
80 switch (m_shareData.guestPermission()) {
81 case KSambaShareData::GuestsNotAllowed:
82 return false;
83 case KSambaShareData::GuestsAllowed:
84 return true;
85 }
86 Q_UNREACHABLE();
87 return false;
88 }
89
setGuestEnabled(bool enabled)90 void setGuestEnabled(bool enabled)
91 {
92 m_shareData.setGuestPermission(enabled ? KSambaShareData::GuestsAllowed : KSambaShareData::GuestsNotAllowed);
93 Q_EMIT guestEnabledChanged();
94 }
95
name() const96 QString name() const
97 {
98 return m_shareData.name();
99 }
100
setName(const QString & name)101 void setName(const QString &name)
102 {
103 m_shareData.setName(name);
104 Q_EMIT nameChanged();
105 }
106
maximumNameLength()107 static constexpr int maximumNameLength()
108 {
109 // Windows 10 allows creating shares with a maximum of 60 characters when measured on 2020-08-13.
110 // We consider this kind of a soft limit as there appears to be no actual limit specified anywhere.
111 return 60;
112 }
113
114
isNameFree(const QString & name)115 Q_INVOKABLE static bool isNameFree(const QString &name)
116 {
117 return KSambaShare::instance()->isShareNameAvailable(name);
118 }
119
120 public Q_SLOTS:
newShareName(const QUrl & url)121 QString newShareName(const QUrl &url)
122 {
123 Q_ASSERT(url.isValid());
124 Q_ASSERT(!url.isEmpty());
125 // TODO pretty sure this is buggy for urls with trailing slash where filename would be ""
126 return url.fileName().left(maximumNameLength());
127 }
128
129 Q_SIGNALS:
130 void enabledChanged();
131 void guestEnabledChanged();
132 void nameChanged();
133
134 private:
resolveShare(const QUrl & url)135 KSambaShareData resolveShare(const QUrl &url)
136 {
137 QFileInfo info(url.toLocalFile());
138 const QString path = info.canonicalFilePath();
139 Q_ASSERT(!path.isEmpty());
140 const QList<KSambaShareData> shareList = KSambaShare::instance()->getSharesByPath(path);
141 if (!shareList.isEmpty()) {
142 return shareList.first(); // FIXME: using just the first in the list for a while
143 }
144 KSambaShareData newShare;
145 newShare.setName(newShareName(url));
146 newShare.setGuestPermission(KSambaShareData::GuestsNotAllowed);
147 newShare.setPath(path);
148 return newShare;
149 }
150
151 public:
152 // TODO shouldn't be public may need refactoring though because the ACL model needs an immutable copy
153 KSambaShareData m_shareData;
154 private:
155 bool m_enabled = false; // this gets cached so we can manipulate its state from qml
156 };
157
SambaUserSharePlugin(QObject * parent,const QList<QVariant> & args)158 SambaUserSharePlugin::SambaUserSharePlugin(QObject *parent, const QList<QVariant> &args)
159 : KPropertiesDialogPlugin(qobject_cast<KPropertiesDialog *>(parent))
160 , m_url(properties->item().mostLocalUrl().toLocalFile())
161 , m_userManager(new UserManager(this))
162 {
163 Q_UNUSED(args)
164
165 if (m_url.isEmpty()) {
166 return;
167 }
168
169 const QFileInfo pathInfo(m_url);
170 if (!pathInfo.permission(QFile::ReadUser | QFile::WriteUser)) {
171 return;
172 }
173
174 // TODO: this could be made to load delayed via invokemethod. we technically don't need to fully load
175 // the backing data in the ctor, only the qml view with busyindicator
176 // TODO: relatedly if we make ShareContext and the Model more async vis a vis construction we can init them from
177 // QML and stop holding them as members in the plugin
178 m_context = new ShareContext(properties->item().mostLocalUrl(), this);
179 // FIXME maybe the manager ought to be owned by the model
180 qmlRegisterAnonymousType<UserManager>("org.kde.filesharing.samba", 1);
181 qmlRegisterAnonymousType<User>("org.kde.filesharing.samba", 1);
182 m_model = new UserPermissionModel(m_context->m_shareData, m_userManager, this);
183
184 #ifdef SAMBA_INSTALL
185 qmlRegisterType<SambaInstaller>("org.kde.filesharing.samba", 1, 0, "Installer");
186 #endif
187 qmlRegisterType<GroupManager>("org.kde.filesharing.samba", 1, 0, "GroupManager");
188 // Need access to the column enum, so register this as uncreatable.
189 qmlRegisterUncreatableType<UserPermissionModel>("org.kde.filesharing.samba", 1, 0, "UserPermissionModel",
190 QStringLiteral("Access through sambaPlugin.userPermissionModel"));
191 qmlRegisterAnonymousType<ShareContext>("org.kde.filesharing.samba", 1);
192 qmlRegisterAnonymousType<SambaUserSharePlugin>("org.kde.filesharing.samba", 1);
193
194 m_page.reset(new QWidget(qobject_cast<KPropertiesDialog *>(parent)));
195 m_page->setAttribute(Qt::WA_TranslucentBackground);
196 auto widget = new QQuickWidget(m_page.get());
197 // Load kdeclarative and set translation domain before setting the source so strings gets translated.
198 KDeclarative::KDeclarative::setupEngine(widget->engine());
199 auto i18nContext = new KLocalizedContext(widget->engine());
200 i18nContext->setTranslationDomain(QStringLiteral(TRANSLATION_DOMAIN));
201 widget->engine()->rootContext()->setContextObject(i18nContext);
202
203 widget->setResizeMode(QQuickWidget::SizeRootObjectToView);
204 widget->setFocusPolicy(Qt::StrongFocus);
205 widget->setAttribute(Qt::WA_AlwaysStackOnTop, true);
206 widget->quickWindow()->setColor(Qt::transparent);
207 auto layout = new QVBoxLayout(m_page.get());
208 layout->addWidget(widget);
209
210 widget->rootContext()->setContextProperty(QStringLiteral("sambaPlugin"), this);
211
212 const QUrl url(QStringLiteral("qrc:/org.kde.filesharing.samba/qml/main.qml"));
213 widget->setSource(url);
214
215 properties->setFileSharingPage(m_page.get());
216 if (qEnvironmentVariableIsSet("TEST_FOCUS_SHARE")) {
217 QTimer::singleShot(100, properties, &KPropertiesDialog::showFileSharingPage);
218 }
219
220 QTimer::singleShot(0, [this] {
221 connect(m_userManager, &UserManager::loaded, this, [this] {
222 setReady(true);
223 });
224 m_userManager->load();
225 });
226 }
227
isSambaInstalled()228 bool SambaUserSharePlugin::isSambaInstalled()
229 {
230 return QFile::exists(QStringLiteral("/usr/sbin/smbd"))
231 || QFile::exists(QStringLiteral("/usr/local/sbin/smbd"));
232 }
233
showSambaStatus()234 void SambaUserSharePlugin::showSambaStatus()
235 {
236 KService::Ptr kcm = KService::serviceByStorageId(QStringLiteral("smbstatus"));
237 if (!kcm) {
238 // TODO: meh - we have no availability handling. I may have a handy class in plasma-disks
239 return;
240 }
241 KIO::ApplicationLauncherJob(kcm).start();
242 }
243
applyChanges()244 void SambaUserSharePlugin::applyChanges()
245 {
246 qDebug() << "!!! applying changes !!!" << m_context->enabled() << m_context->name() << m_context->guestEnabled() << m_model->getAcl() << m_context->m_shareData.path();
247 if (!m_context->enabled()) {
248 reportRemove(m_context->m_shareData.remove());
249 return;
250 }
251
252 // TODO: should run this through reportAdd() as well, ACLs may be invalid and then we shouldn't try to save
253 m_context->m_shareData.setAcl(m_model->getAcl());
254 reportAdd(m_context->m_shareData.save());
255 }
256
errorToString(KSambaShareData::UserShareError error)257 static QString errorToString(KSambaShareData::UserShareError error)
258 {
259 // KSambaShare is a right mess. Every function with an error returns the same enum but can only return a subset of
260 // possible values. Even so, because it returns the enum we had best mapped all values to semi-suitable string
261 // representations even when those are utter garbage when they require specific (e.g. an invalid ACL) that
262 // we do not have here.
263 switch (error) {
264 case KSambaShareData::UserShareNameOk: Q_FALLTHROUGH();
265 case KSambaShareData::UserSharePathOk: Q_FALLTHROUGH();
266 case KSambaShareData::UserShareAclOk: Q_FALLTHROUGH();
267 case KSambaShareData::UserShareCommentOk: Q_FALLTHROUGH();
268 case KSambaShareData::UserShareGuestsOk: Q_FALLTHROUGH();
269 case KSambaShareData::UserShareOk:
270 // Technically anything but UserShareOk cannot happen, but best handle everything regardless.
271 return QString();
272 case KSambaShareData::UserShareExceedMaxShares:
273 return i18nc("@info detailed error messsage",
274 "You have exhausted the maximum amount of shared directories you may have active at the same time.");
275 case KSambaShareData::UserShareNameInvalid:
276 return i18nc("@info detailed error messsage", "The share name is invalid.");
277 case KSambaShareData::UserShareNameInUse:
278 return i18nc("@info detailed error messsage", "The share name is already in use for a different directory.");
279 case KSambaShareData::UserSharePathInvalid:
280 return i18nc("@info detailed error messsage", "The path is invalid.");
281 case KSambaShareData::UserSharePathNotExists:
282 return i18nc("@info detailed error messsage", "The path does not exist.");
283 case KSambaShareData::UserSharePathNotDirectory:
284 return i18nc("@info detailed error messsage", "The path is not a directory.");
285 case KSambaShareData::UserSharePathNotAbsolute:
286 return i18nc("@info detailed error messsage", "The path is relative.");
287 case KSambaShareData::UserSharePathNotAllowed:
288 return i18nc("@info detailed error messsage", "This path may not be shared.");
289 case KSambaShareData::UserShareAclInvalid:
290 return i18nc("@info detailed error messsage", "The access rule is invalid.");
291 case KSambaShareData::UserShareAclUserNotValid:
292 return i18nc("@info detailed error messsage", "An access rule's user is not valid.");
293 case KSambaShareData::UserShareGuestsInvalid:
294 return i18nc("@info detailed error messsage", "The 'Guest' access rule is invalid.");
295 case KSambaShareData::UserShareGuestsNotAllowed:
296 return i18nc("@info detailed error messsage", "Enabling guest access is not allowed.");
297 case KSambaShareData::UserShareSystemError:
298 return KSambaShare::instance()->lastSystemErrorString().simplified();
299 }
300 Q_UNREACHABLE();
301 return QString();
302 }
303
reportAdd(KSambaShareData::UserShareError error)304 void SambaUserSharePlugin::reportAdd(KSambaShareData::UserShareError error)
305 {
306 if (error == KSambaShareData::UserShareOk) {
307 return;
308 }
309
310 QString errorMessage = errorToString(error);
311 if (error == KSambaShareData::UserShareSystemError) {
312 // System errors are (untranslated) CLI output. Give them localized context.
313 errorMessage = xi18nc("@info error in the underlying binaries. %1 is CLI output",
314 "<para>An error occurred while trying to share the directory."
315 " The share has not been created.</para>"
316 "<para>Samba internals report:</para><message>%1</message>",
317 errorMessage);
318 }
319 KMessageBox::error(qobject_cast<QWidget *>(parent()),
320 errorMessage,
321 i18nc("@info/title", "Failed to Create Network Share"));
322 }
323
reportRemove(KSambaShareData::UserShareError error)324 void SambaUserSharePlugin::reportRemove(KSambaShareData::UserShareError error)
325 {
326 if (error == KSambaShareData::UserShareOk) {
327 return;
328 }
329
330 QString errorMessage = errorToString(error);
331 if (error == KSambaShareData::UserShareSystemError) {
332 // System errors are (untranslated) CLI output. Give them localized context.
333 errorMessage = xi18nc("@info error in the underlying binaries. %1 is CLI output",
334 "<para>An error occurred while trying to un-share the directory."
335 " The share has not been removed.</para>"
336 "<para>Samba internals report:</para><message>%1</message>",
337 errorMessage);
338 }
339 KMessageBox::error(qobject_cast<QWidget *>(parent()),
340 errorMessage,
341 i18nc("@info/title", "Failed to Remove Network Share"));
342 }
343
isReady() const344 bool SambaUserSharePlugin::isReady() const
345 {
346 return m_ready;
347 }
348
setReady(bool ready)349 void SambaUserSharePlugin::setReady(bool ready)
350 {
351 m_ready = ready;
352 Q_EMIT readyChanged();
353 }
354
reboot()355 void SambaUserSharePlugin::reboot()
356 {
357 QDBusInterface interface(QStringLiteral("org.kde.ksmserver"), QStringLiteral("/KSMServer"),
358 QStringLiteral("org.kde.KSMServerInterface"), QDBusConnection::sessionBus());
359 interface.asyncCall(QStringLiteral("logout"), 0, 1, 2); // Options: do not ask again | reboot | force
360 }
361
362 #include "sambausershareplugin.moc"
363