1 /***************************************************************************
2     Copyright (C) 2006-2009 Robby Stephenson <robby@periapsis.org>
3  ***************************************************************************/
4 
5 /***************************************************************************
6  *                                                                         *
7  *   This program is free software; you can redistribute it and/or         *
8  *   modify it under the terms of the GNU General Public License as        *
9  *   published by the Free Software Foundation; either version 2 of        *
10  *   the License or (at your option) version 3 or any later version        *
11  *   accepted by the membership of KDE e.V. (or its successor approved     *
12  *   by the membership of KDE e.V.), which shall act as a proxy            *
13  *   defined in Section 14 of version 3 of the license.                    *
14  *                                                                         *
15  *   This program is distributed in the hope that it will be useful,       *
16  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
17  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
18  *   GNU General Public License for more details.                          *
19  *                                                                         *
20  *   You should have received a copy of the GNU General Public License     *
21  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
22  *                                                                         *
23  ***************************************************************************/
24 
25 #include "manager.h"
26 #include "newstuffadaptor.h"
27 #include "../core/filehandler.h"
28 #include "../utils/cursorsaver.h"
29 #include "../utils/tellico_utils.h"
30 #include "../tellico_debug.h"
31 
32 #include <KTar>
33 #include <KConfig>
34 #include <KFileItem>
35 #include <KConfigGroup>
36 #include <KSharedConfig>
37 #include <KDesktopFile>
38 #include <KIO/Job>
39 #include <KIO/DeleteJob>
40 
41 #include <QFileInfo>
42 #include <QDir>
43 #include <QStandardPaths>
44 #include <QTemporaryFile>
45 #include <QGlobalStatic>
46 
47 #include <sys/types.h>
48 #include <sys/stat.h>
49 // for msvc
50 #ifndef S_IXUSR
51 #define S_IXUSR 00100
52 #endif
53 
54 namespace Tellico {
55   namespace NewStuff {
56 
57 class ManagerSingleton {
58 public:
59   Tellico::NewStuff::Manager self;
60 };
61 
62   }
63 }
64 
65 Q_GLOBAL_STATIC(Tellico::NewStuff::ManagerSingleton, s_instance)
66 
67 using Tellico::NewStuff::Manager;
68 
Manager()69 Manager::Manager() : QObject(nullptr) {
70   QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.tellico"));
71   new NewstuffAdaptor(this);
72   QDBusConnection::sessionBus().registerObject(QStringLiteral("/NewStuff"), this);
73 }
74 
~Manager()75 Manager::~Manager() {
76   QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/NewStuff"));
77   auto interface = QDBusConnection::sessionBus().interface();
78   if(interface) {
79     // the windows build was crashing here when the interface was null
80     // see https://bugs.kde.org/show_bug.cgi?id=422468
81     interface->unregisterService(QStringLiteral("org.kde.tellico"));
82   }
83 }
84 
self()85 Manager* Manager::self() {
86   return &s_instance->self;
87 }
88 
installTemplate(const QString & file_)89 bool Manager::installTemplate(const QString& file_) {
90   if(file_.isEmpty()) {
91     return false;
92   }
93   GUI::CursorSaver cs;
94 
95   QString xslFile;
96   QStringList allFiles;
97 
98   bool success = true;
99 
100   // is there a better way to figure out if the url points to a XSL file or a tar archive
101   // than just trying to open it?
102   KTar archive(file_);
103   if(archive.open(QIODevice::ReadOnly)) {
104     const KArchiveDirectory* archiveDir = archive.directory();
105     archiveDir->copyTo(Tellico::saveLocation(QStringLiteral("entry-templates/")));
106 
107     allFiles = archiveFiles(archiveDir);
108     // remember files installed for template
109     xslFile = findXSL(archiveDir);
110   } else { // assume it's an xsl file
111     QString name = QFileInfo(file_).fileName();
112     if(!name.endsWith(QLatin1String(".xsl"))) {
113       name += QLatin1String(".xsl");
114     }
115     name.remove(QRegularExpression(QLatin1String("^\\d+-"))); // Remove possible kde-files.org id
116     name = Tellico::saveLocation(QStringLiteral("entry-templates/")) + name;
117     // Should overwrite since we might be upgrading
118     if(QFile::exists(name)) {
119       QFile::remove(name);
120     }
121     auto job = KIO::file_copy(QUrl::fromLocalFile(file_), QUrl::fromLocalFile(name));
122     if(job->exec()) {
123       xslFile = QFileInfo(name).fileName();
124       allFiles << xslFile;
125     }
126   }
127 
128   if(xslFile.isEmpty()) {
129     success = false;
130   } else {
131     KConfigGroup config(KSharedConfig::openConfig(), "KNewStuffFiles");
132     config.writeEntry(file_, allFiles);
133     config.writeEntry(xslFile, file_);
134   }
135   Tellico::checkCommonXSLFile();
136   return success;
137 }
138 
userTemplates()139 QMap<QString, QString> Manager::userTemplates() {
140   QDir dir(Tellico::saveLocation(QStringLiteral("entry-templates/")));
141   dir.setNameFilters(QStringList() << QStringLiteral("*.xsl"));
142   dir.setFilter(QDir::Files | QDir::Writable);
143   QStringList files = dir.entryList();
144   QMap<QString, QString> nameFileMap;
145   foreach(const QString& file, files) {
146     QString name = file;
147     name.truncate(file.length()-4); // remove ".xsl"
148     name.replace(QLatin1Char('_'), QLatin1Char(' '));
149     nameFileMap.insert(name, file);
150   }
151   return nameFileMap;
152 }
153 
removeTemplateByName(const QString & name_)154 bool Manager::removeTemplateByName(const QString& name_) {
155   if(name_.isEmpty()) {
156     return false;
157   }
158 
159   QString xslFile = userTemplates().value(name_);
160   if(!xslFile.isEmpty()) {
161     KConfigGroup config(KSharedConfig::openConfig(), "KNewStuffFiles");
162     QString file = config.readEntry(xslFile, QString());
163     if(!file.isEmpty()) {
164       return removeTemplate(file);
165     }
166     // At least remove xsl file
167     QFile::remove(Tellico::saveLocation(QStringLiteral("entry-templates/")) + xslFile);
168     return true;
169   }
170   return false;
171 }
172 
removeTemplate(const QString & file_)173 bool Manager::removeTemplate(const QString& file_) {
174   if(file_.isEmpty()) {
175     return false;
176   }
177   GUI::CursorSaver cs;
178 
179   KConfigGroup fileGroup(KSharedConfig::openConfig(), "KNewStuffFiles");
180   QStringList files = fileGroup.readEntry(file_, QStringList());
181 
182   if(files.isEmpty()) {
183     myWarning() << "No file list found for" << file_;
184     return false;
185   }
186 
187   bool success = true;
188   QString path = Tellico::saveLocation(QStringLiteral("entry-templates/"));
189   foreach(const QString& file, files) {
190     if(file.endsWith(QDir::separator())) {
191       // ok to not delete all directories
192       QDir().rmdir(path + file);
193     } else {
194       success = QFile::remove(path + file) && success;
195       if(!success) {
196         myDebug() << "Failed to remove" << (path+file);
197       }
198     }
199   }
200 
201   // remove config entries even if unsuccessful
202   fileGroup.deleteEntry(file_);
203   QString key = fileGroup.entryMap().key(file_);
204   fileGroup.deleteEntry(key);
205   KSharedConfig::openConfig()->sync();
206   return success;
207 }
208 
installScript(const QString & file_)209 bool Manager::installScript(const QString& file_) {
210   if(file_.isEmpty()) {
211     return false;
212   }
213   GUI::CursorSaver cs;
214 
215   QString realFile = file_;
216 
217   KTar archive(file_);
218   QString copyTarget = Tellico::saveLocation(QStringLiteral("data-sources/"));
219   QString scriptFolder;
220   QString exeFile;
221   QString sourceName;
222 
223   if(archive.open(QIODevice::ReadOnly)) {
224     const KArchiveDirectory* archiveDir = archive.directory();
225     exeFile = findEXE(archiveDir);
226     if(exeFile.isEmpty()) {
227       myDebug() << "No exe file found";
228       return false;
229     }
230     sourceName = QFileInfo(exeFile).baseName();
231     if(sourceName.isEmpty()) {
232       myDebug() << "Invalid packet name";
233       return false;
234     }
235     // package could have a top-level directory or not
236     // it should have a directory...
237     foreach(const QString& entry, archiveDir->entries()) {
238       if(entry.indexOf(QDir::separator()) < 0) {
239         // archive does have multiple root items -> extract own dir
240         copyTarget += sourceName;
241         scriptFolder = copyTarget + QDir::separator();
242         break;
243       }
244     }
245     if(scriptFolder.isEmpty()) { // one root item
246       scriptFolder = copyTarget + exeFile.left(exeFile.indexOf(QDir::separator())) + QDir::separator();
247     }
248     // overwrites stuff there
249     archiveDir->copyTo(copyTarget);
250   } else { // assume it's an script file
251     exeFile = QFileInfo(file_).fileName();
252 
253     exeFile.remove(QRegularExpression(QLatin1String("^\\d+-"))); // Remove possible kde-files.org id
254     sourceName = QFileInfo(exeFile).completeBaseName();
255     if(sourceName.isEmpty()) {
256       myDebug() << "Invalid packet name";
257       return false;
258     }
259     copyTarget += sourceName;
260     scriptFolder = copyTarget + QDir::separator();
261     QDir().mkpath(scriptFolder);
262     auto job = KIO::file_copy(QUrl::fromLocalFile(file_), QUrl::fromLocalFile(scriptFolder + exeFile));
263     if(!job->exec()) {
264       myDebug() << "Copy failed";
265       return false;
266     }
267     realFile = exeFile;
268   }
269 
270   QString specFile = scriptFolder + QFileInfo(exeFile).completeBaseName() + QLatin1String(".spec");
271   QString sourceExec = scriptFolder + exeFile;
272   QUrl dest = QUrl::fromLocalFile(sourceExec);
273   KFileItem item(dest);
274   item.setDelayedMimeTypes(true);
275   int out = ::chmod(QFile::encodeName(dest.path()).constData(), item.permissions() | S_IXUSR);
276   if(out != 0) {
277     myDebug() << "Failed to set permissions for" << dest.path();
278   }
279 
280   KDesktopFile df(specFile);
281   KConfigGroup cg = df.desktopGroup();
282   // update name
283   sourceName = cg.readEntry("Name", sourceName);
284   cg.writeEntry("ExecPath", sourceExec);
285   cg.writeEntry("NewStuffName", sourceName);
286   cg.writeEntry("DeleteOnRemove", true);
287 
288   KConfigGroup config(KSharedConfig::openConfig(), "KNewStuffFiles");
289   config.writeEntry(sourceName, realFile);
290   config.writeEntry(realFile, scriptFolder);
291   //  myDebug() << "exeFile = " << exeFile;
292   //  myDebug() << "sourceExec = " << info->sourceExec;
293   //  myDebug() << "sourceName = " << info->sourceName;
294   //  myDebug() << "specFile = " << info->specFile;
295   KConfigGroup configGroup(KSharedConfig::openConfig(), QStringLiteral("Data Sources"));
296   int nSources = configGroup.readEntry("Sources Count", 0);
297   config.writeEntry(sourceName + QLatin1String("_nbr"), nSources);
298   configGroup.writeEntry("Sources Count", nSources + 1);
299   KConfigGroup sourceGroup(KSharedConfig::openConfig(), QStringLiteral("Data Source %1").arg(nSources));
300   sourceGroup.writeEntry("Name", sourceName);
301   sourceGroup.writeEntry("ExecPath", sourceExec);
302   sourceGroup.writeEntry("DeleteOnRemove", true);
303   sourceGroup.writeEntry("Type", 5);
304   KSharedConfig::openConfig()->sync();
305   return true;
306 }
307 
removeScriptByName(const QString & name_)308 bool Manager::removeScriptByName(const QString& name_) {
309   if(name_.isEmpty()) {
310     return false;
311   }
312 
313   KConfigGroup config(KSharedConfig::openConfig(), "KNewStuffFiles");
314   QString file = config.readEntry(name_, QString());
315   if(!file.isEmpty()) {
316     return removeScript(file);
317   }
318   return false;
319 }
320 
removeScript(const QString & file_)321 bool Manager::removeScript(const QString& file_) {
322   if(file_.isEmpty()) {
323     return false;
324   }
325   GUI::CursorSaver cs;
326 
327   QFileInfo fi(file_);
328   const QString realFile = fi.fileName();
329   const QString sourceName = fi.completeBaseName();
330 
331   bool success = true;
332   KConfigGroup fileGroup(KSharedConfig::openConfig(), "KNewStuffFiles");
333   QString scriptFolder = fileGroup.readEntry(file_, QString());
334   if(scriptFolder.isEmpty()) {
335     scriptFolder = fileGroup.readEntry(realFile, QString());
336   }
337   int source = fileGroup.readEntry(file_ + QLatin1String("_nbr"), -1);
338   if(source == -1) {
339     source = fileGroup.readEntry(sourceName + QLatin1String("_nbr"), -1);
340   }
341 
342   if(!scriptFolder.isEmpty()) {
343     KIO::del(QUrl::fromLocalFile(scriptFolder))->exec();
344   }
345   if(source != -1) {
346     KConfigGroup configGroup(KSharedConfig::openConfig(), QStringLiteral("Data Sources"));
347     int nSources = configGroup.readEntry("Sources Count", 0);
348     configGroup.writeEntry("Sources Count", nSources - 1);
349     KConfigGroup sourceGroup(KSharedConfig::openConfig(), QStringLiteral("Data Source %1").arg(source));
350     sourceGroup.deleteGroup();
351   }
352 
353   // remove config entries even if unsuccessful
354   fileGroup.deleteEntry(file_);
355   QString key = fileGroup.entryMap().key(file_);
356   if(!key.isEmpty()) fileGroup.deleteEntry(key);
357   fileGroup.deleteEntry(realFile);
358   key = fileGroup.entryMap().key(realFile);
359   if(!key.isEmpty()) fileGroup.deleteEntry(key);
360   fileGroup.deleteEntry(file_ + QLatin1String("_nbr"));
361   fileGroup.deleteEntry(sourceName + QLatin1String("_nbr"));
362   KSharedConfig::openConfig()->sync();
363   return success;
364 }
365 
archiveFiles(const KArchiveDirectory * dir_,const QString & path_)366 QStringList Manager::archiveFiles(const KArchiveDirectory* dir_, const QString& path_) {
367   QStringList list;
368 
369   foreach(const QString& entry, dir_->entries()) {
370     const KArchiveEntry* curEntry = dir_->entry(entry);
371     if(curEntry->isFile()) {
372       list += path_ + entry;
373     } else if(curEntry->isDirectory()) {
374       list += archiveFiles(static_cast<const KArchiveDirectory*>(curEntry), path_ + entry + QDir::separator());
375       // add directory AFTER contents, since we delete from the top down later
376       list += path_ + entry + QDir::separator();
377     }
378   }
379 
380   return list;
381 }
382 
383 // don't recurse, the .xsl must be in top directory
findXSL(const KArchiveDirectory * dir_)384 QString Manager::findXSL(const KArchiveDirectory* dir_) {
385   foreach(const QString& entry, dir_->entries()) {
386     if(entry.endsWith(QLatin1String(".xsl"))) {
387       return entry;
388     }
389   }
390   return QString();
391 }
392 
findEXE(const KArchiveDirectory * dir_)393 QString Manager::findEXE(const KArchiveDirectory* dir_) {
394   QStack<const KArchiveDirectory*> dirStack;
395   QStack<QString> dirNameStack;
396 
397   dirStack.push(dir_);
398   dirNameStack.push(QString());
399 
400   do {
401     const QString dirName = dirNameStack.pop();
402     const KArchiveDirectory* curDir = dirStack.pop();
403     foreach(const QString& entry, curDir->entries()) {
404       const KArchiveEntry* archEntry = curDir->entry(entry);
405 
406       if(archEntry->isFile() && (archEntry->permissions() & S_IEXEC)) {
407         return dirName + entry;
408       } else if(archEntry->isDirectory()) {
409         dirStack.push(static_cast<const KArchiveDirectory*>(archEntry));
410         dirNameStack.push(dirName + entry + QDir::separator());
411       }
412     }
413   } while(!dirStack.isEmpty());
414 
415   return QString();
416 }
417