1/******************************************************************************
2* Copyright 2007 by Aaron Seigo <aseigo@kde.org> *
3* Copyright 2007 by Riccardo Iaconelli <riccardo@kde.org> *
4* *
5* This library is free software; you can redistribute it and/or *
6* modify it under the terms of the GNU Library General Public *
7* License as published by the Free Software Foundation; either *
8* version 2 of the License, or (at your option) any later version. *
9* *
10* This library is distributed in the hope that it will be useful, *
11* but WITHOUT ANY WARRANTY; without even the implied warranty of *
12* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
13* Library General Public License for more details. *
14* *
15* You should have received a copy of the GNU Library General Public License *
16* along with this library; see the file COPYING.LIB. If not, write to *
17* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, *
18* Boston, MA 02110-1301, USA. *
19*******************************************************************************/
20
21#include "package.h"
22#include "config-plasma.h"
23
24#include <QDir>
25#include <QFile>
26#include <QRegExp>
27#include <QtNetwork/QHostInfo>
28
29#ifdef QCA2_FOUND
30#include <QtCrypto>
31#endif
32
33#include <karchive.h>
34#include <kcomponentdata.h>
35#include <kdesktopfile.h>
36#include <kmimetype.h>
37#include <kplugininfo.h>
38#include <kstandarddirs.h>
39#include <ktar.h>
40#include <ktempdir.h>
41#include <ktemporaryfile.h>
42#include <kzip.h>
43#include <kdebug.h>
44
45#include "authorizationmanager.h"
46#include "packagemetadata.h"
47#include "private/authorizationmanager_p.h"
48#include "private/package_p.h"
49#include "private/plasmoidservice_p.h"
50#include "private/service_p.h"
51
52namespace Plasma
53{
54
55bool copyFolder(QString sourcePath, QString targetPath)
56{
57 QDir source(sourcePath);
58 if(!source.exists())
59 return false;
60
61 QDir target(targetPath);
62 if(!target.exists()) {
63 QString targetName = target.dirName();
64 target.cdUp();
65 target.mkdir(targetName);
66 target = QDir(targetPath);
67 }
68
69 foreach (const QString &fileName, source.entryList(QDir::Files)) {
70 QString sourceFilePath = sourcePath + QDir::separator() + fileName;
71 QString targetFilePath = targetPath + QDir::separator() + fileName;
72
73 if (!QFile::copy(sourceFilePath, targetFilePath)) {
74 return false;
75 }
76 }
77
78 foreach (const QString &subFolderName, source.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) {
79 QString sourceSubFolderPath = sourcePath + QDir::separator() + subFolderName;
80 QString targetSubFolderPath = targetPath + QDir::separator() + subFolderName;
81
82 if (!copyFolder(sourceSubFolderPath, targetSubFolderPath)) {
83 return false;
84 }
85 }
86
87 return true;
88}
89
90bool removeFolder(QString folderPath)
91{
92 QDir folder(folderPath);
93 if(!folder.exists())
94 return false;
95
96 foreach (const QString &fileName, folder.entryList(QDir::Files)) {
97 if (!QFile::remove(folderPath + QDir::separator() + fileName)) {
98 return false;
99 }
100 }
101
102 foreach (const QString &subFolderName, folder.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) {
103 if (!removeFolder(folderPath + QDir::separator() + subFolderName)) {
104 return false;
105 }
106 }
107
108 QString folderName = folder.dirName();
109 folder.cdUp();
110 return folder.rmdir(folderName);
111}
112
113Package::Package()
114 : d(new PackagePrivate(PackageStructure::Ptr(0), QString()))
115{
116}
117
118Package::Package(const QString &packageRoot, const QString &package,
119 PackageStructure::Ptr structure)
120 : d(new PackagePrivate(structure, packageRoot, package))
121{
122}
123
124Package::Package(const QString &packagePath, PackageStructure::Ptr structure)
125 : d(new PackagePrivate(structure, packagePath))
126{
127}
128
129Package::Package(const Package &other)
130 : d(new PackagePrivate(*other.d))
131{
132}
133
134Package::~Package()
135{
136 delete d;
137}
138
139Package &Package::operator=(const Package &rhs)
140{
141 if (&rhs != this) {
142 *d = *rhs.d;
143 }
144
145 return *this;
146}
147
148bool Package::isValid() const
149{
150 return d->isValid();
151}
152
153bool PackagePrivate::isValid()
154{
155 if (!valid || !structure) {
156 return false;
157 }
158
159 //search for the file in all prefixes and in all possible paths for each prefix
160 //even if it's a big nested loop, usually there is one prefix and one location
161 //so shouldn't cause too much disk access
162 QStringList prefixes = structure->contentsPrefixPaths();
163 if (prefixes.isEmpty()) {
164 prefixes << QString();
165 }
166
167 foreach (const char *dir, structure->requiredDirectories()) {
168 bool failed = true;
169 foreach (const QString &path, structure->searchPath(dir)) {
170 foreach (const QString &prefix, prefixes) {
171 if (QFile::exists(structure->path() + prefix + path)) {
172 failed = false;
173 break;
174 }
175 }
176 if (!failed) {
177 break;
178 }
179 }
180
181 if (failed) {
182 kWarning() << "Could not find required directory" << dir;
183 valid = false;
184 return false;
185 }
186 }
187
188 foreach (const char *file, structure->requiredFiles()) {
189 bool failed = true;
190 foreach (const QString &path, structure->searchPath(file)) {
191 foreach (const QString &prefix, prefixes) {
192 if (QFile::exists(structure->path() + prefix + path)) {
193 failed = false;
194 break;
195 }
196 }
197 if (!failed) {
198 break;
199 }
200 }
201
202 if (failed) {
203 kWarning() << "Could not find required file" << file;
204 valid = false;
205 return false;
206 }
207 }
208
209 valid = true;
210 return true;
211}
212
213QString Package::filePath(const char *fileType, const QString &filename) const
214{
215 if (!d->valid) {
216 //kDebug() << "package is not valid";
217 return QString();
218 }
219
220 QStringList paths;
221
222 if (qstrlen(fileType) != 0) {
223 paths = d->structure->searchPath(fileType);
224
225 if (paths.isEmpty()) {
226 //kDebug() << "no matching path came of it, while looking for" << fileType << filename;
227 return QString();
228 }
229 } else {
230 //when filetype is empty paths is always empty, so try with an empty string
231 paths << QString();
232 }
233
234 //Nested loop, but in the medium case resolves to just one iteration
235 QStringList prefixes = d->structure->contentsPrefixPaths();
236 if (prefixes.isEmpty()) {
237 prefixes << QString();
238 }
239
240 //kDebug() << "prefixes:" << prefixes.count() << prefixes;
241 foreach (const QString &contentsPrefix, prefixes) {
242 const QString prefix(d->structure->path() + contentsPrefix);
243
244 foreach (const QString &path, paths) {
245 QString file = prefix + path;
246
247 if (!filename.isEmpty()) {
248 file.append("/").append(filename);
249 }
250
251 //kDebug() << "testing" << file << QFile::exists("/bin/ls") << QFile::exists(file);
252 if (QFile::exists(file)) {
253 if (d->structure->allowExternalPaths()) {
254 //kDebug() << "found" << file;
255 return file;
256 }
257
258 // ensure that we don't return files outside of our base path
259 // due to symlink or ../ games
260 QDir dir(file);
261 QString canonicalized = dir.canonicalPath() + QDir::separator();
262
263 //kDebug() << "testing that" << canonicalized << "is in" << d->structure->path();
264 if (canonicalized.startsWith(d->structure->path())) {
265 //kDebug() << "found" << file;
266 return file;
267 }
268 }
269 }
270 }
271
272 //kDebug() << fileType << filename << "does not exist in" << prefixes << "at root" << d->structure->path();
273 return QString();
274}
275
276QString Package::filePath(const char *fileType) const
277{
278 return filePath(fileType, QString());
279}
280
281QStringList Package::entryList(const char *fileType) const
282{
283 if (!d->valid) {
284 return QStringList();
285 }
286
287 return d->structure->entryList(fileType);
288}
289
290PackageMetadata Package::metadata() const
291{
292 if (d->structure) {
293 return d->structure->metadata();
294 }
295
296 return PackageMetadata();
297}
298
299void Package::setPath(const QString &path)
300{
301 d->setPathFromStructure(path);
302}
303
304const QString Package::path() const
305{
306 return d->structure ? d->structure->path() : QString();
307}
308
309const PackageStructure::Ptr Package::structure() const
310{
311 return d->structure;
312}
313
314#ifdef QCA2_FOUND
315void PackagePrivate::updateHash(const QString &basePath, const QString &subPath, const QDir &dir, QCA::Hash &hash)
316{
317 // hash is calculated as a function of:
318 // * files ordered alphabetically by name, with each file's:
319 // * path relative to the content root
320 // * file data
321 // * directories ordered alphabetically by name, with each dir's:
322 // * path relative to the content root
323 // * file listing (recursing)
324 // symlinks (in both the file and dir case) are handled by adding
325 // the name of the symlink itself and the abs path of what it points to
326
327 const QDir::SortFlags sorting = QDir::Name | QDir::IgnoreCase;
328 const QDir::Filters filters = QDir::Hidden | QDir::System | QDir::NoDotAndDotDot;
329 foreach (const QString &file, dir.entryList(QDir::Files | filters, sorting)) {
330 if (!subPath.isEmpty()) {
331 hash.update(subPath.toUtf8());
332 }
333
334 hash.update(file.toUtf8());
335
336 QFileInfo info(dir.path() + '/' + file);
337 if (info.isSymLink()) {
338 hash.update(info.symLinkTarget().toUtf8());
339 } else {
340 QFile f(info.filePath());
341 if (f.open(QIODevice::ReadOnly)) {
342 while (!f.atEnd()) {
343 hash.update(f.read(1024));
344 }
345 } else {
346 kWarning() << "could not add" << f.fileName() << "to the hash; file could not be opened for reading. "
347 << "permissions fail?" << info.permissions() << info.isFile();
348 }
349 }
350 }
351
352 foreach (const QString &subDirPath, dir.entryList(QDir::Dirs | filters, sorting)) {
353 const QString relativePath = subPath + subDirPath + '/';
354 hash.update(relativePath.toUtf8());
355
356 QDir subDir(dir.path());
357 subDir.cd(subDirPath);
358
359 if (subDir.path() != subDir.canonicalPath()) {
360 hash.update(subDir.canonicalPath().toUtf8());
361 } else {
362 updateHash(basePath, relativePath, subDir, hash);
363 }
364 }
365}
366#endif
367
368QString Package::contentsHash() const
369{
370#ifdef QCA2_FOUND
371 if (!d->valid) {
372 kWarning() << "can not create hash due to Package being invalid";
373 return QString();
374 }
375
376 //FIXME: the initializer should go somewhere global to be shared between all plasma uses?
377 QCA::Initializer init;
378 if (!QCA::isSupported("sha1")) {
379 kWarning() << "can not create hash for" << path() << "due to no SHA1 support in QCA2";
380 return QString();
381 }
382
383 QCA::Hash hash("sha1");
384 QString metadataPath = d->structure->path() + "metadata.desktop";
385 if (QFile::exists(metadataPath)) {
386 QFile f(metadataPath);
387 if (f.open(QIODevice::ReadOnly)) {
388 while (!f.atEnd()) {
389 hash.update(f.read(1024));
390 }
391 } else {
392 kWarning() << "could not add" << f.fileName() << "to the hash; file could not be opened for reading.";
393 }
394 } else {
395 kWarning() << "no metadata at" << metadataPath;
396 }
397
398 QStringList prefixes = d->structure->contentsPrefixPaths();
399 if (prefixes.isEmpty()) {
400 prefixes << QString();
401 }
402
403 foreach (QString prefix, prefixes) {
404 const QString basePath = d->structure->path() + prefix;
405 QDir dir(basePath);
406
407 if (!dir.exists()) {
408 return QString();
409 }
410
411 d->updateHash(basePath, QString(), dir, hash);
412 }
413 return QCA::arrayToHex(hash.final().toByteArray());
414#else
415 // no QCA2!
416 kWarning() << "can not create hash for" << path() << "due to no cryptographic support (QCA2)";
417 return QString();
418#endif
419}
420
421//TODO: provide a version of this that allows one to ask for certain types of packages, etc?
422// should we be using KService here instead/as well?
423QStringList Package::listInstalled(const QString &packageRoot) // static
424{
425 QDir dir(packageRoot);
426
427 if (!dir.exists()) {
428 return QStringList();
429 }
430
431 QStringList packages;
432
433 foreach (const QString &sdir, dir.entryList(QDir::AllDirs | QDir::Readable)) {
434 QString metadata = packageRoot + '/' + sdir + "/metadata.desktop";
435 if (QFile::exists(metadata)) {
436 PackageMetadata m(metadata);
437 packages << m.pluginName();
438 }
439 }
440
441 return packages;
442}
443
444QStringList Package::listInstalledPaths(const QString &packageRoot) // static
445{
446 QDir dir(packageRoot);
447
448 if (!dir.exists()) {
449 return QStringList();
450 }
451
452 QStringList packages;
453
454 foreach (const QString &sdir, dir.entryList(QDir::AllDirs | QDir::Readable)) {
455 QString metadata = packageRoot + '/' + sdir + "/metadata.desktop";
456 if (QFile::exists(metadata)) {
457 packages << sdir;
458 }
459 }
460
461 return packages;
462}
463
464bool Package::installPackage(const QString &package,
465 const QString &packageRoot,
466 const QString &servicePrefix) // static
467{
468 //TODO: report *what* failed if something does fail
469 QDir root(packageRoot);
470
471 if (!root.exists()) {
472 KStandardDirs::makeDir(packageRoot);
473 if (!root.exists()) {
474 kWarning() << "Could not create package root directory:" << packageRoot;
475 return false;
476 }
477 }
478
479 QFileInfo fileInfo(package);
480 if (!fileInfo.exists()) {
481 kWarning() << "No such file:" << package;
482 return false;
483 }
484
485 QString path;
486 KTempDir tempdir;
487 bool archivedPackage = false;
488
489 if (fileInfo.isDir()) {
490 // we have a directory, so let's just install what is in there
491 path = package;
492
493 // make sure we end in a slash!
494 if (path[path.size() - 1] != '/') {
495 path.append('/');
496 }
497 } else {
498 KArchive *archive = 0;
499 KMimeType::Ptr mimetype = KMimeType::findByPath(package);
500
501 if (mimetype->is("application/zip")) {
502 archive = new KZip(package);
503 } else if (mimetype->is("application/x-compressed-tar") ||
504 mimetype->is("application/x-tar")|| mimetype->is("application/x-bzip-compressed-tar") ||
505 mimetype->is("application/x-xz-compressed-tar") || mimetype->is("application/x-lzma-compressed-tar")) {
506 archive = new KTar(package);
507 } else {
508 kWarning() << "Could not open package file, unsupported archive format:" << package << mimetype->name();
509 return false;
510 }
511
512 if (!archive->open(QIODevice::ReadOnly)) {
513 kWarning() << "Could not open package file:" << package;
514 delete archive;
515 return false;
516 }
517
518 archivedPackage = true;
519 path = tempdir.name();
520
521 const KArchiveDirectory *source = archive->directory();
522 source->copyTo(path);
523
524 QStringList entries = source->entries();
525 if (entries.count() == 1) {
526 const KArchiveEntry *entry = source->entry(entries[0]);
527 if (entry->isDirectory()) {
528 path.append(entry->name()).append("/");
529 }
530 }
531 delete archive;
532 }
533
534 QString metadataPath = path + "metadata.desktop";
535 if (!QFile::exists(metadataPath)) {
536 kWarning() << "No metadata file in package" << package << metadataPath;
537 return false;
538 }
539
540 PackageMetadata meta(metadataPath);
541 QString targetName = meta.pluginName();
542
543 if (targetName.isEmpty()) {
544 kWarning() << "Package plugin name not specified";
545 return false;
546 }
547
548 // Ensure that package names are safe so package uninstall can't inject
549 // bad characters into the paths used for removal.
550 QRegExp validatePluginName("^[\\w-\\.]+$"); // Only allow letters, numbers, underscore and period.
551 if (!validatePluginName.exactMatch(targetName)) {
552 kWarning() << "Package plugin name " << targetName << "contains invalid characters";
553 return false;
554 }
555
556 targetName = packageRoot + '/' + targetName;
557 if (QFile::exists(targetName)) {
558 kWarning() << targetName << "already exists";
559 return false;
560 }
561
562 if (archivedPackage) {
563 // it's in a temp dir, so just move it over.
564 const bool ok = copyFolder(path, targetName);
565 removeFolder(path);
566 if (!ok) {
567 kWarning() << "Could not move package to destination:" << targetName;
568 return false;
569 }
570 } else {
571 kDebug() << "************************** 12";
572 // it's a directory containing the stuff, so copy the contents rather
573 // than move them
574 const bool ok = copyFolder(path, targetName);
575 kDebug() << "************************** 13";
576 if (!ok) {
577 kWarning() << "Could not copy package to destination:" << targetName;
578 return false;
579 }
580 }
581
582 if (archivedPackage) {
583 // no need to remove the temp dir (which has been successfully moved if it's an archive)
584 tempdir.setAutoRemove(false);
585 }
586
587 if (!servicePrefix.isEmpty()) {
588 // and now we register it as a service =)
589 kDebug() << "************************** 1";
590 QString metaPath = targetName + "/metadata.desktop";
591 kDebug() << "************************** 2";
592 KDesktopFile df(metaPath);
593 KConfigGroup cg = df.desktopGroup();
594 kDebug() << "************************** 3";
595
596 // Q: should not installing it as a service disqualify it?
597 // Q: i don't think so since KServiceTypeTrader may not be
598 // used by the installing app in any case, and the
599 // package is properly installed - aseigo
600
601 //TODO: reduce code duplication with registerPackage below
602
603 QString serviceName = servicePrefix + meta.pluginName();
604
605 QString service = KStandardDirs::locateLocal("services", serviceName + ".desktop");
606 kDebug() << "************************** 4";
607 const bool ok = QFile::copy(metaPath, service);
608 kDebug() << "************************** 5";
609 if (ok) {
610 // the icon in the installed file needs to point to the icon in the
611 // installation dir!
612 QString iconPath = targetName + '/' + cg.readEntry("Icon");
613 QFile icon(iconPath);
614 if (icon.exists()) {
615 KDesktopFile df(service);
616 KConfigGroup cg = df.desktopGroup();
617 cg.writeEntry("Icon", iconPath);
618 }
619 } else {
620 kWarning() << "Could not register package as service (this is not necessarily fatal):" << serviceName;
621 }
622 kDebug() << "************************** 7";
623 }
624
625 return true;
626}
627
628bool Package::uninstallPackage(const QString &pluginName,
629 const QString &packageRoot,
630 const QString &servicePrefix) // static
631{
632 // We need to remove the package directory and its metadata file.
633 QString targetName = pluginName;
634 targetName = packageRoot + '/' + targetName;
635
636 if (!QFile::exists(targetName)) {
637 kWarning() << targetName << "does not exist";
638 return false;
639 }
640
641 QString serviceName = servicePrefix + pluginName;
642
643 QString service = KStandardDirs::locateLocal("services", serviceName + ".desktop");
644 kDebug() << "Removing service file " << service;
645 bool ok = QFile::remove(service);
646
647 if (!ok) {
648 kWarning() << "Unable to remove " << service;
649 }
650
651 ok = removeFolder(targetName);
652 const QString errorString("unknown");
653 if (!ok) {
654 kWarning() << "Could not delete package from:" << targetName << " : " << errorString;
655 return false;
656 }
657
658 return true;
659}
660
661bool Package::registerPackage(const PackageMetadata &data, const QString &iconPath)
662{
663 QString serviceName("plasma-applet-" + data.pluginName());
664 QString service = KStandardDirs::locateLocal("services", serviceName + ".desktop");
665
666 if (data.pluginName().isEmpty()) {
667 return false;
668 }
669
670 data.write(service);
671
672 KDesktopFile config(service);
673 KConfigGroup cg = config.desktopGroup();
674 const QString type = data.type().isEmpty() ? "Service" : data.type();
675 cg.writeEntry("Type", type);
676 const QString serviceTypes = data.serviceType().isNull() ? "Plasma/Applet,Plasma/Containment" : data.serviceType();
677 cg.writeEntry("X-KDE-ServiceTypes", serviceTypes);
678 cg.writeEntry("X-KDE-PluginInfo-EnabledByDefault", true);
679
680 QFile icon(iconPath);
681 if (icon.exists()) {
682 //FIXME: the '/' search will break on non-UNIX. do we care?
683 QString installedIcon("plasma_applet_" + data.pluginName() +
684 iconPath.right(iconPath.length() - iconPath.lastIndexOf("/")));
685 cg.writeEntry("Icon", installedIcon);
686 installedIcon = KStandardDirs::locateLocal("icon", installedIcon);
687 QFile::copy(iconPath, installedIcon);
688 }
689
690 return true;
691}
692
693bool Package::createPackage(const PackageMetadata &metadata,
694 const QString &source,
695 const QString &destination,
696 const QString &icon) // static
697{
698 Q_UNUSED(icon)
699 if (!metadata.isValid()) {
700 kWarning() << "Metadata file is not complete";
701 return false;
702 }
703
704 // write metadata in a temporary file
705 KTemporaryFile metadataFile;
706 if (!metadataFile.open()) {
707 return false;
708 }
709 metadata.write(metadataFile.fileName());
710
711 // put everything into a zip archive
712 KZip creation(destination);
713 creation.setCompression(KZip::NoCompression);
714 if (!creation.open(QIODevice::WriteOnly)) {
715 return false;
716 }
717
718 creation.addLocalFile(metadataFile.fileName(), "metadata.desktop");
719 creation.addLocalDirectory(source, "contents");
720 creation.close();
721 return true;
722}
723
724PackagePrivate::PackagePrivate(const PackageStructure::Ptr st, const QString &p)
725 : structure(st),
726 service(0)
727{
728 setPathFromStructure(p);
729}
730
731PackagePrivate::PackagePrivate(const PackageStructure::Ptr st, const QString &packageRoot, const QString &path)
732 : structure(st),
733 service(0)
734{
735 setPathFromStructure(packageRoot.isEmpty() ? path : packageRoot % "/" % path);
736}
737
738PackagePrivate::PackagePrivate(const PackagePrivate &other)
739 : structure(other.structure),
740 service(other.service),
741 valid(other.valid)
742{
743}
744
745PackagePrivate::~PackagePrivate()
746{
747}
748
749PackagePrivate &PackagePrivate::operator=(const PackagePrivate &rhs)
750{
751 structure = rhs.structure;
752 service = rhs.service;
753 valid = rhs.valid;
754 return *this;
755}
756
757void PackagePrivate::setPathFromStructure(const QString &path)
758{
759 if (!structure) {
760 valid = false;
761 return;
762 }
763
764 QStringList paths;
765 if (path.isEmpty()) {
766 paths << structure->defaultPackageRoot();
767 } else if (QDir::isRelativePath(path)) {
768 QString p = structure->defaultPackageRoot() % "/" % path % "/";
769
770 if (QDir::isRelativePath(p)) {
771 paths = KGlobal::dirs()->findDirs("data", p);
772 } else {
773 paths << p;
774 }
775 } else {
776 paths << path;
777 }
778
779 foreach (const QString &p, paths) {
780 structure->setPath(p);
781 // reset valid, otherwise isValid() short-circuits
782 valid = true;
783 if (isValid()) {
784 return;
785 }
786 }
787
788 valid = false;
789 structure->setPath(QString());
790}
791
792void PackagePrivate::publish(AnnouncementMethods methods)
793{
794 if (!structure) {
795 return;
796 }
797
798 if (!service) {
799 service = new PlasmoidService(structure->path());
800 }
801
802 QString resourceName =
803 i18nc("%1 is the name of a plasmoid, %2 the name of the machine that plasmoid is published on",
804 "%1 on %2", structure->metadata().name(), QHostInfo::localHostName());
805 kDebug() << "publishing package under name " << resourceName;
806 service->d->publish(methods, resourceName, structure->metadata());
807}
808
809void PackagePrivate::unpublish()
810{
811 if (service) {
812 service->d->unpublish();
813 }
814}
815
816bool PackagePrivate::isPublished() const
817{
818 if (service) {
819 return service->d->isPublished();
820 } else {
821 return false;
822 }
823}
824
825} // Namespace
826