File indexing completed on 2022-06-02 20:22:00

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