File indexing completed on 2024-03-24 17:13:50

0001 /*
0002     SPDX-FileCopyrightText: 2019 Filip Fila <filipfila.kde@gmail.com>
0003     SPDX-FileCopyrightText: 2013 Reza Fatahilah Shah <rshah0385@kireihana.com>
0004     SPDX-FileCopyrightText: 2011, 2012 David Edmundson <kde@davidedmundson.co.uk>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 #include "sddmauthhelper.h"
0009 #include "src/config.h"
0010 
0011 #include <unistd.h>
0012 
0013 #include <QDebug>
0014 #include <QDir>
0015 #include <QFile>
0016 #include <QFileInfo>
0017 #include <QMimeDatabase>
0018 #include <QMimeType>
0019 #include <QSharedPointer>
0020 
0021 #include <KArchive>
0022 #include <KConfig>
0023 #include <KConfigGroup>
0024 #include <KLazyLocalizedString>
0025 #include <KLocalizedString>
0026 #include <KTar>
0027 #include <KUser>
0028 #include <KZip>
0029 
0030 static QSharedPointer<KConfig> openConfig(const QString &filePath)
0031 {
0032     // if the sddm.conf.d folder doesn't exist we fail to set the right permissions for kde_settings.conf
0033     QFileInfo fileLocation(filePath);
0034     QDir dir(fileLocation.absolutePath());
0035     if (!dir.exists()) {
0036         QDir().mkpath(dir.path());
0037     }
0038     QFile file(filePath);
0039     if (!file.exists()) {
0040         // If we are creating the config file, ensure it is world-readable: if
0041         // we don't do that, KConfig will create a file which is only readable
0042         // by root
0043         file.open(QIODevice::WriteOnly);
0044         file.close();
0045         file.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadGroup | QFile::ReadOther);
0046     }
0047     // in case the file has already been created with wrong permissions
0048     else if (!(file.permissions() & QFile::ReadOwner & QFile::WriteOwner & QFile::ReadGroup & QFile::ReadOther)) {
0049         file.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadGroup | QFile::ReadOther);
0050     }
0051 
0052     return QSharedPointer<KConfig>(new KConfig(file.fileName(), KConfig::SimpleConfig));
0053 }
0054 
0055 void SddmAuthHelper::copyDirectoryRecursively(const QString &source, const QString &destination, QSet<QString> &done)
0056 {
0057     if (done.contains(source)) {
0058         return;
0059     }
0060     done.insert(source);
0061 
0062     const QDir sourceDir(source);
0063     const auto entries = sourceDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files);
0064     for (const auto &entry : entries) {
0065         const auto destinationPath = destination + '/' + entry.fileName();
0066         if (entry.isFile()) {
0067             copyFile(entry.absoluteFilePath(), destinationPath);
0068         } else {
0069             QDir().mkpath(destinationPath);
0070             copyDirectoryRecursively(entry.absoluteFilePath(), destinationPath, done);
0071         }
0072     }
0073 }
0074 
0075 void SddmAuthHelper::copyFile(const QString &source, const QString &destination)
0076 {
0077     KUser sddmUser(QStringLiteral("sddm"));
0078 
0079     if (QFile::exists(destination)) {
0080         QFile::remove(destination);
0081     }
0082 
0083     if (!QFile::copy(source, destination)) {
0084         qWarning() << "Could not copy" << source << "to" << destination;
0085     }
0086     const char *destinationConverted = destination.toLocal8Bit().data();
0087     if (chown(destinationConverted, sddmUser.userId().nativeId(), sddmUser.groupId().nativeId())) {
0088         return;
0089     }
0090 }
0091 
0092 ActionReply SddmAuthHelper::sync(const QVariantMap &args)
0093 {
0094     // initial check for sddm user; abort if user not present
0095     // we have to check with QString and isEmpty() instead of QDir and exists() because
0096     // QDir returns "." and true for exists() in the case of a non-existent user;
0097     QString sddmHomeDirPath = KUser("sddm").homeDir();
0098     if (sddmHomeDirPath.isEmpty()) {
0099         qDebug() << "Cannot proceed, user 'sddm' does not exist. Please check your SDDM install.";
0100         return ActionReply::HelperErrorReply();
0101     }
0102 
0103     // In plasma-framework, ThemePrivate::useCache documents the requirement to
0104     // clear the cache when colors change while the app that uses them isn't running;
0105     // that condition applies to the SDDM greeter here, so clear the cache if it
0106     // exists to make sure SDDM has a fresh state
0107     QDir sddmCacheLocation(sddmHomeDirPath + QStringLiteral("/.cache"));
0108     if (sddmCacheLocation.exists()) {
0109         sddmCacheLocation.removeRecursively();
0110     }
0111 
0112     // create SDDM config directory if it does not exist
0113     QDir sddmConfigLocation(sddmHomeDirPath + QStringLiteral("/.config"));
0114     if (!sddmConfigLocation.exists()) {
0115         QDir().mkpath(sddmConfigLocation.path());
0116     }
0117 
0118     // copy fontconfig (font, font rendering)
0119     if (!args[QStringLiteral("fontconfig")].isNull()) {
0120         QDir fontconfigSource(args[QStringLiteral("fontconfig")].toString());
0121         QStringList sourceFileEntries = fontconfigSource.entryList(QDir::Files);
0122         QStringList sourceDirEntries = fontconfigSource.entryList(QDir::AllDirs);
0123         QDir fontconfigDestination(sddmConfigLocation.path() + QStringLiteral("/fontconfig"));
0124 
0125         if (!fontconfigDestination.exists()) {
0126             QDir().mkpath(fontconfigDestination.path());
0127         }
0128 
0129         if (sourceDirEntries.count() != 0) {
0130             for (int i = 0; i < sourceDirEntries.count(); i++) {
0131                 QString directoriesSource = fontconfigSource.path() + QDir::separator() + sourceDirEntries[i];
0132                 QString directoriesDestination = fontconfigDestination.path() + QDir::separator() + sourceDirEntries[i];
0133                 fontconfigSource.mkpath(directoriesDestination);
0134                 copyFile(directoriesSource, directoriesDestination);
0135             }
0136         }
0137 
0138         if (sourceFileEntries.count() != 0) {
0139             for (int i = 0; i < sourceFileEntries.count(); i++) {
0140                 QString filesSource = fontconfigSource.path() + QDir::separator() + sourceFileEntries[i];
0141                 QString filesDestination = fontconfigDestination.path() + QDir::separator() + sourceFileEntries[i];
0142                 copyFile(filesSource, filesDestination);
0143             }
0144         }
0145     }
0146 
0147     // copy kdeglobals (color scheme)
0148     if (!args[QStringLiteral("kdeglobals")].isNull()) {
0149         QDir kdeglobalsSource(args[QStringLiteral("kdeglobals")].toString());
0150         QDir kdeglobalsDestination(sddmConfigLocation.path() + QStringLiteral("/kdeglobals"));
0151         copyFile(kdeglobalsSource.path(), kdeglobalsDestination.path());
0152     }
0153 
0154     // copy plasmarc (icons, UI style)
0155     if (!args[QStringLiteral("plasmarc")].isNull()) {
0156         QDir plasmarcSource(args[QStringLiteral("plasmarc")].toString());
0157         QDir plasmarcDestination(sddmConfigLocation.path() + QStringLiteral("/plasmarc"));
0158         copyFile(plasmarcSource.path(), plasmarcDestination.path());
0159     }
0160 
0161     // copy kscreen config
0162     if (!args[QStringLiteral("kscreen-config")].isNull()) {
0163         const QString destinationDir = sddmHomeDirPath + "/.local/share/kscreen/";
0164         QSet<QString> done;
0165         copyDirectoryRecursively(args[QStringLiteral("kscreen-config")].toString(), destinationDir, done);
0166     }
0167 
0168     // write cursor theme, NumLock preference, and scaling DPI to config file
0169     ActionReply reply = ActionReply::HelperErrorReply();
0170     QSharedPointer<KConfig> sddmConfig = openConfig(args[QStringLiteral("kde_settings.conf")].toString());
0171     QSharedPointer<KConfig> sddmOldConfig = openConfig(args[QStringLiteral("sddm.conf")].toString());
0172 
0173     QMap<QString, QVariant>::const_iterator iterator;
0174 
0175     for (iterator = args.constBegin(); iterator != args.constEnd(); ++iterator) {
0176         if (iterator.key() == QLatin1String("kde_settings.conf")) {
0177             continue;
0178         }
0179 
0180         QStringList configFields = iterator.key().split(QLatin1Char('/'));
0181         if (configFields.size() != 3) {
0182             continue;
0183         }
0184 
0185         QSharedPointer<KConfig> config;
0186         QString fileName = configFields[0];
0187         QString groupName = configFields[1];
0188         QString keyName = configFields[2];
0189 
0190         if (fileName == QLatin1String("kde_settings.conf") && iterator.value().isValid()) {
0191             sddmConfig->group(groupName).writeEntry(keyName, iterator.value());
0192             sddmOldConfig->group(groupName).deleteEntry(keyName);
0193         }
0194     }
0195 
0196     sddmOldConfig->sync();
0197     sddmConfig->sync();
0198 
0199     return ActionReply::SuccessReply();
0200 }
0201 
0202 ActionReply SddmAuthHelper::reset(const QVariantMap &args)
0203 {
0204     // initial check for sddm user; abort if user not present
0205     // we have to check with QString and isEmpty() instead of QDir and exists() because
0206     // QDir returns "." and true for exists() in the case of a non-existent user;
0207     QString sddmHomeDirPath = KUser("sddm").homeDir();
0208     if (sddmHomeDirPath.isEmpty()) {
0209         qDebug() << "Cannot proceed, user 'sddm' does not exist. Please check your SDDM install.";
0210         return ActionReply::HelperErrorReply();
0211     }
0212 
0213     QDir sddmConfigLocation(sddmHomeDirPath + QStringLiteral("/.config"));
0214     QDir fontconfigDir(args[QStringLiteral("sddmUserConfig")].toString() + QStringLiteral("/fontconfig"));
0215 
0216     fontconfigDir.removeRecursively();
0217     QFile::remove(sddmConfigLocation.path() + QStringLiteral("/kdeglobals"));
0218     QFile::remove(sddmConfigLocation.path() + QStringLiteral("/plasmarc"));
0219 
0220     QDir(sddmHomeDirPath + "/.local/share/kscreen/").removeRecursively();
0221 
0222     // remove cursor theme, NumLock preference, and scaling DPI from config file
0223     ActionReply reply = ActionReply::HelperErrorReply();
0224     QSharedPointer<KConfig> sddmConfig = openConfig(args[QStringLiteral("kde_settings.conf")].toString());
0225     QSharedPointer<KConfig> sddmOldConfig = openConfig(args[QStringLiteral("sddm.conf")].toString());
0226 
0227     QMap<QString, QVariant>::const_iterator iterator;
0228 
0229     for (iterator = args.constBegin(); iterator != args.constEnd(); ++iterator) {
0230         if (iterator.key() == QLatin1String("kde_settings.conf")) {
0231             continue;
0232         }
0233 
0234         QStringList configFields = iterator.key().split(QLatin1Char('/'));
0235         if (configFields.size() != 3) {
0236             continue;
0237         }
0238 
0239         QSharedPointer<KConfig> config;
0240         QString fileName = configFields[0];
0241         QString groupName = configFields[1];
0242         QString keyName = configFields[2];
0243 
0244         if (fileName == QLatin1String("kde_settings.conf")) {
0245             sddmConfig->group(groupName).deleteEntry(keyName);
0246             sddmOldConfig->group(groupName).deleteEntry(keyName);
0247         }
0248     }
0249 
0250     sddmOldConfig->sync();
0251     sddmConfig->sync();
0252 
0253     return ActionReply::SuccessReply();
0254 }
0255 
0256 ActionReply SddmAuthHelper::save(const QVariantMap &args)
0257 {
0258     ActionReply reply = ActionReply::HelperErrorReply();
0259     QSharedPointer<KConfig> sddmConfig = openConfig(QString{QLatin1String(SDDM_CONFIG_DIR "/") + QStringLiteral("kde_settings.conf")});
0260     QSharedPointer<KConfig> sddmOldConfig = openConfig(QStringLiteral(SDDM_CONFIG_FILE));
0261     QSharedPointer<KConfig> themeConfig;
0262     QString themeConfigFile = args[QStringLiteral("theme.conf.user")].toString();
0263 
0264     if (!themeConfigFile.isEmpty()) {
0265         themeConfig = openConfig(themeConfigFile);
0266     }
0267 
0268     QMap<QString, QVariant>::const_iterator iterator;
0269 
0270     for (iterator = args.constBegin(); iterator != args.constEnd(); ++iterator) {
0271         if (iterator.key() == QLatin1String("kde_settings.conf") || iterator.key() == QLatin1String("theme.conf.user")) {
0272             continue;
0273         }
0274 
0275         QStringList configFields = iterator.key().split(QLatin1Char('/'));
0276         if (configFields.size() != 3) {
0277             continue;
0278         }
0279 
0280         QString fileName = configFields[0];
0281         QString groupName = configFields[1];
0282         QString keyName = configFields[2];
0283 
0284         // if there is an identical keyName in "sddm.conf" we want to delete it so SDDM doesn't read from the old file
0285         // hierarchically SDDM prefers "etc/sddm.conf" to "/etc/sddm.conf.d/some_file.conf"
0286 
0287         if (fileName == QLatin1String("kde_settings.conf")) {
0288             sddmConfig->group(groupName).writeEntry(keyName, iterator.value());
0289             sddmOldConfig->group(groupName).deleteEntry(keyName);
0290         } else if (fileName == QLatin1String("theme.conf.user") && !themeConfig.isNull()) {
0291             QFileInfo themeConfigFileInfo(themeConfigFile);
0292             QDir configRootDirectory = themeConfigFileInfo.absoluteDir();
0293 
0294             if (keyName == QLatin1String("background")) {
0295                 QFileInfo newBackgroundFileInfo(iterator.value().toString());
0296                 QString previousBackground = themeConfig->group(groupName).readEntry(keyName);
0297 
0298                 bool backgroundChanged = newBackgroundFileInfo.fileName() != previousBackground;
0299                 if (backgroundChanged) {
0300                     if (!previousBackground.isEmpty()) {
0301                         QString previousBackgroundPath = configRootDirectory.filePath(previousBackground);
0302                         if (QFile::remove(previousBackgroundPath)) {
0303                             qDebug() << "Removed previous background " << previousBackgroundPath;
0304                         }
0305                     }
0306 
0307                     if (newBackgroundFileInfo.exists()) {
0308                         QString newBackgroundPath = configRootDirectory.filePath(newBackgroundFileInfo.fileName());
0309                         qDebug() << "Copying background from " << newBackgroundFileInfo.absoluteFilePath() << " to " << newBackgroundPath;
0310                         if (QFile::copy(newBackgroundFileInfo.absoluteFilePath(), newBackgroundPath)) {
0311                             QFile::setPermissions(newBackgroundPath, QFile::ReadOwner | QFile::WriteOwner | QFile::ReadGroup | QFile::ReadOther);
0312                             themeConfig->group(groupName).writeEntry(keyName, newBackgroundFileInfo.fileName());
0313                         }
0314                     } else {
0315                         themeConfig->group(groupName).deleteEntry(keyName);
0316                     }
0317                 }
0318             } else {
0319                 themeConfig->group(groupName).writeEntry(keyName, iterator.value());
0320             }
0321         }
0322     }
0323 
0324     sddmOldConfig->sync();
0325     sddmConfig->sync();
0326 
0327     if (!themeConfig.isNull()) {
0328         themeConfig->sync();
0329     }
0330 
0331     return ActionReply::SuccessReply();
0332 }
0333 
0334 ActionReply SddmAuthHelper::installtheme(const QVariantMap &args)
0335 {
0336     const QString filePath = args[QStringLiteral("filePath")].toString();
0337     if (filePath.isEmpty()) {
0338         return ActionReply::HelperErrorReply();
0339     }
0340 
0341     const QString themesBaseDir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sddm/themes"), QStandardPaths::LocateDirectory);
0342     QDir dir(themesBaseDir);
0343     if (!dir.exists()) {
0344         return ActionReply::HelperErrorReply();
0345     }
0346 
0347     qDebug() << "Installing " << filePath << " into " << themesBaseDir;
0348 
0349     if (!QFile::exists(filePath)) {
0350         return ActionReply::HelperErrorReply();
0351     }
0352 
0353     QMimeDatabase db;
0354     QMimeType mimeType = db.mimeTypeForFile(filePath);
0355     qWarning() << "Postinstallation: uncompress the file";
0356 
0357     QScopedPointer<KArchive> archive;
0358 
0359     // there must be a better way to do this? If not, make a static bool KZip::supportsMimeType(const QMimeType &type); ?
0360     // or even a factory class in KArchive
0361 
0362     if (mimeType.inherits(QStringLiteral("application/zip"))) {
0363         archive.reset(new KZip(filePath));
0364     } else if (mimeType.inherits(QStringLiteral("application/tar")) || mimeType.inherits(QStringLiteral("application/x-gzip"))
0365                || mimeType.inherits(QStringLiteral("application/x-bzip")) || mimeType.inherits(QStringLiteral("application/x-lzma"))
0366                || mimeType.inherits(QStringLiteral("application/x-xz")) || mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar"))
0367                || mimeType.inherits(QStringLiteral("application/x-compressed-tar"))) {
0368         archive.reset(new KTar(filePath));
0369     } else {
0370         auto e = ActionReply::HelperErrorReply();
0371         e.setErrorDescription(kli18n("Invalid theme package").untranslatedText());
0372         return e;
0373     }
0374 
0375     if (!archive->open(QIODevice::ReadOnly)) {
0376         auto e = ActionReply::HelperErrorReply();
0377         e.setErrorDescription(kli18n("Could not open file").untranslatedText());
0378         return e;
0379     }
0380 
0381     auto directory = archive->directory();
0382 
0383     QStringList installedPaths;
0384 
0385     // some basic validation
0386     // the top level should only have folders, and those folders should contain a valid metadata.desktop file
0387     // if we get anything else, abort everything before copying
0388     const auto entries = directory->entries();
0389     for (const QString &name : entries) {
0390         auto entry = directory->entry(name);
0391         if (!entry->isDirectory()) {
0392             auto e = ActionReply::HelperErrorReply();
0393             e.setErrorDescription(kli18n("Invalid theme package").untranslatedText());
0394             return e;
0395         }
0396         auto subDirectory = static_cast<const KArchiveDirectory *>(entry);
0397         auto metadataFile = subDirectory->file(QStringLiteral("metadata.desktop"));
0398         if (!metadataFile || !metadataFile->data().contains("[SddmGreeterTheme]")) {
0399             auto e = ActionReply::HelperErrorReply();
0400             e.setErrorDescription(kli18n("Invalid theme package").untranslatedText());
0401             return e;
0402         }
0403         installedPaths.append(themesBaseDir + QLatin1Char('/') + name);
0404     }
0405 
0406     if (!directory->copyTo(themesBaseDir)) {
0407         auto e = ActionReply::HelperErrorReply();
0408         e.setErrorDescription(kli18n("Could not decompress archive").untranslatedText());
0409         return e;
0410     }
0411 
0412     auto rc = ActionReply::SuccessReply();
0413     rc.addData(QStringLiteral("installedPaths"), installedPaths);
0414     return rc;
0415 }
0416 
0417 ActionReply SddmAuthHelper::uninstalltheme(const QVariantMap &args)
0418 {
0419     const QString themePath = args[QStringLiteral("filePath")].toString();
0420     const QString themesBaseDir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sddm/themes"), QStandardPaths::LocateDirectory);
0421 
0422     QDir dir(themePath);
0423     if (!dir.exists()) {
0424         return ActionReply::HelperErrorReply();
0425     }
0426 
0427     // validate the themePath is directly inside the themesBaseDir
0428     QDir baseDir(themesBaseDir);
0429     if (baseDir.absoluteFilePath(dir.dirName()) != dir.absolutePath()) {
0430         return ActionReply::HelperErrorReply();
0431     }
0432 
0433     if (!dir.removeRecursively()) {
0434         return ActionReply::HelperErrorReply();
0435     }
0436 
0437     return ActionReply::SuccessReply();
0438 }
0439 
0440 KAUTH_HELPER_MAIN("org.kde.kcontrol.kcmsddm", SddmAuthHelper)
0441 
0442 #include "moc_sddmauthhelper.cpp"