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