File indexing completed on 2024-05-12 05:09:53

0001 /***************************************************************************
0002     Copyright (C) 2006-2009 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include "manager.h"
0026 #include "newstuffadaptor.h"
0027 #include "../utils/cursorsaver.h"
0028 #include "../utils/tellico_utils.h"
0029 #include "../tellico_debug.h"
0030 
0031 #include <KTar>
0032 #include <KConfig>
0033 #include <KFileItem>
0034 #include <KConfigGroup>
0035 #include <KSharedConfig>
0036 #include <KDesktopFile>
0037 #include <KIO/Job>
0038 #include <KIO/DeleteJob>
0039 
0040 #include <QFileInfo>
0041 #include <QDir>
0042 #include <QStandardPaths>
0043 #include <QTemporaryFile>
0044 #include <QGlobalStatic>
0045 
0046 #include <sys/types.h>
0047 #include <sys/stat.h>
0048 // for msvc
0049 #ifndef S_IXUSR
0050 #define S_IXUSR 00100
0051 #endif
0052 
0053 namespace Tellico {
0054   namespace NewStuff {
0055 
0056 class ManagerSingleton {
0057 public:
0058   Tellico::NewStuff::Manager self;
0059 };
0060 
0061   }
0062 }
0063 
0064 Q_GLOBAL_STATIC(Tellico::NewStuff::ManagerSingleton, s_instance)
0065 
0066 using Tellico::NewStuff::Manager;
0067 
0068 Manager::Manager() : QObject(nullptr) {
0069   QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.tellico"));
0070   new NewstuffAdaptor(this);
0071   QDBusConnection::sessionBus().registerObject(QStringLiteral("/NewStuff"), this);
0072 }
0073 
0074 Manager::~Manager() {
0075   QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/NewStuff"));
0076   auto interface = QDBusConnection::sessionBus().interface();
0077   if(interface) {
0078     // the windows build was crashing here when the interface was null
0079     // see https://bugs.kde.org/show_bug.cgi?id=422468
0080     interface->unregisterService(QStringLiteral("org.kde.tellico"));
0081   }
0082 }
0083 
0084 Manager* Manager::self() {
0085   return &s_instance->self;
0086 }
0087 
0088 bool Manager::installTemplate(const QString& file_) {
0089   if(file_.isEmpty()) {
0090     return false;
0091   }
0092   GUI::CursorSaver cs;
0093 
0094   QString xslFile;
0095   QStringList allFiles;
0096 
0097   bool success = true;
0098 
0099   static const QRegularExpression digitsDashRx(QLatin1String("^\\d+-"));
0100   // is there a better way to figure out if the url points to a XSL file or a tar archive
0101   // than just trying to open it?
0102   KTar archive(file_);
0103   if(archive.open(QIODevice::ReadOnly)) {
0104     const KArchiveDirectory* archiveDir = archive.directory();
0105     archiveDir->copyTo(Tellico::saveLocation(QStringLiteral("entry-templates/")));
0106 
0107     allFiles = archiveFiles(archiveDir);
0108     // remember files installed for template
0109     xslFile = findXSL(archiveDir);
0110   } else { // assume it's an xsl file
0111     QString name = QFileInfo(file_).fileName();
0112     if(!name.endsWith(QLatin1String(".xsl"))) {
0113       name += QLatin1String(".xsl");
0114     }
0115     name.remove(digitsDashRx); // Remove possible kde-files.org id
0116     name = Tellico::saveLocation(QStringLiteral("entry-templates/")) + name;
0117     // Should overwrite since we might be upgrading
0118     if(QFile::exists(name)) {
0119       QFile::remove(name);
0120     }
0121     auto job = KIO::file_copy(QUrl::fromLocalFile(file_), QUrl::fromLocalFile(name));
0122     if(job->exec()) {
0123       xslFile = QFileInfo(name).fileName();
0124       allFiles << xslFile;
0125     }
0126   }
0127 
0128   if(xslFile.isEmpty()) {
0129     success = false;
0130   } else {
0131     KConfigGroup config(KSharedConfig::openConfig(), "KNewStuffFiles");
0132     config.writeEntry(file_, allFiles);
0133     config.writeEntry(xslFile, file_);
0134   }
0135   Tellico::checkCommonXSLFile();
0136   return success;
0137 }
0138 
0139 QMap<QString, QString> Manager::userTemplates() {
0140   QDir dir(Tellico::saveLocation(QStringLiteral("entry-templates/")));
0141   dir.setNameFilters(QStringList() << QStringLiteral("*.xsl"));
0142   dir.setFilter(QDir::Files | QDir::Writable);
0143   QStringList files = dir.entryList();
0144   QMap<QString, QString> nameFileMap;
0145   foreach(const QString& file, files) {
0146     QString name = file;
0147     name.truncate(file.length()-4); // remove ".xsl"
0148     name.replace(QLatin1Char('_'), QLatin1Char(' '));
0149     nameFileMap.insert(name, file);
0150   }
0151   return nameFileMap;
0152 }
0153 
0154 bool Manager::removeTemplateByName(const QString& name_) {
0155   if(name_.isEmpty()) {
0156     return false;
0157   }
0158 
0159   QString xslFile = userTemplates().value(name_);
0160   if(!xslFile.isEmpty()) {
0161     KConfigGroup config(KSharedConfig::openConfig(), "KNewStuffFiles");
0162     QString file = config.readEntry(xslFile, QString());
0163     if(!file.isEmpty()) {
0164       return removeTemplate(file);
0165     }
0166     // At least remove xsl file
0167     QFile::remove(Tellico::saveLocation(QStringLiteral("entry-templates/")) + xslFile);
0168     return true;
0169   }
0170   return false;
0171 }
0172 
0173 bool Manager::removeTemplate(const QString& file_) {
0174   if(file_.isEmpty()) {
0175     return false;
0176   }
0177   GUI::CursorSaver cs;
0178 
0179   KConfigGroup fileGroup(KSharedConfig::openConfig(), "KNewStuffFiles");
0180   QStringList files = fileGroup.readEntry(file_, QStringList());
0181 
0182   if(files.isEmpty()) {
0183     myWarning() << "No file list found for" << file_;
0184     return false;
0185   }
0186 
0187   bool success = true;
0188   QString path = Tellico::saveLocation(QStringLiteral("entry-templates/"));
0189   foreach(const QString& file, files) {
0190     if(file.endsWith(QDir::separator())) {
0191       // ok to not delete all directories
0192       QDir().rmdir(path + file);
0193     } else {
0194       success = QFile::remove(path + file) && success;
0195       if(!success) {
0196         myDebug() << "Failed to remove" << (path+file);
0197       }
0198     }
0199   }
0200 
0201   // remove config entries even if unsuccessful
0202   fileGroup.deleteEntry(file_);
0203   QString key = fileGroup.entryMap().key(file_);
0204   fileGroup.deleteEntry(key);
0205   KSharedConfig::openConfig()->sync();
0206   return success;
0207 }
0208 
0209 bool Manager::installScript(const QString& file_) {
0210   if(file_.isEmpty()) {
0211     return false;
0212   }
0213   GUI::CursorSaver cs;
0214 
0215   QString realFile = file_;
0216 
0217   KTar archive(file_);
0218   QString copyTarget = Tellico::saveLocation(QStringLiteral("data-sources/"));
0219   QString scriptFolder;
0220   QString exeFile;
0221   QString sourceName;
0222 
0223   static const QRegularExpression digitsDashRx(QLatin1String("^\\d+-"));
0224   if(archive.open(QIODevice::ReadOnly)) {
0225     const KArchiveDirectory* archiveDir = archive.directory();
0226     exeFile = findEXE(archiveDir);
0227     if(exeFile.isEmpty()) {
0228       myDebug() << "No exe file found";
0229       return false;
0230     }
0231     sourceName = QFileInfo(exeFile).baseName();
0232     if(sourceName.isEmpty()) {
0233       myDebug() << "Invalid packet name";
0234       return false;
0235     }
0236     // package could have a top-level directory or not
0237     // it should have a directory...
0238     foreach(const QString& entry, archiveDir->entries()) {
0239       if(entry.indexOf(QDir::separator()) < 0) {
0240         // archive does have multiple root items -> extract own dir
0241         copyTarget += sourceName;
0242         scriptFolder = copyTarget + QDir::separator();
0243         break;
0244       }
0245     }
0246     if(scriptFolder.isEmpty()) { // one root item
0247       scriptFolder = copyTarget + exeFile.left(exeFile.indexOf(QDir::separator())) + QDir::separator();
0248     }
0249     // overwrites stuff there
0250     archiveDir->copyTo(copyTarget);
0251   } else { // assume it's an script file
0252     exeFile = QFileInfo(file_).fileName();
0253 
0254     exeFile.remove(digitsDashRx); // Remove possible kde-files.org id
0255     sourceName = QFileInfo(exeFile).completeBaseName();
0256     if(sourceName.isEmpty()) {
0257       myDebug() << "Invalid packet name";
0258       return false;
0259     }
0260     copyTarget += sourceName;
0261     scriptFolder = copyTarget + QDir::separator();
0262     QDir().mkpath(scriptFolder);
0263     auto job = KIO::file_copy(QUrl::fromLocalFile(file_), QUrl::fromLocalFile(scriptFolder + exeFile));
0264     if(!job->exec()) {
0265       myDebug() << "Copy failed";
0266       return false;
0267     }
0268     realFile = exeFile;
0269   }
0270 
0271   QString specFile = scriptFolder + QFileInfo(exeFile).completeBaseName() + QLatin1String(".spec");
0272   QString sourceExec = scriptFolder + exeFile;
0273   QUrl dest = QUrl::fromLocalFile(sourceExec);
0274   KFileItem item(dest);
0275   item.setDelayedMimeTypes(true);
0276   int out = ::chmod(QFile::encodeName(dest.path()).constData(), item.permissions() | S_IXUSR);
0277   if(out != 0) {
0278     myDebug() << "Failed to set permissions for" << dest.path();
0279   }
0280 
0281   KDesktopFile df(specFile);
0282   KConfigGroup cg = df.desktopGroup();
0283   // update name
0284   sourceName = cg.readEntry("Name", sourceName);
0285   cg.writeEntry("ExecPath", sourceExec);
0286   cg.writeEntry("NewStuffName", sourceName);
0287   cg.writeEntry("DeleteOnRemove", true);
0288 
0289   KConfigGroup config(KSharedConfig::openConfig(), "KNewStuffFiles");
0290   config.writeEntry(sourceName, realFile);
0291   config.writeEntry(realFile, scriptFolder);
0292   //  myDebug() << "exeFile = " << exeFile;
0293   //  myDebug() << "sourceExec = " << info->sourceExec;
0294   //  myDebug() << "sourceName = " << info->sourceName;
0295   //  myDebug() << "specFile = " << info->specFile;
0296   KConfigGroup configGroup(KSharedConfig::openConfig(), QStringLiteral("Data Sources"));
0297   int nSources = configGroup.readEntry("Sources Count", 0);
0298   config.writeEntry(sourceName + QLatin1String("_nbr"), nSources);
0299   configGroup.writeEntry("Sources Count", nSources + 1);
0300   KConfigGroup sourceGroup(KSharedConfig::openConfig(), QStringLiteral("Data Source %1").arg(nSources));
0301   sourceGroup.writeEntry("Name", sourceName);
0302   sourceGroup.writeEntry("ExecPath", sourceExec);
0303   sourceGroup.writeEntry("DeleteOnRemove", true);
0304   sourceGroup.writeEntry("Type", 5);
0305   KSharedConfig::openConfig()->sync();
0306   return true;
0307 }
0308 
0309 bool Manager::removeScriptByName(const QString& name_) {
0310   if(name_.isEmpty()) {
0311     return false;
0312   }
0313 
0314   KConfigGroup config(KSharedConfig::openConfig(), "KNewStuffFiles");
0315   QString file = config.readEntry(name_, QString());
0316   if(!file.isEmpty()) {
0317     return removeScript(file);
0318   }
0319   return false;
0320 }
0321 
0322 bool Manager::removeScript(const QString& file_) {
0323   if(file_.isEmpty()) {
0324     return false;
0325   }
0326   GUI::CursorSaver cs;
0327 
0328   QFileInfo fi(file_);
0329   const QString realFile = fi.fileName();
0330   const QString sourceName = fi.completeBaseName();
0331 
0332   bool success = true;
0333   KConfigGroup fileGroup(KSharedConfig::openConfig(), "KNewStuffFiles");
0334   QString scriptFolder = fileGroup.readEntry(file_, QString());
0335   if(scriptFolder.isEmpty()) {
0336     scriptFolder = fileGroup.readEntry(realFile, QString());
0337   }
0338   int source = fileGroup.readEntry(file_ + QLatin1String("_nbr"), -1);
0339   if(source == -1) {
0340     source = fileGroup.readEntry(sourceName + QLatin1String("_nbr"), -1);
0341   }
0342 
0343   if(!scriptFolder.isEmpty()) {
0344     KIO::del(QUrl::fromLocalFile(scriptFolder))->exec();
0345   }
0346   if(source != -1) {
0347     KConfigGroup configGroup(KSharedConfig::openConfig(), QStringLiteral("Data Sources"));
0348     int nSources = configGroup.readEntry("Sources Count", 0);
0349     configGroup.writeEntry("Sources Count", nSources - 1);
0350     KConfigGroup sourceGroup(KSharedConfig::openConfig(), QStringLiteral("Data Source %1").arg(source));
0351     sourceGroup.deleteGroup();
0352   }
0353 
0354   // remove config entries even if unsuccessful
0355   fileGroup.deleteEntry(file_);
0356   QString key = fileGroup.entryMap().key(file_);
0357   if(!key.isEmpty()) fileGroup.deleteEntry(key);
0358   fileGroup.deleteEntry(realFile);
0359   key = fileGroup.entryMap().key(realFile);
0360   if(!key.isEmpty()) fileGroup.deleteEntry(key);
0361   fileGroup.deleteEntry(file_ + QLatin1String("_nbr"));
0362   fileGroup.deleteEntry(sourceName + QLatin1String("_nbr"));
0363   KSharedConfig::openConfig()->sync();
0364   return success;
0365 }
0366 
0367 QStringList Manager::archiveFiles(const KArchiveDirectory* dir_, const QString& path_) {
0368   QStringList list;
0369 
0370   foreach(const QString& entry, dir_->entries()) {
0371     const KArchiveEntry* curEntry = dir_->entry(entry);
0372     if(curEntry->isFile()) {
0373       list += path_ + entry;
0374     } else if(curEntry->isDirectory()) {
0375       list += archiveFiles(static_cast<const KArchiveDirectory*>(curEntry), path_ + entry + QDir::separator());
0376       // add directory AFTER contents, since we delete from the top down later
0377       list += path_ + entry + QDir::separator();
0378     }
0379   }
0380 
0381   return list;
0382 }
0383 
0384 // don't recurse, the .xsl must be in top directory
0385 QString Manager::findXSL(const KArchiveDirectory* dir_) {
0386   foreach(const QString& entry, dir_->entries()) {
0387     if(entry.endsWith(QLatin1String(".xsl"))) {
0388       return entry;
0389     }
0390   }
0391   return QString();
0392 }
0393 
0394 QString Manager::findEXE(const KArchiveDirectory* dir_) {
0395   QStack<const KArchiveDirectory*> dirStack;
0396   QStack<QString> dirNameStack;
0397 
0398   dirStack.push(dir_);
0399   dirNameStack.push(QString());
0400 
0401   do {
0402     const QString dirName = dirNameStack.pop();
0403     const KArchiveDirectory* curDir = dirStack.pop();
0404     foreach(const QString& entry, curDir->entries()) {
0405       const KArchiveEntry* archEntry = curDir->entry(entry);
0406 
0407       if(archEntry->isFile() && (archEntry->permissions() & S_IEXEC)) {
0408         return dirName + entry;
0409       } else if(archEntry->isDirectory()) {
0410         dirStack.push(static_cast<const KArchiveDirectory*>(archEntry));
0411         dirNameStack.push(dirName + entry + QDir::separator());
0412       }
0413     }
0414   } while(!dirStack.isEmpty());
0415 
0416   return QString();
0417 }