#include "ftpshareplugin.h" #include #include #include #include #include #include #include #include #include #include K_PLUGIN_CLASS_WITH_JSON(FtpSharePlugin, "ftpshareplugin.json") // Permission levels for the combo boxes static const QStringList permissionLabels = { QStringLiteral("---"), QStringLiteral("Read Only"), QStringLiteral("Read-Write") }; // Map combo index to ACL string static QString aclForIndex(int index) { switch (index) { case 1: return QStringLiteral("ro"); case 2: return QStringLiteral("rw"); default: return QString(); } } // Map ACL string back to combo index static int indexForAcl(const QString &acl) { if (acl == QLatin1String("ro")) return 1; if (acl == QLatin1String("rw")) return 2; return 0; } // Check if a virtual FTP user exists static bool ftpUserExists(const QString &username) { QProcess proc; proc.start(QStringLiteral("dolphin-ftp-share"), {QStringLiteral("userexists"), username}); proc.waitForFinished(5000); return proc.exitCode() == 0; } // Create a virtual FTP user with password static bool createFtpUser(const QString &username, const QString &password) { QProcess proc; proc.start(QStringLiteral("dolphin-ftp-share"), {QStringLiteral("passwd"), username, password}); proc.waitForFinished(5000); return proc.exitCode() == 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; } FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args) : KPropertiesDialogPlugin(qobject_cast(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 via FTP")); 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); // User permissions section auto *usersGroup = new QGroupBox(i18n("User Permissions")); 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 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::of(&QComboBox::currentIndexChanged), this, [this]() { setDirty(true); }); } scrollArea->setWidget(m_usersWidget); usersLayout->addWidget(scrollArea); mainLayout->addWidget(usersGroup); // Change password section auto *pwGroup = new QGroupBox(i18n("FTP Password")); auto *pwLayout = new QHBoxLayout(pwGroup); m_pwUserCombo = new QComboBox(); m_pwUserCombo->addItems(users); auto *pwButton = new QPushButton(i18n("Change Password...")); pwLayout->addWidget(m_pwUserCombo); pwLayout->addWidget(pwButton); mainLayout->addWidget(pwGroup); connect(pwButton, &QPushButton::clicked, this, &FtpSharePlugin::onChangePassword); mainLayout->addStretch(); // Connect signals connect(m_shareCheckBox, &QCheckBox::toggled, this, &FtpSharePlugin::onShareToggled); connect(m_nameEdit, &QLineEdit::textChanged, 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("FTP")); } void FtpSharePlugin::onShareToggled(bool checked) { m_nameEdit->setEnabled(checked); m_usersWidget->setEnabled(checked); setDirty(true); } void FtpSharePlugin::loadCurrentShare() { // Run: dolphin-ftp-share info QProcess proc; proc.start(QStringLiteral("dolphin-ftp-share"), {QStringLiteral("info")}); 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 // user_acl=user1:rw,user2:ro QString currentSection; QString sharePath; QString shareAcl; 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(); } else if (line.startsWith(QLatin1String("path="))) { sharePath = line.mid(5); } else if (line.startsWith(QLatin1String("user_acl="))) { shareAcl = 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); // Parse ACL: "user1:rw,user2:ro" if (!shareAcl.isEmpty()) { 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 FtpSharePlugin::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("dolphin-ftp-share"), {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("dolphin-ftp-share"), {QStringLiteral("delete"), m_originalShareName}); proc.waitForFinished(5000); } // Create FTP users for users that have permissions but no FTP account yet for (const auto &up : m_userPerms) { if (up.combo->currentIndex() == 0) continue; // No permission set, skip if (ftpUserExists(up.username)) continue; // Already has FTP account // Ask for FTP password bool ok = false; const QString password = QInputDialog::getText( m_page, i18n("FTP Password"), i18n("Set FTP password for user '%1':", up.username), QLineEdit::Password, QString(), &ok); if (!ok || password.isEmpty()) { QMessageBox::warning(m_page, i18n("FTP Share"), i18n("No password set for '%1'. This user will be skipped.", up.username)); up.combo->setCurrentIndex(0); continue; } if (!createFtpUser(up.username, password)) { QMessageBox::warning(m_page, i18n("FTP Share"), i18n("Failed to create FTP user '%1'.", up.username)); up.combo->setCurrentIndex(0); continue; } } // 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; } } const QString aclString = aclParts.join(QLatin1Char(',')); // dolphin-ftp-share add QProcess proc; proc.start(QStringLiteral("dolphin-ftp-share"), {QStringLiteral("add"), shareName, m_path, aclString}); proc.waitForFinished(5000); if (proc.exitCode() == 0) { m_isShared = true; m_originalShareName = shareName; } } void FtpSharePlugin::onChangePassword() { const QString username = m_pwUserCombo->currentText(); if (username.isEmpty()) return; if (!ftpUserExists(username)) { QMessageBox::information(m_page, i18n("FTP Password"), i18n("User '%1' has no FTP account yet.\nSet permissions for this user first.", username)); return; } bool ok = false; const QString password = QInputDialog::getText( m_page, i18n("FTP Password"), i18n("New FTP password for '%1':", username), QLineEdit::Password, QString(), &ok); if (!ok || password.isEmpty()) return; if (createFtpUser(username, password)) { QMessageBox::information(m_page, i18n("FTP Password"), i18n("Password for '%1' updated.", username)); } else { QMessageBox::warning(m_page, i18n("FTP Password"), i18n("Failed to update password for '%1'.", username)); } } #include "ftpshareplugin.moc"