|
|
|
@@ -0,0 +1,308 @@
|
|
|
|
|
#include "smbshareplugin.h"
|
|
|
|
|
|
|
|
|
|
#include <KPluginFactory>
|
|
|
|
|
#include <KLocalizedString>
|
|
|
|
|
|
|
|
|
|
#include <QFormLayout>
|
|
|
|
|
#include <QGroupBox>
|
|
|
|
|
#include <QLabel>
|
|
|
|
|
#include <QProcess>
|
|
|
|
|
#include <QScrollArea>
|
|
|
|
|
#include <QVBoxLayout>
|
|
|
|
|
|
|
|
|
|
K_PLUGIN_CLASS_WITH_JSON(SmbSharePlugin, "smbshareplugin.json")
|
|
|
|
|
|
|
|
|
|
// Permission levels for the combo boxes
|
|
|
|
|
static const QStringList permissionLabels = {
|
|
|
|
|
QStringLiteral("---"),
|
|
|
|
|
QStringLiteral("Read Only"),
|
|
|
|
|
QStringLiteral("Full Access"),
|
|
|
|
|
QStringLiteral("Deny")
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Map combo index to net usershare ACL character
|
|
|
|
|
static QString aclForIndex(int index)
|
|
|
|
|
{
|
|
|
|
|
switch (index) {
|
|
|
|
|
case 1: return QStringLiteral("R");
|
|
|
|
|
case 2: return QStringLiteral("F");
|
|
|
|
|
case 3: return QStringLiteral("D");
|
|
|
|
|
default: return QString();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Map ACL character back to combo index
|
|
|
|
|
static int indexForAcl(const QString &acl)
|
|
|
|
|
{
|
|
|
|
|
if (acl == QLatin1String("R")) return 1;
|
|
|
|
|
if (acl == QLatin1String("F")) return 2;
|
|
|
|
|
if (acl == QLatin1String("D")) return 3;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get list of local users (UID >= 1000, excluding nobody)
|
|
|
|
|
static QStringList getLocalUsers()
|
|
|
|
|
{
|
|
|
|
|
QStringList users;
|
|
|
|
|
QFile passwd(QStringLiteral("/etc/passwd"));
|
|
|
|
|
if (passwd.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
|
|
|
while (!passwd.atEnd()) {
|
|
|
|
|
const QString line = QString::fromLocal8Bit(passwd.readLine()).trimmed();
|
|
|
|
|
if (line.isEmpty() || line.startsWith(QLatin1Char('#')))
|
|
|
|
|
continue;
|
|
|
|
|
const QStringList fields = line.split(QLatin1Char(':'));
|
|
|
|
|
if (fields.size() < 7)
|
|
|
|
|
continue;
|
|
|
|
|
const int uid = fields[2].toInt();
|
|
|
|
|
const QString shell = fields[6];
|
|
|
|
|
if (uid >= 1000 && uid < 65534
|
|
|
|
|
&& !shell.endsWith(QLatin1String("/nologin"))
|
|
|
|
|
&& !shell.endsWith(QLatin1String("/false"))) {
|
|
|
|
|
users << fields[0];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return users;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SmbSharePlugin::SmbSharePlugin(QObject *parent, const QVariantList &args)
|
|
|
|
|
: KPropertiesDialogPlugin(qobject_cast<KPropertiesDialog *>(parent))
|
|
|
|
|
{
|
|
|
|
|
Q_UNUSED(args)
|
|
|
|
|
|
|
|
|
|
m_path = properties->item().localPath();
|
|
|
|
|
if (m_path.isEmpty())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// Default share name = folder name
|
|
|
|
|
m_defaultShareName = m_path.section(QLatin1Char('/'), -1);
|
|
|
|
|
|
|
|
|
|
// --- Build the UI ---
|
|
|
|
|
m_page = new QWidget();
|
|
|
|
|
auto *mainLayout = new QVBoxLayout(m_page);
|
|
|
|
|
|
|
|
|
|
// Share checkbox
|
|
|
|
|
m_shareCheckBox = new QCheckBox(
|
|
|
|
|
i18n("Share this folder with other computers on the local network"));
|
|
|
|
|
mainLayout->addWidget(m_shareCheckBox);
|
|
|
|
|
|
|
|
|
|
// Share name
|
|
|
|
|
auto *nameLayout = new QFormLayout();
|
|
|
|
|
m_nameEdit = new QLineEdit(m_defaultShareName);
|
|
|
|
|
nameLayout->addRow(i18n("Name:"), m_nameEdit);
|
|
|
|
|
mainLayout->addLayout(nameLayout);
|
|
|
|
|
|
|
|
|
|
// Guest access
|
|
|
|
|
m_guestCheckBox = new QCheckBox(i18n("Allow Guests"));
|
|
|
|
|
mainLayout->addWidget(m_guestCheckBox);
|
|
|
|
|
|
|
|
|
|
// User permissions section
|
|
|
|
|
auto *usersGroup = new QGroupBox();
|
|
|
|
|
usersGroup->setFlat(true);
|
|
|
|
|
auto *usersLayout = new QVBoxLayout(usersGroup);
|
|
|
|
|
|
|
|
|
|
// Scroll area for user list
|
|
|
|
|
auto *scrollArea = new QScrollArea();
|
|
|
|
|
scrollArea->setWidgetResizable(true);
|
|
|
|
|
scrollArea->setFrameShape(QFrame::NoFrame);
|
|
|
|
|
|
|
|
|
|
m_usersWidget = new QWidget();
|
|
|
|
|
auto *usersFormLayout = new QFormLayout(m_usersWidget);
|
|
|
|
|
usersFormLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
// Add "Everyone" entry first
|
|
|
|
|
{
|
|
|
|
|
auto *combo = new QComboBox();
|
|
|
|
|
combo->addItems(permissionLabels);
|
|
|
|
|
combo->setCurrentIndex(1); // Default: Read Only
|
|
|
|
|
usersFormLayout->addRow(QStringLiteral("Everyone"), combo);
|
|
|
|
|
m_userPerms.append({QStringLiteral("Everyone"), combo});
|
|
|
|
|
connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
|
|
|
|
this, [this]() { setDirty(true); });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add local users
|
|
|
|
|
const QStringList users = getLocalUsers();
|
|
|
|
|
for (const QString &user : users) {
|
|
|
|
|
auto *combo = new QComboBox();
|
|
|
|
|
combo->addItems(permissionLabels);
|
|
|
|
|
combo->setCurrentIndex(0); // Default: ---
|
|
|
|
|
usersFormLayout->addRow(user, combo);
|
|
|
|
|
m_userPerms.append({user, combo});
|
|
|
|
|
connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
|
|
|
|
this, [this]() { setDirty(true); });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scrollArea->setWidget(m_usersWidget);
|
|
|
|
|
usersLayout->addWidget(scrollArea);
|
|
|
|
|
mainLayout->addWidget(usersGroup);
|
|
|
|
|
|
|
|
|
|
mainLayout->addStretch();
|
|
|
|
|
|
|
|
|
|
// Connect signals
|
|
|
|
|
connect(m_shareCheckBox, &QCheckBox::toggled,
|
|
|
|
|
this, &SmbSharePlugin::onShareToggled);
|
|
|
|
|
connect(m_nameEdit, &QLineEdit::textChanged,
|
|
|
|
|
this, [this]() { setDirty(true); });
|
|
|
|
|
connect(m_guestCheckBox, &QCheckBox::toggled,
|
|
|
|
|
this, [this]() { setDirty(true); });
|
|
|
|
|
|
|
|
|
|
// Load existing share info
|
|
|
|
|
loadCurrentShare();
|
|
|
|
|
|
|
|
|
|
// Initial UI state
|
|
|
|
|
onShareToggled(m_shareCheckBox->isChecked());
|
|
|
|
|
|
|
|
|
|
// Add the page as a tab
|
|
|
|
|
properties->addPage(m_page, i18n("Share"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SmbSharePlugin::onShareToggled(bool checked)
|
|
|
|
|
{
|
|
|
|
|
m_nameEdit->setEnabled(checked);
|
|
|
|
|
m_guestCheckBox->setEnabled(checked);
|
|
|
|
|
m_usersWidget->setEnabled(checked);
|
|
|
|
|
setDirty(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SmbSharePlugin::loadCurrentShare()
|
|
|
|
|
{
|
|
|
|
|
// Run: net usershare info --long
|
|
|
|
|
QProcess proc;
|
|
|
|
|
proc.start(QStringLiteral("net"),
|
|
|
|
|
{QStringLiteral("usershare"), QStringLiteral("info"),
|
|
|
|
|
QStringLiteral("--long")});
|
|
|
|
|
proc.waitForFinished(5000);
|
|
|
|
|
|
|
|
|
|
const QString output = QString::fromLocal8Bit(proc.readAllStandardOutput());
|
|
|
|
|
if (output.isEmpty())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// Parse the output to find a share matching our path
|
|
|
|
|
// Format:
|
|
|
|
|
// [sharename]
|
|
|
|
|
// path=/some/path
|
|
|
|
|
// comment=
|
|
|
|
|
// usershare_acl=Everyone:R,user:F
|
|
|
|
|
// guest_ok=n
|
|
|
|
|
QString currentSection;
|
|
|
|
|
QString sharePath;
|
|
|
|
|
QString shareAcl;
|
|
|
|
|
QString guestOk;
|
|
|
|
|
|
|
|
|
|
const QStringList lines = output.split(QLatin1Char('\n'));
|
|
|
|
|
for (const QString &line : lines) {
|
|
|
|
|
if (line.startsWith(QLatin1Char('['))) {
|
|
|
|
|
// If we had a previous section that matched, break
|
|
|
|
|
if (!currentSection.isEmpty() && sharePath == m_path)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
// New section
|
|
|
|
|
currentSection = line.mid(1, line.indexOf(QLatin1Char(']')) - 1);
|
|
|
|
|
sharePath.clear();
|
|
|
|
|
shareAcl.clear();
|
|
|
|
|
guestOk.clear();
|
|
|
|
|
} else if (line.startsWith(QLatin1String("path="))) {
|
|
|
|
|
sharePath = line.mid(5);
|
|
|
|
|
} else if (line.startsWith(QLatin1String("usershare_acl="))) {
|
|
|
|
|
shareAcl = line.mid(14);
|
|
|
|
|
} else if (line.startsWith(QLatin1String("guest_ok="))) {
|
|
|
|
|
guestOk = line.mid(9);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if last section matched
|
|
|
|
|
if (sharePath != m_path)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// Found an existing share!
|
|
|
|
|
m_isShared = true;
|
|
|
|
|
m_originalShareName = currentSection;
|
|
|
|
|
m_shareCheckBox->setChecked(true);
|
|
|
|
|
m_nameEdit->setText(currentSection);
|
|
|
|
|
m_guestCheckBox->setChecked(guestOk == QLatin1String("y"));
|
|
|
|
|
|
|
|
|
|
// Parse ACL: "Everyone:R,username:F"
|
|
|
|
|
const QStringList aclEntries = shareAcl.split(QLatin1Char(','));
|
|
|
|
|
for (const QString &entry : aclEntries) {
|
|
|
|
|
const int colonPos = entry.lastIndexOf(QLatin1Char(':'));
|
|
|
|
|
if (colonPos < 0)
|
|
|
|
|
continue;
|
|
|
|
|
const QString name = entry.left(colonPos);
|
|
|
|
|
const QString perm = entry.mid(colonPos + 1);
|
|
|
|
|
|
|
|
|
|
for (auto &up : m_userPerms) {
|
|
|
|
|
if (up.username.compare(name, Qt::CaseInsensitive) == 0) {
|
|
|
|
|
up.combo->setCurrentIndex(indexForAcl(perm));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reset dirty state after loading
|
|
|
|
|
setDirty(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SmbSharePlugin::applyChanges()
|
|
|
|
|
{
|
|
|
|
|
const bool wantShared = m_shareCheckBox->isChecked();
|
|
|
|
|
const QString shareName = m_nameEdit->text().trimmed();
|
|
|
|
|
|
|
|
|
|
// If unchecked and was previously shared, delete the share
|
|
|
|
|
if (!wantShared && m_isShared) {
|
|
|
|
|
QProcess proc;
|
|
|
|
|
proc.start(QStringLiteral("net"),
|
|
|
|
|
{QStringLiteral("usershare"), QStringLiteral("delete"),
|
|
|
|
|
m_originalShareName});
|
|
|
|
|
proc.waitForFinished(5000);
|
|
|
|
|
m_isShared = false;
|
|
|
|
|
m_originalShareName.clear();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!wantShared)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// If name changed, delete old share first
|
|
|
|
|
if (m_isShared && !m_originalShareName.isEmpty()
|
|
|
|
|
&& m_originalShareName != shareName) {
|
|
|
|
|
QProcess proc;
|
|
|
|
|
proc.start(QStringLiteral("net"),
|
|
|
|
|
{QStringLiteral("usershare"), QStringLiteral("delete"),
|
|
|
|
|
m_originalShareName});
|
|
|
|
|
proc.waitForFinished(5000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build ACL string
|
|
|
|
|
QStringList aclParts;
|
|
|
|
|
for (const auto &up : m_userPerms) {
|
|
|
|
|
const QString acl = aclForIndex(up.combo->currentIndex());
|
|
|
|
|
if (!acl.isEmpty()) {
|
|
|
|
|
aclParts << up.username + QLatin1Char(':') + acl;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Default ACL if nothing selected
|
|
|
|
|
if (aclParts.isEmpty()) {
|
|
|
|
|
aclParts << QStringLiteral("Everyone:R");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QString aclString = aclParts.join(QLatin1Char(','));
|
|
|
|
|
const QString guestOk = m_guestCheckBox->isChecked()
|
|
|
|
|
? QStringLiteral("guest_ok=y")
|
|
|
|
|
: QStringLiteral("guest_ok=n");
|
|
|
|
|
|
|
|
|
|
// net usershare add <name> <path> <comment> <acl> <guest_ok>
|
|
|
|
|
QProcess proc;
|
|
|
|
|
proc.start(QStringLiteral("net"),
|
|
|
|
|
{QStringLiteral("usershare"), QStringLiteral("add"),
|
|
|
|
|
shareName, m_path, QString(), aclString, guestOk});
|
|
|
|
|
proc.waitForFinished(5000);
|
|
|
|
|
|
|
|
|
|
if (proc.exitCode() == 0) {
|
|
|
|
|
m_isShared = true;
|
|
|
|
|
m_originalShareName = shareName;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#include "smbshareplugin.moc"
|