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