1 | /****************************************************************************** |
2 | * Copyright 2007 by Aaron Seigo <aseigo@kde.org> * |
3 | * * |
4 | * This library is free software; you can redistribute it and/or * |
5 | * modify it under the terms of the GNU Library General Public * |
6 | * License as published by the Free Software Foundation; either * |
7 | * version 2 of the License, or (at your option) any later version. * |
8 | * * |
9 | * This library is distributed in the hope that it will be useful, * |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of * |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * |
12 | * Library General Public License for more details. * |
13 | * * |
14 | * You should have received a copy of the GNU Library General Public License * |
15 | * along with this library; see the file COPYING.LIB. If not, write to * |
16 | * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * |
17 | * Boston, MA 02110-1301, USA. * |
18 | *******************************************************************************/ |
19 | |
20 | #include "packagestructure.h" |
21 | |
22 | #include "config-plasma.h" |
23 | |
24 | #include <QDir> |
25 | #include <QMap> |
26 | #include <QMutableListIterator> |
27 | #include <QFileInfo> |
28 | |
29 | #include <kconfiggroup.h> |
30 | #include <kdebug.h> |
31 | #ifndef PLASMA_NO_KIO |
32 | #include <kio/job.h> |
33 | #endif |
34 | #include <kmimetype.h> |
35 | #include <kstandarddirs.h> |
36 | #include <kservicetypetrader.h> |
37 | #include <ktar.h> |
38 | #include <ktemporaryfile.h> |
39 | #include <ktempdir.h> |
40 | #include <kurl.h> |
41 | #include <kzip.h> |
42 | |
43 | #include "package.h" |
44 | #include "private/packages_p.h" |
45 | #include "theme.h" |
46 | |
47 | namespace Plasma |
48 | { |
49 | |
50 | class ContentStructure |
51 | { |
52 | public: |
53 | ContentStructure() |
54 | : directory(false), |
55 | required(false) |
56 | { |
57 | } |
58 | |
59 | ContentStructure(const ContentStructure &other) |
60 | { |
61 | paths = other.paths; |
62 | name = other.name; |
63 | mimetypes = other.mimetypes; |
64 | directory = other.directory; |
65 | required = other.required; |
66 | } |
67 | |
68 | QStringList paths; |
69 | QString name; |
70 | QStringList mimetypes; |
71 | bool directory : 1; |
72 | bool required : 1; |
73 | }; |
74 | |
75 | class PackageStructurePrivate |
76 | { |
77 | public: |
78 | PackageStructurePrivate(const QString &t) |
79 | : type(t), |
80 | packageRoot("plasma/plasmoids" ), |
81 | servicePrefix("plasma-applet-" ), |
82 | metadata(0), |
83 | externalPaths(false) |
84 | { |
85 | contentsPrefixPaths << "contents/" ; |
86 | } |
87 | |
88 | ~PackageStructurePrivate() |
89 | { |
90 | delete metadata; |
91 | } |
92 | |
93 | void createPackageMetadata(const QString &path); |
94 | QStringList entryList(const QString &prefix, const QString &requestedPath); |
95 | |
96 | QString type; |
97 | QString path; |
98 | QStringList contentsPrefixPaths; |
99 | QString packageRoot; |
100 | QString servicePrefix; |
101 | QMap<QByteArray, ContentStructure> contents; |
102 | QStringList mimetypes; |
103 | PackageMetadata *metadata; |
104 | bool externalPaths; |
105 | }; |
106 | |
107 | PackageStructure::PackageStructure(QObject *parent, const QString &type) |
108 | : QObject(parent), |
109 | d(new PackageStructurePrivate(type)) |
110 | { |
111 | } |
112 | |
113 | PackageStructure::~PackageStructure() |
114 | { |
115 | delete d; |
116 | } |
117 | |
118 | PackageStructure::Ptr PackageStructure::load(const QString &packageFormat) |
119 | { |
120 | if (packageFormat.isEmpty()) { |
121 | return Ptr(new PackageStructure()); |
122 | } |
123 | |
124 | PackageStructure::Ptr structure; |
125 | |
126 | if (packageFormat == "Plasma/Applet" ) { |
127 | structure = defaultPackageStructure(AppletComponent); |
128 | structure->d->type = "Plasma/Applet" ; |
129 | } else if (packageFormat == "Plasma/DataEngine" ) { |
130 | structure = defaultPackageStructure(DataEngineComponent); |
131 | structure->d->type = "Plasma/DataEngine" ; |
132 | } else if (packageFormat == "Plasma/Runner" ) { |
133 | structure = defaultPackageStructure(RunnerComponent); |
134 | structure->d->type = "Plasma/Runner" ; |
135 | } else if (packageFormat == "Plasma/Wallpaper" ) { |
136 | structure = defaultPackageStructure(WallpaperComponent); |
137 | structure->d->type = "Plasma/Wallpaper" ; |
138 | } else if (packageFormat == "Plasma/Theme" ) { |
139 | structure = Theme::packageStructure(); |
140 | structure->d->type = "Plasma/Theme" ; |
141 | } else if (packageFormat == "Plasma/Generic" ) { |
142 | structure = defaultPackageStructure(GenericComponent); |
143 | structure->d->type = "Plasma/Generic" ; |
144 | structure->setDefaultPackageRoot("plasma/packages/" ); |
145 | } |
146 | |
147 | if (structure) { |
148 | return structure; |
149 | } |
150 | |
151 | // first we check for plugins in sycoca |
152 | QString constraint = QString("[X-KDE-PluginInfo-Name] == '%1'" ).arg(packageFormat); |
153 | KService::List offers = |
154 | KServiceTypeTrader::self()->query("Plasma/PackageStructure" , constraint); |
155 | |
156 | QVariantList args; |
157 | QString error; |
158 | foreach (const KService::Ptr &offer, offers) { |
159 | PackageStructure::Ptr structure( |
160 | offer->createInstance<Plasma::PackageStructure>(0, args, &error)); |
161 | |
162 | if (structure) { |
163 | return structure; |
164 | } |
165 | |
166 | kDebug() << "Couldn't load PackageStructure for" << packageFormat |
167 | << "! reason given: " << error; |
168 | } |
169 | |
170 | // if that didn't give us any love, then we try to load from a config file |
171 | structure = new PackageStructure(); |
172 | QString configPath("plasma/packageformats/%1rc" ); |
173 | configPath = KStandardDirs::locate("data" , configPath.arg(packageFormat)); |
174 | |
175 | if (!configPath.isEmpty()) { |
176 | KConfig config(configPath); |
177 | structure->read(&config); |
178 | return structure; |
179 | } |
180 | |
181 | // try to load from absolute file path |
182 | KUrl url(packageFormat); |
183 | if (url.isLocalFile()) { |
184 | KConfig config(url.toLocalFile(), KConfig::SimpleConfig); |
185 | structure->read(&config); |
186 | } |
187 | #ifndef PLASMA_NO_KIO |
188 | else { |
189 | KTemporaryFile tmp; |
190 | if (tmp.open()) { |
191 | KIO::Job *job = KIO::file_copy(url, KUrl(tmp.fileName()), |
192 | -1, KIO::Overwrite | KIO::HideProgressInfo); |
193 | if (job->exec()) { |
194 | KConfig config(tmp.fileName(), KConfig::SimpleConfig); |
195 | structure->read(&config); |
196 | } |
197 | } |
198 | } |
199 | #endif |
200 | |
201 | return structure; |
202 | } |
203 | |
204 | PackageStructure &PackageStructure::operator=(const PackageStructure &rhs) |
205 | { |
206 | if (this == &rhs) { |
207 | return *this; |
208 | } |
209 | |
210 | *d = *rhs.d; |
211 | return *this; |
212 | } |
213 | |
214 | QString PackageStructure::type() const |
215 | { |
216 | return d->type; |
217 | } |
218 | |
219 | QList<const char*> PackageStructure::directories() const |
220 | { |
221 | QList<const char*> dirs; |
222 | QMap<QByteArray, ContentStructure>::const_iterator it = d->contents.constBegin(); |
223 | while (it != d->contents.constEnd()) { |
224 | if (it.value().directory) { |
225 | dirs << it.key(); |
226 | } |
227 | ++it; |
228 | } |
229 | return dirs; |
230 | } |
231 | |
232 | QList<const char*> PackageStructure::requiredDirectories() const |
233 | { |
234 | QList<const char*> dirs; |
235 | QMap<QByteArray, ContentStructure>::const_iterator it = d->contents.constBegin(); |
236 | while (it != d->contents.constEnd()) { |
237 | if (it.value().directory && |
238 | it.value().required) { |
239 | dirs << it.key(); |
240 | } |
241 | ++it; |
242 | } |
243 | return dirs; |
244 | } |
245 | |
246 | QList<const char*> PackageStructure::files() const |
247 | { |
248 | QList<const char*> files; |
249 | QMap<QByteArray, ContentStructure>::const_iterator it = d->contents.constBegin(); |
250 | while (it != d->contents.constEnd()) { |
251 | if (!it.value().directory) { |
252 | files << it.key(); |
253 | } |
254 | ++it; |
255 | } |
256 | return files; |
257 | } |
258 | |
259 | QList<const char*> PackageStructure::requiredFiles() const |
260 | { |
261 | QList<const char*> files; |
262 | QMap<QByteArray, ContentStructure>::const_iterator it = d->contents.constBegin(); |
263 | while (it != d->contents.constEnd()) { |
264 | if (!it.value().directory && it.value().required) { |
265 | files << it.key(); |
266 | } |
267 | ++it; |
268 | } |
269 | return files; |
270 | } |
271 | |
272 | QStringList PackageStructure::entryList(const char *key) |
273 | { |
274 | QString p = path(key); |
275 | |
276 | if (p.isEmpty()) { |
277 | return QStringList(); |
278 | } |
279 | |
280 | QStringList list; |
281 | if (d->contentsPrefixPaths.isEmpty()) { |
282 | // no prefixes is the same as d->contentsPrefixPths with QStringList() << QString() |
283 | list << d->entryList(QString(), p); |
284 | } else { |
285 | foreach (QString prefix, d->contentsPrefixPaths) { |
286 | list << d->entryList(prefix, p); |
287 | } |
288 | } |
289 | |
290 | return list; |
291 | } |
292 | |
293 | QStringList PackageStructurePrivate::entryList(const QString &prefix, const QString &requestedPath) |
294 | { |
295 | QDir dir(path + prefix + requestedPath); |
296 | |
297 | if (externalPaths) { |
298 | return dir.entryList(QDir::Files | QDir::Readable); |
299 | } |
300 | |
301 | // ensure that we don't return files outside of our base path |
302 | // due to symlink or ../ games |
303 | QString canonicalized = dir.canonicalPath(); |
304 | if (canonicalized.startsWith(path)) { |
305 | return dir.entryList(QDir::Files | QDir::Readable); |
306 | } |
307 | |
308 | return QStringList(); |
309 | } |
310 | |
311 | void PackageStructure::addDirectoryDefinition(const char *key, |
312 | const QString &path, const QString &name) |
313 | { |
314 | ContentStructure s; |
315 | |
316 | if (d->contents.contains(key)) { |
317 | s = d->contents[key]; |
318 | } |
319 | |
320 | if (!name.isEmpty()) { |
321 | s.name = name; |
322 | } |
323 | |
324 | s.paths.append(path); |
325 | s.directory = true; |
326 | |
327 | d->contents[key] = s; |
328 | } |
329 | |
330 | void PackageStructure::addFileDefinition(const char *key, const QString &path, const QString &name) |
331 | { |
332 | ContentStructure s; |
333 | |
334 | if (d->contents.contains(key)) { |
335 | s = d->contents[key]; |
336 | } |
337 | |
338 | if (!name.isEmpty()) { |
339 | s.name = name; |
340 | } |
341 | |
342 | s.paths.append(path); |
343 | s.directory = false; |
344 | |
345 | d->contents[key] = s; |
346 | } |
347 | |
348 | void PackageStructure::removeDefinition(const char *key) |
349 | { |
350 | d->contents.remove(key); |
351 | } |
352 | |
353 | QString PackageStructure::path(const char *key) const |
354 | { |
355 | //kDebug() << "looking for" << key; |
356 | QMap<QByteArray, ContentStructure>::const_iterator it = d->contents.constFind(key); |
357 | if (it == d->contents.constEnd()) { |
358 | return QString(); |
359 | } |
360 | |
361 | //kDebug() << "found" << key << "and the value is" << it.value().paths.first(); |
362 | return it.value().paths.first(); |
363 | } |
364 | |
365 | QStringList PackageStructure::searchPath(const char *key) const |
366 | { |
367 | //kDebug() << "looking for" << key; |
368 | QMap<QByteArray, ContentStructure>::const_iterator it = d->contents.constFind(key); |
369 | if (it == d->contents.constEnd()) { |
370 | return QStringList(); |
371 | } |
372 | |
373 | //kDebug() << "found" << key << "and the value is" << it.value().paths; |
374 | return it.value().paths; |
375 | } |
376 | |
377 | QString PackageStructure::name(const char *key) const |
378 | { |
379 | QMap<QByteArray, ContentStructure>::const_iterator it = d->contents.constFind(key); |
380 | if (it == d->contents.constEnd()) { |
381 | return QString(); |
382 | } |
383 | |
384 | return it.value().name; |
385 | } |
386 | |
387 | void PackageStructure::setRequired(const char *key, bool required) |
388 | { |
389 | QMap<QByteArray, ContentStructure>::iterator it = d->contents.find(key); |
390 | if (it == d->contents.end()) { |
391 | return; |
392 | } |
393 | |
394 | it.value().required = required; |
395 | } |
396 | |
397 | bool PackageStructure::isRequired(const char *key) const |
398 | { |
399 | QMap<QByteArray, ContentStructure>::const_iterator it = d->contents.constFind(key); |
400 | if (it == d->contents.constEnd()) { |
401 | return false; |
402 | } |
403 | |
404 | return it.value().required; |
405 | } |
406 | |
407 | void PackageStructure::setDefaultMimetypes(QStringList mimetypes) |
408 | { |
409 | d->mimetypes = mimetypes; |
410 | } |
411 | |
412 | void PackageStructure::setMimetypes(const char *key, QStringList mimetypes) |
413 | { |
414 | QMap<QByteArray, ContentStructure>::iterator it = d->contents.find(key); |
415 | if (it == d->contents.end()) { |
416 | return; |
417 | } |
418 | |
419 | it.value().mimetypes = mimetypes; |
420 | } |
421 | |
422 | QStringList PackageStructure::mimetypes(const char *key) const |
423 | { |
424 | QMap<QByteArray, ContentStructure>::const_iterator it = d->contents.constFind(key); |
425 | if (it == d->contents.constEnd()) { |
426 | return QStringList(); |
427 | } |
428 | |
429 | if (it.value().mimetypes.isEmpty()) { |
430 | return d->mimetypes; |
431 | } |
432 | |
433 | return it.value().mimetypes; |
434 | } |
435 | |
436 | void PackageStructure::setPath(const QString &path) |
437 | { |
438 | KUrl url(path); |
439 | QDir dir(url.toLocalFile()); |
440 | QString basePath = dir.canonicalPath(); |
441 | bool valid = QFile::exists(basePath); |
442 | |
443 | if (valid) { |
444 | QFileInfo info(basePath); |
445 | if (info.isDir() && !basePath.endsWith('/')) { |
446 | basePath.append('/'); |
447 | } |
448 | //kDebug() << "basePath is" << basePath; |
449 | } else { |
450 | kDebug() << path << "invalid, basePath is" << basePath; |
451 | return; |
452 | } |
453 | |
454 | if (d->path == basePath) { |
455 | return; |
456 | } |
457 | |
458 | d->path = basePath; |
459 | delete d->metadata; |
460 | d->metadata = 0; |
461 | pathChanged(); |
462 | } |
463 | |
464 | QString PackageStructure::path() const |
465 | { |
466 | return d->path; |
467 | } |
468 | |
469 | void PackageStructure::pathChanged() |
470 | { |
471 | // default impl does nothing, this is a hook for subclasses. |
472 | } |
473 | |
474 | void PackageStructure::read(const KConfigBase *config) |
475 | { |
476 | d->contents.clear(); |
477 | d->mimetypes.clear(); |
478 | KConfigGroup general(config, QString()); |
479 | d->type = general.readEntry("Type" , QString()); |
480 | d->contentsPrefixPaths = general.readEntry("ContentsPrefixPaths" , d->contentsPrefixPaths); |
481 | d->packageRoot = general.readEntry("DefaultPackageRoot" , d->packageRoot); |
482 | d->externalPaths = general.readEntry("AllowExternalPaths" , d->externalPaths); |
483 | |
484 | QStringList groups = config->groupList(); |
485 | foreach (const QString &group, groups) { |
486 | KConfigGroup entry(config, group); |
487 | QByteArray key = group.toLatin1(); |
488 | |
489 | QString path = entry.readEntry("Path" , QString()); |
490 | QString name = entry.readEntry("Name" , QString()); |
491 | QStringList mimetypes = entry.readEntry("Mimetypes" , QStringList()); |
492 | bool directory = entry.readEntry("Directory" , false); |
493 | bool required = entry.readEntry("Required" , false); |
494 | |
495 | if (directory) { |
496 | addDirectoryDefinition(key, path, name); |
497 | } else { |
498 | addFileDefinition(key, path, name); |
499 | } |
500 | |
501 | setMimetypes(key, mimetypes); |
502 | setRequired(key, required); |
503 | } |
504 | } |
505 | |
506 | void PackageStructure::write(KConfigBase *config) const |
507 | { |
508 | KConfigGroup general = KConfigGroup(config, "" ); |
509 | general.writeEntry("Type" , type()); |
510 | general.writeEntry("ContentsPrefixPaths" , d->contentsPrefixPaths); |
511 | general.writeEntry("DefaultPackageRoot" , d->packageRoot); |
512 | general.writeEntry("AllowExternalPaths" , d->externalPaths); |
513 | |
514 | QMap<QByteArray, ContentStructure>::const_iterator it = d->contents.constBegin(); |
515 | while (it != d->contents.constEnd()) { |
516 | KConfigGroup group = config->group(it.key()); |
517 | group.writeEntry("Path" , it.value().paths); |
518 | group.writeEntry("Name" , it.value().name); |
519 | if (!it.value().mimetypes.isEmpty()) { |
520 | group.writeEntry("Mimetypes" , it.value().mimetypes); |
521 | } |
522 | if (it.value().directory) { |
523 | group.writeEntry("Directory" , true); |
524 | } |
525 | if (it.value().required) { |
526 | group.writeEntry("Required" , true); |
527 | } |
528 | |
529 | ++it; |
530 | } |
531 | } |
532 | |
533 | QString PackageStructure::contentsPrefix() const |
534 | { |
535 | return d->contentsPrefixPaths.isEmpty() ? QString() : d->contentsPrefixPaths.first(); |
536 | } |
537 | |
538 | void PackageStructure::setContentsPrefix(const QString &prefix) |
539 | { |
540 | d->contentsPrefixPaths.clear(); |
541 | d->contentsPrefixPaths << prefix; |
542 | } |
543 | |
544 | QStringList PackageStructure::contentsPrefixPaths() const |
545 | { |
546 | return d->contentsPrefixPaths; |
547 | } |
548 | |
549 | void PackageStructure::setContentsPrefixPaths(const QStringList &prefixPaths) |
550 | { |
551 | d->contentsPrefixPaths = prefixPaths; |
552 | |
553 | // the code assumes that the prefixes have a trailing slash |
554 | // so let's make that true here |
555 | QMutableStringListIterator it(d->contentsPrefixPaths); |
556 | while (it.hasNext()) { |
557 | it.next(); |
558 | |
559 | if (!it.value().endsWith('/')) { |
560 | it.setValue(it.value() % '/'); |
561 | } |
562 | } |
563 | } |
564 | |
565 | bool PackageStructure::installPackage(const QString &package, const QString &packageRoot) |
566 | { |
567 | return Package::installPackage(package, packageRoot, d->servicePrefix); |
568 | } |
569 | |
570 | bool PackageStructure::uninstallPackage(const QString &packageName, const QString &packageRoot) |
571 | { |
572 | return Package::uninstallPackage(packageName, packageRoot, d->servicePrefix); |
573 | } |
574 | |
575 | void PackageStructure::createNewWidgetBrowser(QWidget *parent) |
576 | { |
577 | Q_UNUSED(parent) |
578 | emit newWidgetBrowserFinished(); |
579 | } |
580 | |
581 | QString PackageStructure::defaultPackageRoot() const |
582 | { |
583 | return d->packageRoot; |
584 | } |
585 | |
586 | QString PackageStructure::servicePrefix() const |
587 | { |
588 | return d->servicePrefix; |
589 | } |
590 | |
591 | void PackageStructure::setDefaultPackageRoot(const QString &packageRoot) |
592 | { |
593 | d->packageRoot = packageRoot; |
594 | } |
595 | |
596 | void PackageStructure::setServicePrefix(const QString &servicePrefix) |
597 | { |
598 | d->servicePrefix = servicePrefix; |
599 | } |
600 | |
601 | void PackageStructurePrivate::createPackageMetadata(const QString &path) |
602 | { |
603 | delete metadata; |
604 | metadata = 0; |
605 | |
606 | QString metadataPath(path + "/metadata.desktop" ); |
607 | if (!QFile::exists(metadataPath)) { |
608 | kWarning() << "No metadata file in the package, expected it at:" << metadataPath; |
609 | metadataPath.clear(); |
610 | } |
611 | |
612 | metadata = new PackageMetadata(metadataPath); |
613 | } |
614 | |
615 | //FIXME KDE5: should be const |
616 | PackageMetadata PackageStructure::metadata() |
617 | { |
618 | if (!d->metadata && !d->path.isEmpty()) { |
619 | QFileInfo fileInfo(d->path); |
620 | |
621 | if (fileInfo.isDir()) { |
622 | d->createPackageMetadata(d->path); |
623 | } else if (fileInfo.exists()) { |
624 | KArchive *archive = 0; |
625 | KMimeType::Ptr mimetype = KMimeType::findByPath(d->path); |
626 | |
627 | if (mimetype->is("application/zip" )) { |
628 | archive = new KZip(d->path); |
629 | } else if (mimetype->is("application/x-compressed-tar" ) || mimetype->is("application/x-gzip" ) || |
630 | mimetype->is("application/x-xz-compressed-tar" ) || mimetype->is("application/x-lzma-compressed-tar" ) || |
631 | mimetype->is("application/x-tar" )|| mimetype->is("application/x-bzip-compressed-tar" )) { |
632 | archive = new KTar(d->path); |
633 | } else { |
634 | kWarning() << "Could not open package file, unsupported archive format:" << d->path << mimetype->name(); |
635 | } |
636 | |
637 | if (archive && archive->open(QIODevice::ReadOnly)) { |
638 | const KArchiveDirectory *source = archive->directory(); |
639 | KTempDir tempdir; |
640 | source->copyTo(tempdir.name()); |
641 | d->createPackageMetadata(tempdir.name()); |
642 | } else { |
643 | kWarning() << "Could not open package file:" << d->path; |
644 | } |
645 | |
646 | delete archive; |
647 | } |
648 | } |
649 | |
650 | if (!d->metadata) { |
651 | d->metadata = new PackageMetadata(); |
652 | } |
653 | |
654 | return *d->metadata; |
655 | } |
656 | |
657 | bool PackageStructure::allowExternalPaths() const |
658 | { |
659 | return d->externalPaths; |
660 | } |
661 | |
662 | void PackageStructure::setAllowExternalPaths(bool allow) |
663 | { |
664 | d->externalPaths = allow; |
665 | } |
666 | |
667 | } // Plasma namespace |
668 | |
669 | #include "packagestructure.moc" |
670 | |
671 | |