diff --git a/KF5PackageMacros.cmake b/KF5PackageMacros.cmake index c3e7cb8..761eab3 100644 --- a/KF5PackageMacros.cmake +++ b/KF5PackageMacros.cmake @@ -1,170 +1,170 @@ find_package(ECM 1.6.0 CONFIG REQUIRED) include(${ECM_KDE_MODULE_DIR}/KDEInstallDirs.cmake) set(KPACKAGE_RELATIVE_DATA_INSTALL_DIR "kpackage") # kpackage_install_package(path componentname [root] [install_dir]) # # Installs a package to the system path # @arg path The source path to install from, location of metadata.desktop # @arg componentname The plugin name of the component, corresponding to the # X-KDE-PluginInfo-Name key in metadata.desktop # @arg root The subdirectory to install to, default: packages # @arg install_dir the path where to install packages, # such as myapp, that would go under prefix/share/myapp: # default ${KPACKAGE_RELATIVE_DATA_INSTALL_DIR} # # Examples: # kpackage_install_package(mywidget org.kde.plasma.mywidget plasmoids) # installs an applet # kpackage_install_package(declarativetoolbox org.kde.toolbox packages) # installs a generic package # kpackage_install_package(declarativetoolbox org.kde.toolbox) # installs a generic package # set(kpackagedir ${CMAKE_CURRENT_LIST_DIR}) function(kpackage_install_package dir component) # allow plasma_install_package to disable the warning until it is ported away from this if (NOT ARGV4 STREQUAL NO_DEPRECATED_WARNING) message(AUTHOR_WARNING "Deprecated: use kpackage_install_bundled_package") endif() set(root ${ARGV2}) set(install_dir ${ARGV3}) if(NOT root) set(root packages) endif() if(NOT install_dir) set(install_dir ${KPACKAGE_RELATIVE_DATA_INSTALL_DIR}) endif() install(DIRECTORY ${dir}/ DESTINATION ${KDE_INSTALL_DATADIR}/${install_dir}/${root}/${component} PATTERN .svn EXCLUDE PATTERN *.qmlc EXCLUDE PATTERN CMakeLists.txt EXCLUDE PATTERN Messages.sh EXCLUDE PATTERN dummydata EXCLUDE) set(metadatajson) set(ORIGINAL_METADATA "${CMAKE_CURRENT_SOURCE_DIR}/${dir}/metadata.desktop") if(NOT EXISTS ${component}-${root}-metadata.json AND EXISTS ${ORIGINAL_METADATA}) set(GENERATED_METADATA "${CMAKE_CURRENT_BINARY_DIR}/${component}-${root}-metadata.json") add_custom_command(OUTPUT ${GENERATED_METADATA} DEPENDS ${ORIGINAL_METADATA} COMMAND KF5::desktoptojson -i ${ORIGINAL_METADATA} -o ${GENERATED_METADATA}) add_custom_target(${component}-${root}-metadata-json ALL DEPENDS ${GENERATED_METADATA}) install(FILES ${GENERATED_METADATA} DESTINATION ${KDE_INSTALL_DATADIR}/${install_dir}/${root}/${component} RENAME metadata.json) set(metadatajson ${GENERATED_METADATA}) endif() get_target_property(kpackagetool_cmd KF5::kpackagetool5 LOCATION) if (${component} MATCHES "^.+\\..+\\.") #we make sure there's at least 2 dots set(APPDATAFILE "${CMAKE_CURRENT_BINARY_DIR}/${component}.appdata.xml") execute_process( COMMAND ${kpackagetool_cmd} --appstream-metainfo ${CMAKE_CURRENT_SOURCE_DIR}/${dir} --appstream-metainfo-output ${APPDATAFILE} ERROR_VARIABLE appstreamerror RESULT_VARIABLE result) if (NOT result EQUAL 0) message(WARNING "couldn't generate metainfo for ${component}: ${appstreamerror}") else() if(appstreamerror) message(WARNING "warnings during generation of metainfo for ${component}: ${appstreamerror}") endif() # OPTIONAL because desktop files can be NoDisplay so they render no XML. install(FILES ${APPDATAFILE} DESTINATION ${KDE_INSTALL_METAINFODIR} OPTIONAL) endif() else() message(WARNING "KPackage components should be specified in reverse domain notation. Appstream information won't be generated for ${component}.") endif() set(newentry "${kpackagetool_cmd} --generate-index -g -p ${CMAKE_INSTALL_PREFIX}/${KDE_INSTALL_DATADIR}/${install_dir}/${root}\n") get_directory_property(currentindex kpackageindex) string(FIND "${currentindex}" "${newentry}" alreadyin) if (alreadyin LESS 0) set(regenerateindex "${currentindex}${newentry}") set_directory_properties(PROPERTIES kpackageindex "${regenerateindex}") file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/regenerateindex.sh ${regenerateindex}) endif() endfunction() #use this version instead: it compresses the contents directory #into a binary rcc file function(kpackage_install_bundled_package dir component) set(root ${ARGV2}) set(install_dir ${ARGV3}) if(NOT root) set(root packages) endif() if(NOT install_dir) set(install_dir ${KPACKAGE_RELATIVE_DATA_INSTALL_DIR}) endif() set(metadatajson) set(ORIGINAL_METADATA "${CMAKE_CURRENT_SOURCE_DIR}/${dir}/metadata.desktop") if(NOT EXISTS ${component}-${root}-metadata.json AND EXISTS ${ORIGINAL_METADATA}) set(GENERATED_METADATA "${CMAKE_CURRENT_BINARY_DIR}/${component}-${root}-metadata.json") add_custom_command(OUTPUT ${GENERATED_METADATA} DEPENDS ${ORIGINAL_METADATA} COMMAND KF5::desktoptojson -i ${ORIGINAL_METADATA} -o ${GENERATED_METADATA}) add_custom_target(${component}-${root}-metadata-json ALL DEPENDS ${GENERATED_METADATA}) install(FILES ${GENERATED_METADATA} DESTINATION ${KDE_INSTALL_DATADIR}/${install_dir}/${root}/${component} RENAME metadata.json) set(metadatajson ${GENERATED_METADATA}) endif() get_target_property(kpackagetool_cmd KF5::kpackagetool5 LOCATION) if (${component} MATCHES "^.+\\..+\\.") #we make sure there's at least 2 dots set(APPDATAFILE "${CMAKE_CURRENT_BINARY_DIR}/${component}.appdata.xml") execute_process( COMMAND ${kpackagetool_cmd} --appstream-metainfo ${CMAKE_CURRENT_SOURCE_DIR}/${dir} --appstream-metainfo-output ${APPDATAFILE} ERROR_VARIABLE appstreamerror RESULT_VARIABLE result) if (NOT result EQUAL 0) message(WARNING "couldn't generate metainfo for ${component}: ${appstreamerror}") else() if(appstreamerror) message(WARNING "warnings during generation of metainfo for ${component}: ${appstreamerror}") endif() # OPTIONAL because desktop files can be NoDisplay so they render no XML. install(FILES ${APPDATAFILE} DESTINATION ${KDE_INSTALL_METAINFODIR} OPTIONAL) endif() else() message(WARNING "KPackage components should be specified in reverse domain notation. Appstream information won't be generated for ${component}.") endif() set(newentry "${kpackagetool_cmd} --generate-index -g -p ${CMAKE_INSTALL_PREFIX}/${KDE_INSTALL_DATADIR}/${install_dir}/${root}\n") get_directory_property(currentindex kpackageindex) string(FIND "${currentindex}" "${newentry}" alreadyin) if (alreadyin LESS 0) set(regenerateindex "${currentindex}${newentry}") set_directory_properties(PROPERTIES kpackageindex "${regenerateindex}") file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/regenerateindex.sh ${regenerateindex}) endif() set(kpkgqrc "${CMAKE_CURRENT_BINARY_DIR}/${component}.qrc") set(metadatajson ${metadatajson}) set(component ${component}) set(root ${root}) set(BINARYDIR ${CMAKE_CURRENT_BINARY_DIR}) set(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${dir}") set(OUTPUTFILE "${kpkgqrc}") add_custom_target(${component}-${root}-qrc ALL COMMAND ${CMAKE_COMMAND} -DINSTALL_DIR=${install_dir} -DROOT=${root} -DCOMPONENT=${component} -DDIRECTORY=${DIRECTORY} -D OUTPUTFILE=${OUTPUTFILE} -P ${kpackagedir}/qrc.cmake DEPENDS ${component}-${root}-metadata-json) set(GENERATED_RCC_CONTENTS "${CMAKE_CURRENT_BINARY_DIR}/${component}-contents.rcc") - # add_custom_target depends on ALL target so qrc is run everytime + # add_custom_target depends on ALL target so qrc is run every time # it doesn't have OUTPUT property so it's considered out-of-date every build add_custom_target(${component}-${root}-contents-rcc ALL COMMENT "Generating ${component}-contents.rcc" COMMAND Qt5::rcc "${kpkgqrc}" --binary -o "${GENERATED_RCC_CONTENTS}" DEPENDS ${component}-${root}-metadata-json ${component}-${root}-qrc) install(FILES ${GENERATED_RCC_CONTENTS} DESTINATION ${KDE_INSTALL_DATADIR}/${install_dir}/${root}/${component}/ RENAME contents.rcc) endfunction() diff --git a/src/kpackage/package.cpp b/src/kpackage/package.cpp index 6cfe82c..cb2bf8a 100644 --- a/src/kpackage/package.cpp +++ b/src/kpackage/package.cpp @@ -1,1043 +1,1043 @@ /****************************************************************************** * Copyright 2007 by Aaron Seigo * * Copyright 2010 by Marco Martin * * Copyright 2010 by Kevin Ottens * * Copyright 2009 by Rob Scheepmaker * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public License * * along with this library; see the file COPYING.LIB. If not, write to * * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301, USA. * *******************************************************************************/ #include "package.h" #include #include #include #include "kpackage_debug.h" #include #include #include #include #include "config-package.h" #include #include #include "packagestructure.h" #include "packageloader.h" #include "private/package_p.h" //#include "private/packages_p.h" #include "private/packagejob_p.h" #include "private/packageloader_p.h" namespace KPackage { Package::Package(PackageStructure *structure) : d(new PackagePrivate()) { d->structure = structure; if (d->structure) { d->structure.data()->initPackage(this); addFileDefinition("metadata", QStringLiteral("metadata.desktop"), i18n("Desktop file that describes this package.")); } } Package::Package(const Package &other) : d(other.d) { } Package::~Package() { //guard against deletion on application shutdown if (PackageDeletionNotifier::self()) { Q_EMIT PackageDeletionNotifier::self()->packageDeleted(this); } } Package &Package::operator=(const Package &rhs) { if (&rhs != this) { d = rhs.d; } return *this; } bool Package::hasValidStructure() const { return d->structure; } bool Package::isValid() const { if (!d->structure) { return false; } //Minimal packages with no metadata *are* supposed to be possible //so if !metadata().isValid() go ahead if (metadata().isValid() && metadata().value(QStringLiteral("isHidden"), QStringLiteral("false")) == QLatin1String("true")) { return false; } if (d->checkedValid) { return d->valid; } const QString rootPath = d->tempRoot.isEmpty() ? d->path : d->tempRoot; if(rootPath.isEmpty()) { return false; } d->valid = true; //search for the file in all prefixes and in all possible paths for each prefix //even if it's a big nested loop, usually there is one prefix and one location //so shouldn't cause too much disk access QHashIterator it(d->contents); while (it.hasNext()) { it.next(); if (!it.value().required) { continue; } const QString foundPath = filePath(it.key(), {}); if (foundPath.isEmpty()) { //qCWarning(KPACKAGE_LOG) << "Could not find required" << (it.value().directory ? "directory" : "file") << it.key() << "for package" << path() << "should be" << it.value().paths; d->valid = false; break; } } return d->valid; } QString Package::name(const QByteArray &key) const { QHash::const_iterator it = d->contents.constFind(key); if (it == d->contents.constEnd()) { return QString(); } return it.value().name; } bool Package::isRequired(const QByteArray &key) const { QHash::const_iterator it = d->contents.constFind(key); if (it == d->contents.constEnd()) { return false; } return it.value().required; } QStringList Package::mimeTypes(const QByteArray &key) const { QHash::const_iterator it = d->contents.constFind(key); if (it == d->contents.constEnd()) { return QStringList(); } if (it.value().mimeTypes.isEmpty()) { return d->mimeTypes; } return it.value().mimeTypes; } QString Package::defaultPackageRoot() const { return d->defaultPackageRoot; } void Package::setDefaultPackageRoot(const QString &packageRoot) { d.detach(); d->defaultPackageRoot = packageRoot; if (!d->defaultPackageRoot.isEmpty() && !d->defaultPackageRoot.endsWith(QLatin1Char('/'))) { d->defaultPackageRoot.append(QLatin1Char('/')); } } void Package::setFallbackPackage(const KPackage::Package &package) { if ((d->fallbackPackage && d->fallbackPackage->path() == package.path() && d->fallbackPackage->metadata() == package.metadata()) || //can't be fallback of itself (package.path() == path() && package.metadata() == metadata()) || d->hasCycle(package)) { return; } d->fallbackPackage = new Package(package); } KPackage::Package Package::fallbackPackage() const { if (d->fallbackPackage) { return (*d->fallbackPackage); } else { return Package(); } } bool Package::allowExternalPaths() const { return d->externalPaths; } void Package::setAllowExternalPaths(bool allow) { d.detach(); d->externalPaths = allow; } KPluginMetaData Package::metadata() const { //qCDebug(KPACKAGE_LOG) << "metadata: " << d->path << filePath("metadata"); if (!d->metadata && !d->path.isEmpty()) { const QString metadataPath = filePath("metadata"); if (!metadataPath.isEmpty()) { d->createPackageMetadata(metadataPath); } else { // d->path might still be a file, if its path has a trailing /, // the fileInfo lookup will fail, so remove it. QString p = d->path; if (p.endsWith(QLatin1Char('/'))) { p.chop(1); } QFileInfo fileInfo(p); if (fileInfo.isDir()) { d->createPackageMetadata(d->path); } else if (fileInfo.exists()) { d->path = fileInfo.canonicalFilePath(); d->tempRoot = d->unpack(p); } } } if (!d->metadata) { d->metadata = new KPluginMetaData(); } return *d->metadata; } QString PackagePrivate::unpack(const QString &filePath) { KArchive *archive = nullptr; QMimeDatabase db; QMimeType mimeType = db.mimeTypeForFile(filePath); if (mimeType.inherits(QStringLiteral("application/zip"))) { archive = new KZip(filePath); } else if (mimeType.inherits(QStringLiteral("application/x-compressed-tar")) || mimeType.inherits(QStringLiteral("application/x-gzip")) || mimeType.inherits(QStringLiteral("application/x-tar")) || mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || mimeType.inherits(QStringLiteral("application/x-xz")) || mimeType.inherits(QStringLiteral("application/x-lzma"))) { archive = new KTar(filePath); } else { //qCWarning(KPACKAGE_LOG) << "Could not open package file, unsupported archive format:" << filePath << mimeType.name(); } QString tempRoot; if (archive && archive->open(QIODevice::ReadOnly)) { const KArchiveDirectory *source = archive->directory(); QTemporaryDir tempdir; tempdir.setAutoRemove(false); tempRoot = tempdir.path() + QLatin1Char('/'); source->copyTo(tempRoot); if (!QFile::exists(tempdir.path() + QLatin1String("/metadata.json")) && !QFile::exists(tempdir.path() + QLatin1String("/metadata.desktop"))) { // search metadata.desktop, the zip file might have the package contents in a subdirectory QDir unpackedPath(tempdir.path()); const auto entries = unpackedPath.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); foreach (const auto &pack, entries) { if (QFile::exists(pack.filePath() + QLatin1String("/metadata.json")) || QFile::exists(pack.filePath() + QLatin1String("/metadata.desktop"))) { tempRoot = pack.filePath() + QLatin1Char('/'); } } } createPackageMetadata(tempRoot); } else { //qCWarning(KPACKAGE_LOG) << "Could not open package file:" << path; } delete archive; return tempRoot; } bool PackagePrivate::isInsidePackageDir(const QString &canonicalPath) const { // make sure that the target file is actually inside the package dir to prevent // path traversal using symlinks or "../" path segments // make sure we got passed a valid path Q_ASSERT(QFileInfo::exists(canonicalPath)); Q_ASSERT(QFileInfo(canonicalPath).canonicalFilePath() == canonicalPath); // make sure that the base path is also canonical // this was not the case until 5.8, making this check fail e.g. if /home is a symlink // which in turn would make plasmashell not find the .qml files //installed package if (tempRoot.isEmpty()) { Q_ASSERT(QDir(path).exists()); Q_ASSERT(path == QStringLiteral("/") || QDir(path).canonicalPath() + QLatin1Char('/') == path); if (canonicalPath.startsWith(path)) { return true; } //temporary compressed package } else { Q_ASSERT(QDir(tempRoot).exists()); Q_ASSERT(tempRoot == QStringLiteral("/") || QDir(tempRoot).canonicalPath() + QLatin1Char('/') == tempRoot); if (canonicalPath.startsWith(tempRoot)) { return true; } } qCWarning(KPACKAGE_LOG) << "Path traversal attempt detected:" << canonicalPath << "is not inside" << path; return false; } QString Package::filePath(const QByteArray &fileType, const QString &filename) const { if (!d->valid) { QString result = d->fallbackFilePath(fileType, filename); if (result.isEmpty()) { // qCDebug(KPACKAGE_LOG) << fileType << "file with name" << filename // << "was not found in package with path" << d->path; } return result; } const QString discoveryKey(QString::fromUtf8(fileType) + filename); const auto path = d->discoveries.value(discoveryKey); if (!path.isEmpty()) { return path; } QStringList paths; if (fileType.size()) { const auto contents = d->contents.constFind(fileType); if (contents == d->contents.constEnd()) { //qCDebug(KPACKAGE_LOG) << "package does not contain" << fileType << filename; return d->fallbackFilePath(fileType, filename); } paths = contents->paths; if (paths.isEmpty()) { //qCDebug(KPACKAGE_LOG) << "no matching path came of it, while looking for" << fileType << filename; d->discoveries.insert(discoveryKey, QString()); return d->fallbackFilePath(fileType, filename); } } else { //when filetype is empty paths is always empty, so try with an empty string paths << QString(); } //Nested loop, but in the medium case resolves to just one iteration // qCDebug(KPACKAGE_LOG) << "prefixes:" << d->contentsPrefixPaths.count() << d->contentsPrefixPaths; foreach (const QString &contentsPrefix, d->contentsPrefixPaths) { QString prefix; //We are an installed package if (d->tempRoot.isEmpty()) { prefix = fileType == "metadata" ? d->path : (d->path + contentsPrefix); - //We are a compressed package temporarly uncompressed in /tmp + //We are a compressed package temporarily uncompressed in /tmp } else { prefix = fileType == "metadata" ? d->tempRoot : (d->tempRoot + contentsPrefix); } foreach (const QString &path, paths) { QString file = prefix + path; if (!filename.isEmpty()) { file = file + QLatin1Char('/') + filename; } QFileInfo fi(file); if (fi.exists()) { if (d->externalPaths) { //qCDebug(KPACKAGE_LOG) << "found" << file; d->discoveries.insert(discoveryKey, file); return file; } // ensure that we don't return files outside of our base path // due to symlink or ../ games if (d->isInsidePackageDir(fi.canonicalFilePath())) { //qCDebug(KPACKAGE_LOG) << "found" << file; d->discoveries.insert(discoveryKey, file); return file; } } } } //qCDebug(KPACKAGE_LOG) << fileType << filename << "does not exist in" << prefixes << "at root" << d->path; return d->fallbackFilePath(fileType, filename); } QUrl Package::fileUrl(const QByteArray &fileType, const QString &filename) const { QString path = filePath(fileType, filename); //construct a qrc:/ url or a file:/ url, the only two protocols supported if (path.startsWith(QStringLiteral(":"))) { return QUrl(QStringLiteral("qrc") + path); } else { return QUrl::fromLocalFile(path); } } QStringList Package::entryList(const QByteArray &key) const { if (!d->valid) { return QStringList(); } QHash::const_iterator it = d->contents.constFind(key); if (it == d->contents.constEnd()) { //qCDebug(KPACKAGE_LOG) << "couldn't find" << key; return QStringList(); } //qCDebug(KPACKAGE_LOG) << "going to list" << key; QStringList list; foreach (const QString &prefix, d->contentsPrefixPaths) { //qCDebug(KPACKAGE_LOG) << " looking in" << prefix; foreach (const QString &path, it.value().paths) { //qCDebug(KPACKAGE_LOG) << " looking in" << path; if (it.value().directory) { //qCDebug(KPACKAGE_LOG) << "it's a directory, so trying out" << d->path + prefix + path; QDir dir(d->path + prefix + path); if (d->externalPaths) { list += dir.entryList(QDir::Files | QDir::Readable); } else { // ensure that we don't return files outside of our base path // due to symlink or ../ games QString canonicalized = dir.canonicalPath(); if (canonicalized.startsWith(d->path)) { list += dir.entryList(QDir::Files | QDir::Readable); } } } else { const QString fullPath = d->path + prefix + path; //qCDebug(KPACKAGE_LOG) << "it's a file at" << fullPath << QFile::exists(fullPath); if (!QFile::exists(fullPath)) { continue; } if (d->externalPaths) { list += fullPath; } else { QDir dir(fullPath); QString canonicalized = dir.canonicalPath() + QDir::separator(); //qCDebug(KPACKAGE_LOG) << "testing that" << canonicalized << "is in" << d->path; if (canonicalized.startsWith(d->path)) { list += fullPath; } } } } } return list; } void Package::setPath(const QString &path) { // if the path is already what we have, don't bother if (path == d->path) { return; } // our dptr is shared, and it is almost certainly going to change. // hold onto the old pointer just in case it does not, however! QExplicitlySharedDataPointer oldD(d); d.detach(); // without structure we're doomed if (!d->structure) { d->path.clear(); d->discoveries.clear(); d->valid = false; d->checkedValid = true; return; } // empty path => nothing to do if (path.isEmpty()) { d->path.clear(); d->discoveries.clear(); d->valid = false; d->structure.data()->pathChanged(this); return; } // now we look for all possible paths, including resolving // relative paths against the system search paths QStringList paths; if (QDir::isRelativePath(path)) { QString p; if (d->defaultPackageRoot.isEmpty()) { p = path % QLatin1Char('/'); } else { p = d->defaultPackageRoot % path % QLatin1Char('/'); } if (QDir::isRelativePath(p)) { //FIXME: can searching of the qrc be better? paths << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, p, QStandardPaths::LocateDirectory); } else { const QDir dir(p); if (QFile::exists(dir.canonicalPath())) { paths << p; } } //qCDebug(KPACKAGE_LOG) << "paths:" << p << paths << d->defaultPackageRoot; } else { const QDir dir(path); if (QFile::exists(dir.canonicalPath())) { paths << path; } } QFileInfo fileInfo(path); if (fileInfo.isFile() && d->tempRoot.isEmpty()) { d->path = fileInfo.canonicalFilePath(); d->tempRoot = d->unpack(path); } // now we search each path found, caching our previous path to know if // anything actually really changed const QString previousPath = d->path; foreach (const QString &p, paths) { d->checkedValid = false; QDir dir(p); Q_ASSERT(QFile::exists(dir.canonicalPath())); //if it has a contents.rcc, give priority to it if (dir.exists(QStringLiteral("contents.rcc"))) { d->rccPath = dir.absoluteFilePath(QStringLiteral("contents.rcc")); QResource::registerResource(d->rccPath); //we need just the plugin name here, never the absolute path dir = QDir(QStringLiteral(":/") + defaultPackageRoot() + path.midRef(path.lastIndexOf(QLatin1Char('/')))); } d->path = dir.canonicalPath(); // canonicalPath() does not include a trailing slash (unless it is the root dir) if (!d->path.endsWith(QLatin1Char('/'))) { d->path.append(QLatin1Char('/')); } const QString fallbackPath = metadata().value(QStringLiteral("X-Plasma-RootPath")); if (!fallbackPath.isEmpty()) { const KPackage::Package fp = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"), fallbackPath); setFallbackPackage(fp); } // we need to tell the structure we're changing paths ... d->structure.data()->pathChanged(this); // ... and then testing the results for validity if (isValid()) { break; } } // if nothing did change, then we go back to the old dptr if (d->path == previousPath) { d = oldD; return; } // .. but something did change, so we get rid of our discovery cache d->discoveries.clear(); delete d->metadata; d->metadata = nullptr; // uh-oh, but we didn't end up with anything valid, so we sadly reset ourselves // to futility. if (!d->valid) { d->path.clear(); d->structure.data()->pathChanged(this); } } const QString Package::path() const { return d->path; } QStringList Package::contentsPrefixPaths() const { return d->contentsPrefixPaths; } void Package::setContentsPrefixPaths(const QStringList &prefixPaths) { d.detach(); d->contentsPrefixPaths = prefixPaths; if (d->contentsPrefixPaths.isEmpty()) { d->contentsPrefixPaths << QString(); } else { // the code assumes that the prefixes have a trailing slash // so let's make that true here QMutableStringListIterator it(d->contentsPrefixPaths); while (it.hasNext()) { it.next(); if (!it.value().endsWith(QLatin1Char('/'))) { it.setValue(it.value() % QLatin1Char('/')); } } } } #ifndef PACKAGE_NO_DEPRECATED QString Package::contentsHash() const { return QString::fromLocal8Bit(cryptographicHash(QCryptographicHash::Sha1)); } #endif QByteArray Package::cryptographicHash(QCryptographicHash::Algorithm algorithm) const { if (!d->valid) { qCWarning(KPACKAGE_LOG) << "can not create hash due to Package being invalid"; return QByteArray(); } QCryptographicHash hash(algorithm); const QString metadataPath = QFile::exists(d->path + QLatin1String("metadata.json")) ? d->path + QLatin1String("metadata.json") : QFile::exists(d->path + QLatin1String("metadata.desktop")) ? d->path + QLatin1String("metadata.desktop") : QString(); if (!metadataPath.isEmpty()) { QFile f(metadataPath); if (f.open(QIODevice::ReadOnly)) { while (!f.atEnd()) { hash.addData(f.read(1024)); } } else { qCWarning(KPACKAGE_LOG) << "could not add" << f.fileName() << "to the hash; file could not be opened for reading."; } } else { qCWarning(KPACKAGE_LOG) << "no metadata at" << metadataPath; } foreach (const QString &prefix, d->contentsPrefixPaths) { const QString basePath = d->path + prefix; QDir dir(basePath); if (!dir.exists()) { return QByteArray(); } d->updateHash(basePath, QString(), dir, hash); } return hash.result().toHex(); } void Package::addDirectoryDefinition(const QByteArray &key, const QString &path, const QString &name) { const auto contentsIt = d->contents.constFind(key); ContentStructure s; if (contentsIt != d->contents.constEnd()) { if (contentsIt->paths.contains(path) && contentsIt->directory == true && contentsIt->name == name) { return; } s = *contentsIt; } d.detach(); if (!name.isEmpty()) { s.name = name; } s.paths.append(path); s.directory = true; d->contents[key] = s; } void Package::addFileDefinition(const QByteArray &key, const QString &path, const QString &name) { const auto contentsIt = d->contents.constFind(key); ContentStructure s; if (contentsIt != d->contents.constEnd()) { if (contentsIt->paths.contains(path) && contentsIt->directory == true && contentsIt->name == name) { return; } s = *contentsIt; } d.detach(); if (!name.isEmpty()) { s.name = name; } s.paths.append(path); s.directory = false; d->contents[key] = s; } void Package::removeDefinition(const QByteArray &key) { if (d->contents.contains(key)) { d.detach(); d->contents.remove(key); } if (d->discoveries.contains(QString::fromLatin1(key))) { d.detach(); d->discoveries.remove(QString::fromLatin1(key)); } } void Package::setRequired(const QByteArray &key, bool required) { QHash::iterator it = d->contents.find(key); if (it == d->contents.end()) { return; } d.detach(); // have to find the item again after detaching: d->contents is a different object now it = d->contents.find(key); it.value().required = required; } void Package::setDefaultMimeTypes(const QStringList &mimeTypes) { d.detach(); d->mimeTypes = mimeTypes; } void Package::setMimeTypes(const QByteArray &key, const QStringList &mimeTypes) { QHash::iterator it = d->contents.find(key); if (it == d->contents.end()) { return; } d.detach(); // have to find the item again after detaching: d->contents is a different object now it = d->contents.find(key); it.value().mimeTypes = mimeTypes; } QList Package::directories() const { QList dirs; QHash::const_iterator it = d->contents.constBegin(); while (it != d->contents.constEnd()) { if (it.value().directory) { dirs << it.key(); } ++it; } return dirs; } QList Package::requiredDirectories() const { QList dirs; QHash::const_iterator it = d->contents.constBegin(); while (it != d->contents.constEnd()) { if (it.value().directory && it.value().required) { dirs << it.key(); } ++it; } return dirs; } QList Package::files() const { QList files; QHash::const_iterator it = d->contents.constBegin(); while (it != d->contents.constEnd()) { if (!it.value().directory) { files << it.key(); } ++it; } return files; } QList Package::requiredFiles() const { QList files; QHash::const_iterator it = d->contents.constBegin(); while (it != d->contents.constEnd()) { if (!it.value().directory && it.value().required) { files << it.key(); } ++it; } return files; } KJob *Package::install(const QString &sourcePackage, const QString &packageRoot) { const QString src = sourcePackage; QString dest = packageRoot.isEmpty() ? defaultPackageRoot() : packageRoot; KPackage::PackageLoader::self()->d->maxCacheAge = -1; //use absolute paths if passed, otherwise go under share if (!QDir::isAbsolutePath(dest)) { dest = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + dest; } if (!d->structure) { return nullptr; } //qCDebug(KPACKAGE_LOG) << "Source: " << src; //qCDebug(KPACKAGE_LOG) << "PackageRoot: " << dest; KJob *j = d->structure.data()->install(this, src, dest); return j; } KJob *Package::update(const QString &sourcePackage, const QString &packageRoot) { const QString src = sourcePackage; QString dest = packageRoot.isEmpty() ? defaultPackageRoot() : packageRoot; KPackage::PackageLoader::self()->d->maxCacheAge = -1; //use absolute paths if passed, otherwise go under share if (!QDir::isAbsolutePath(dest)) { dest = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + dest; } if (!d->structure) { return nullptr; } //qCDebug(KPACKAGE_LOG) << "Source: " << src; //qCDebug(KPACKAGE_LOG) << "PackageRoot: " << dest; KJob *j = d->structure.data()->update(this, src, dest); return j; } KJob *Package::uninstall(const QString &packageName, const QString &packageRoot) { KPackage::PackageLoader::self()->d->maxCacheAge = -1; d->createPackageMetadata(packageRoot + QLatin1Char('/') + packageName); if (!d->structure) { return nullptr; } return d->structure.data()->uninstall(this, packageRoot); } PackagePrivate::PackagePrivate() : QSharedData(), fallbackPackage(nullptr), metadata(nullptr), externalPaths(false), valid(false), checkedValid(false) { contentsPrefixPaths << QStringLiteral("contents/"); } PackagePrivate::PackagePrivate(const PackagePrivate &other) : QSharedData() { *this = other; metadata = nullptr; } PackagePrivate::~PackagePrivate() { if (!rccPath.isEmpty()) { //qresource register/unregisterpath is refcounted if we call it two times //on the same path, the resource will actually be unregistered only when //unregister is called 2 times QResource::unregisterResource(rccPath); } if (!tempRoot.isEmpty()) { QDir dir(tempRoot); dir.removeRecursively(); } delete metadata; delete fallbackPackage; } PackagePrivate &PackagePrivate::operator=(const PackagePrivate &rhs) { if (&rhs == this) { return *this; } structure = rhs.structure; if (rhs.fallbackPackage) { fallbackPackage = new Package(*rhs.fallbackPackage); } else { fallbackPackage = nullptr; } path = rhs.path; contentsPrefixPaths = rhs.contentsPrefixPaths; contents = rhs.contents; mimeTypes = rhs.mimeTypes; defaultPackageRoot = rhs.defaultPackageRoot; metadata = nullptr; externalPaths = rhs.externalPaths; valid = rhs.valid; return *this; } void PackagePrivate::updateHash(const QString &basePath, const QString &subPath, const QDir &dir, QCryptographicHash &hash) { // hash is calculated as a function of: // * files ordered alphabetically by name, with each file's: // * path relative to the content root // * file data // * directories ordered alphabetically by name, with each dir's: // * path relative to the content root // * file listing (recursing) // symlinks (in both the file and dir case) are handled by adding // the name of the symlink itself and the abs path of what it points to const QDir::SortFlags sorting = QDir::Name | QDir::IgnoreCase; const QDir::Filters filters = QDir::Hidden | QDir::System | QDir::NoDotAndDotDot; foreach (const QString &file, dir.entryList(QDir::Files | filters, sorting)) { if (!subPath.isEmpty()) { hash.addData(subPath.toUtf8()); } hash.addData(file.toUtf8()); QFileInfo info(dir.path() + QLatin1Char('/') + file); if (info.isSymLink()) { hash.addData(info.symLinkTarget().toUtf8()); } else { QFile f(info.filePath()); if (f.open(QIODevice::ReadOnly)) { while (!f.atEnd()) { hash.addData(f.read(1024)); } } else { qCWarning(KPACKAGE_LOG) << "could not add" << f.fileName() << "to the hash; file could not be opened for reading. " << "permissions fail?" << info.permissions() << info.isFile(); } } } foreach (const QString &subDirPath, dir.entryList(QDir::Dirs | filters, sorting)) { const QString relativePath = subPath + subDirPath + QLatin1Char('/'); hash.addData(relativePath.toUtf8()); QDir subDir(dir.path()); subDir.cd(subDirPath); if (subDir.path() != subDir.canonicalPath()) { hash.addData(subDir.canonicalPath().toUtf8()); } else { updateHash(basePath, relativePath, subDir, hash); } } } void PackagePrivate::createPackageMetadata(const QString &path) { const bool isDir = QFileInfo(path).isDir(); delete metadata; if (isDir && QFile::exists(path + QStringLiteral("/metadata.json"))) { metadata = new KPluginMetaData(path + QStringLiteral("/metadata.json")); } else if (isDir && QFile::exists(path + QStringLiteral("/metadata.desktop"))) { auto md = KPluginMetaData::fromDesktopFile(path + QStringLiteral("/metadata.desktop"), {QStringLiteral(":/kservicetypes5/kpackage-generic.desktop")}); metadata = new KPluginMetaData(md); } else { if (isDir) { qCWarning(KPACKAGE_LOG) << "No metadata file in the package, expected it at:" << path; } else if (path.endsWith(QLatin1String(".desktop"))) { auto md = KPluginMetaData::fromDesktopFile(path, {QStringLiteral(":/kservicetypes5/kpackage-generic.desktop")}); metadata = new KPluginMetaData(md); } else { metadata = new KPluginMetaData(path); } } } QString PackagePrivate::fallbackFilePath(const QByteArray &key, const QString &filename) const { //don't fallback if the package isn't valid and never fallback the metadata file if (qstrcmp(key, "metadata") != 0 && fallbackPackage && fallbackPackage->isValid()) { return fallbackPackage->filePath(key, filename); } else { return QString(); } } bool PackagePrivate::hasCycle(const KPackage::Package &package) { if (!package.d->fallbackPackage) { return false; } //This is the Floyd cycle detection algorithm //http://en.wikipedia.org/wiki/Cycle_detection#Tortoise_and_hare KPackage::Package *slowPackage = const_cast(&package); KPackage::Package *fastPackage = const_cast(&package); while (fastPackage && fastPackage->d->fallbackPackage) { //consider two packages the same if they have the same metadata if ((fastPackage->d->fallbackPackage->metadata().isValid() && fastPackage->d->fallbackPackage->metadata() == slowPackage->metadata()) || (fastPackage->d->fallbackPackage->d->fallbackPackage && fastPackage->d->fallbackPackage->d->fallbackPackage->metadata().isValid() && fastPackage->d->fallbackPackage->d->fallbackPackage->metadata() == slowPackage->metadata())) { qCWarning(KPACKAGE_LOG) << "Warning: the fallback chain of " << package.metadata().pluginId() << "contains a cyclical dependency."; return true; } fastPackage = fastPackage->d->fallbackPackage->d->fallbackPackage; slowPackage = slowPackage->d->fallbackPackage; } return false; } Q_GLOBAL_STATIC(PackageDeletionNotifier, s_packageDeletionNotifier) PackageDeletionNotifier* PackageDeletionNotifier::self() { return s_packageDeletionNotifier; } } // Namespace #include "private/moc_package_p.cpp" diff --git a/src/kpackage/package.h b/src/kpackage/package.h index af30d30..cd89af0 100644 --- a/src/kpackage/package.h +++ b/src/kpackage/package.h @@ -1,406 +1,406 @@ /****************************************************************************** * Copyright 2007-2011 by Aaron Seigo * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public License * * along with this library; see the file COPYING.LIB. If not, write to * * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301, USA. * *******************************************************************************/ #ifndef KPACKAGE_PACKAGE_H #define KPACKAGE_PACKAGE_H #include #include #include #include #include #include #include namespace KPackage { /** * @class Package kpackage/package.h * * @short object representing an installed package * * Package defines what is in a package and provides easy access to the contents. * * To define a package, one might write the following code: * @code Package package; package.addDirectoryDefinition("images", "pics/", i18n("Images")); QStringList mimeTypes; mimeTypes << "image/svg" << "image/png" << "image/jpeg"; package.setMimeTypes("images", mimeTypes); package.addDirectoryDefinition("scripts", "code/", i18n("Executable Scripts")); mimeTypes.clear(); mimeTypes << "text/\*"; package.setMimeTypes("scripts", mimeTypes); package.addFileDefinition("mainscript", "code/main.js", i18n("Main Script File")); package.setRequired("mainscript", true); @endcode * One may also choose to create a subclass of PackageStructure and include the setup * in the constructor. * * Either way, Package creates a self-documenting contract between the packager and * the application without exposing package internals such as actual on-disk structure * of the package or requiring that all contents be explicitly known ahead of time. * * Subclassing PackageStructure does have provide a number of potential const benefits: * * the package can be notified of path changes via the virtual pathChanged() method * * the subclass may implement mechanisms to install and remove packages using the * virtual install and uninstall methods * * subclasses can be compiled as plugins for easy re-use **/ //TODO: write documentation on USING a package class PackagePrivate; class PackageStructure; class PACKAGE_EXPORT Package { public: /** * Error codes for the install/update/remove jobs * @since 5.17 */ enum JobError { RootCreationError = KJob::UserDefinedError + 1, /**< Cannot create package root directory */ PackageFileNotFoundError, /**< The package file does not exist */ UnsupportedArchiveFormatError, /**< The archive format of the package is not supported */ PackageOpenError, /**< Can't open the package file for reading */ MetadataFileMissingError, /**< The package doesn't have a metadata.desktop file */ PluginNameMissingError, /**< The metadata.desktop file doesn't specify a plugin name */ - PluginNameInvalidError, /**< The plugin name contains charaters different from letters, digits, dots and underscores */ + PluginNameInvalidError, /**< The plugin name contains characters different from letters, digits, dots and underscores */ UpdatePackageTypeMismatchError, /**< A package with this plugin name was already installed, but has a different type in the metadata.desktop file */ OldVersionRemovalError, /**< Failed to remove the old version of the package during an upgrade */ NewerVersionAlreadyInstalledError, /**< We tried to update, but the same version or a newer one is already installed */ PackageAlreadyInstalledError, /**< The package is already installed and a normal install (not update) was performed */ PackageMoveError, /**< Failure to move a package from the system temporary folder to its final destination */ PackageCopyError, /**< Failure to copy a package folder from somewhere in the filesystem to its final destination */ PackageUninstallError /**< Failure to uninstall a package */ }; /** * Default constructor * * @param structure if a null pointer is passed in, this will creates an empty (invalid) Package; * otherwise the structure is allowed to set up the Package's initial layout * @since 4.6 */ explicit Package(PackageStructure *structure = nullptr); /** * Copy constructor * @since 4.6 */ Package(const Package &other); virtual ~Package(); /** * Assignment operator * @since 4.6 */ Package &operator=(const Package &rhs); /** * @return true if this package has a valid PackageStructure associatedw it with it. * A package may not be valid, but have a valid structure. Useful when dealing with * Package objects in a semi-initialized state (e.g. before calling setPath()) * @since 5.1 */ bool hasValidStructure() const; /** * @return true if all the required components exist **/ bool isValid() const; /** * Sets the path to the root of this package * @param path an absolute path, or a relative path to the default package root * @since 4.3 */ void setPath(const QString &path); /** * @return the path to the root of this particular package */ const QString path() const; /** * Get the path to a given file based on the key and an optional filename. * Example: finding the main script in a scripting package: * filePath("mainscript") * * Example: finding a specific image in the images directory: * filePath("images", "myimage.png") * * @param key the key of the file type to look for, * @param filename optional name of the file to locate within the package * @return path to the file on disk. QString() if not found. **/ QString filePath(const QByteArray &key, const QString &filename = QString()) const; /** * Get the url to a given file based on the key and an optional filename, is the file:// or qrc:// format * Example: finding the main script in a scripting package: * filePath("mainscript") * * Example: finding a specific image in the images directory: * filePath("images", "myimage.png") * * @param key the key of the file type to look for, * @param filename optional name of the file to locate within the package * @return path to the file on disk. QString() if not found. * @since 5.41 **/ QUrl fileUrl(const QByteArray &key, const QString &filename = QString()) const; /** * Get the list of files of a given type. * * @param fileType the type of file to look for, as defined in the * package structure. * @return list of files by name, suitable for passing to filePath **/ QStringList entryList(const QByteArray &key) const; /** * @return user visible name for the given entry **/ QString name(const QByteArray &key) const; /** * @return true if the item at path exists and is required **/ bool isRequired(const QByteArray &key) const; /** * @return the mimeTypes associated with the path, if any **/ QStringList mimeTypes(const QByteArray &key) const; /** * @return the prefix paths inserted between the base path and content entries, in order of priority. * When searching for a file, all paths will be tried in order. * @since 4.6 */ QStringList contentsPrefixPaths() const; /** * @return preferred package root. This defaults to kpackage/generic/ */ QString defaultPackageRoot() const; /** * @return true if paths/symlinks outside the package itself should be followed. * By default this is set to false for security reasons. */ bool allowExternalPaths() const; /** * @return the package metadata object. */ KPluginMetaData metadata() const; /** * @return a SHA1 hash digest of the contents of the package in hexadecimal form * @since 4.4 * @deprecated Since 5.21 use cryptographicHash */ #ifndef PACKAGE_NO_DEPRECATED PACKAGE_DEPRECATED QString contentsHash() const; #endif /** * @return a hash digest of the contents of the package in hexadecimal form * @since 5.21 */ QByteArray cryptographicHash(QCryptographicHash::Algorithm algorithm) const; /** * Adds a directory to the structure of the package. It is added as * a not-required element with no associated mimeTypes. * * Starting in 4.6, if an entry with the given key * already exists, the path is added to it as a search alternative. * * @param key used as an internal label for this directory * @param path the path within the package for this directory * @param name the user visible (translated) name for the directory **/ void addDirectoryDefinition(const QByteArray &key, const QString &path, const QString &name); /** * Adds a file to the structure of the package. It is added as * a not-required element with no associated mimeTypes. * * Starting in 4.6, if an entry with the given key * already exists, the path is added to it as a search alternative. * * @param key used as an internal label for this file * @param path the path within the package for this file * @param name the user visible (translated) name for the file **/ void addFileDefinition(const QByteArray &key, const QString &path, const QString &name); /** * Removes a definition from the structure of the package. * @since 4.6 * @param key the internal label of the file or directory to remove */ void removeDefinition(const QByteArray &key); /** * Sets whether or not a given part of the structure is required or not. * The path must already have been added using addDirectoryDefinition * or addFileDefinition. * * @param key the entry within the package * @param required true if this entry is required, false if not */ void setRequired(const QByteArray &key, bool required); /** * Defines the default mimeTypes for any definitions that do not have * associated mimeTypes. Handy for packages with only one or predominantly * one file type. * * @param mimeTypes a list of mimeTypes **/ void setDefaultMimeTypes(const QStringList &mimeTypes); /** * Define mimeTypes for a given part of the structure * The path must already have been added using addDirectoryDefinition * or addFileDefinition. * * @param key the entry within the package * @param mimeTypes a list of mimeTypes **/ void setMimeTypes(const QByteArray &key, const QStringList &mimeTypes); /** * Sets the prefixes that all the contents in this package should * appear under. This defaults to "contents/" and is added automatically * between the base path and the entries as defined by the package * structure. Multiple entries can be added. * In this case each file request will be searched in all prefixes in order, * and the first found will be returned. * * @param prefix paths the directory prefix to use * @since 4.6 */ void setContentsPrefixPaths(const QStringList &prefixPaths); /** * Sets whether or not external paths/symlinks can be followed by a package * @param allow true if paths/symlinks outside of the package should be followed, * false if they should be rejected. */ void setAllowExternalPaths(bool allow); /** * Sets preferred package root. */ void setDefaultPackageRoot(const QString &packageRoot); /** * Sets the fallback package root path * If a file won't be found in this package, it will search it in the package * with the same structure identified by path * It is intended to be used by the packageStructure * @param path package root path @see setPath */ void setFallbackPackage(const KPackage::Package &package); /** * @return The fallback package root path */ KPackage::Package fallbackPackage() const; // Content structure description methods /** * @return all directories registered as part of this Package's structure */ QList directories() const; /** * @return all directories registered as part of this Package's required structure */ QList requiredDirectories() const; /** * @return all files registered as part of this Package's structure */ QList files() const; /** * @return all files registered as part of this Package's required structure */ QList requiredFiles() const; /** * Installs a package matching this package structure. By default installs a * native KPackage::Package. * After the KJob will emitted finished(), if the install went well * the Package instance will be guaranteed to have loaded the package just * installed, and be valid and usable. * * @return KJob to track installation progress and result **/ KJob *install(const QString &sourcePackage, const QString &packageRoot = QString()); /** * Updates a package matching this package structure. By default installs a * native KPackage::Package. If an older version is already installed, * it removes the old one. If the installed one is newer, * an error will occur. * After the KJob will emitted finished(), if the install went well * the Package instance will be guaranteed to have loaded the package just * updated, and be valid and usable. * * @return KJob to track installation progress and result * @since 5.17 **/ KJob *update(const QString &sourcePackage, const QString &packageRoot = QString()); /** * Uninstalls a package matching this package structure. * * @return KJob to track removal progress and result */ KJob *uninstall(const QString &packageName, const QString &packageRoot); private: QExplicitlySharedDataPointer d; friend class PackagePrivate; }; } Q_DECLARE_METATYPE(KPackage::Package) #endif diff --git a/src/kpackage/packageloader.cpp b/src/kpackage/packageloader.cpp index 33d763b..aa30484 100644 --- a/src/kpackage/packageloader.cpp +++ b/src/kpackage/packageloader.cpp @@ -1,392 +1,392 @@ /* * Copyright 2010 Ryan Rix * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "packageloader.h" #include "private/packageloader_p.h" #include #include #include #include #include #include #include #include "kpackage_debug.h" #include #include #include #include #include "config-package.h" #include "private/packages_p.h" #include "package.h" #include "packagestructure.h" #include "private/packagejobthread_p.h" namespace KPackage { static PackageLoader *s_packageTrader = nullptr; QSet PackageLoaderPrivate::s_customCategories; QSet PackageLoaderPrivate::knownCategories() { - // this is to trick the tranlsation tools into making the correct + // this is to trick the translation tools into making the correct // strings for translation QSet categories = s_customCategories; categories << QStringLiteral(I18N_NOOP("Accessibility")).toLower() << QStringLiteral(I18N_NOOP("Application Launchers")).toLower() << QStringLiteral(I18N_NOOP("Astronomy")).toLower() << QStringLiteral(I18N_NOOP("Date and Time")).toLower() << QStringLiteral(I18N_NOOP("Development Tools")).toLower() << QStringLiteral(I18N_NOOP("Education")).toLower() << QStringLiteral(I18N_NOOP("Environment and Weather")).toLower() << QStringLiteral(I18N_NOOP("Examples")).toLower() << QStringLiteral(I18N_NOOP("File System")).toLower() << QStringLiteral(I18N_NOOP("Fun and Games")).toLower() << QStringLiteral(I18N_NOOP("Graphics")).toLower() << QStringLiteral(I18N_NOOP("Language")).toLower() << QStringLiteral(I18N_NOOP("Mapping")).toLower() << QStringLiteral(I18N_NOOP("Miscellaneous")).toLower() << QStringLiteral(I18N_NOOP("Multimedia")).toLower() << QStringLiteral(I18N_NOOP("Online Services")).toLower() << QStringLiteral(I18N_NOOP("Productivity")).toLower() << QStringLiteral(I18N_NOOP("System Information")).toLower() << QStringLiteral(I18N_NOOP("Utilities")).toLower() << QStringLiteral(I18N_NOOP("Windows and Tasks")).toLower(); return categories; } QString PackageLoaderPrivate::parentAppConstraint(const QString &parentApp) { if (parentApp.isEmpty()) { const QCoreApplication *app = QCoreApplication::instance(); if (!app) { return QString(); } return QStringLiteral("((not exist [X-KDE-ParentApp] or [X-KDE-ParentApp] == '') or [X-KDE-ParentApp] == '%1')") .arg(app->applicationName()); } return QStringLiteral("[X-KDE-ParentApp] == '%1'").arg(parentApp); } PackageLoader::PackageLoader() : d(new PackageLoaderPrivate) { } PackageLoader::~PackageLoader() { foreach (auto wp, d->structures) { delete wp.data(); } delete d; } void PackageLoader::setPackageLoader(PackageLoader *loader) { if (!s_packageTrader) { s_packageTrader = loader; } else { #ifndef NDEBUG // qCDebug(KPACKAGE_LOG) << "Cannot set packageTrader, already set!" << s_packageTrader; #endif } } PackageLoader *PackageLoader::self() { if (!s_packageTrader) { // we have been called before any PackageLoader was set, so just use the default // implementation. this prevents plugins from nefariously injecting their own // plugin loader if the app doesn't s_packageTrader = new PackageLoader; s_packageTrader->d->isDefaultLoader = true; } return s_packageTrader; } Package PackageLoader::loadPackage(const QString &packageFormat, const QString &packagePath) { if (!d->isDefaultLoader) { Package p = internalLoadPackage(packageFormat); if (p.hasValidStructure()) { if (!packagePath.isEmpty()) { p.setPath(packagePath); } return p; } } if (packageFormat.isEmpty()) { return Package(); } PackageStructure *structure = loadPackageStructure(packageFormat); if (structure) { Package p(structure); if (!packagePath.isEmpty()) { p.setPath(packagePath); } return p; } #ifndef NDEBUG // qCDebug(KPACKAGE_LOG) << "Couldn't load Package for" << packageFormat << "! reason given: " << error; #endif return Package(); } QList PackageLoader::listPackages(const QString &packageFormat, const QString &packageRoot) { // Note: Use QDateTime::currentSecsSinceEpoch() once we can depend on Qt 5.8 const qint64 now = qRound64(QDateTime::currentMSecsSinceEpoch() / 1000.0); bool useRuntimeCache = true; if (now - d->pluginCacheAge > d->maxCacheAge && d->pluginCacheAge != 0) { // cache is old and we're not within a few seconds of startup anymore useRuntimeCache = false; d->pluginCache.clear(); } QString cacheKey = QString(QStringLiteral("%1.%2")).arg(packageFormat, packageRoot); if (useRuntimeCache && d->pluginCache.contains(cacheKey)) { return d->pluginCache.value(cacheKey); } if (d->pluginCacheAge == 0) { d->pluginCacheAge = now; } QList lst; //has been a root specified? QString actualRoot = packageRoot; //try to take it from the package structure if (actualRoot.isEmpty()) { PackageStructure *structure = d->structures.value(packageFormat).data(); if (!structure) { if (packageFormat == QStringLiteral("KPackage/Generic")) { structure = new GenericPackage(); } else if (packageFormat == QStringLiteral("KPackage/GenericQML")) { structure = new GenericQMLPackage(); } } if (!structure) { structure = loadPackageStructure(packageFormat); } if (structure) { d->structures.insert(packageFormat, structure); Package p(structure); actualRoot = p.defaultPackageRoot(); } } if (actualRoot.isEmpty()) { actualRoot = packageFormat; } QSet uniqueIds; QStringList paths; if (QDir::isAbsolutePath(actualRoot)) { paths = QStringList(actualRoot); } else { foreach(const QString &path, QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation)) { paths += path + QLatin1Char('/') + actualRoot; } } Q_FOREACH(auto const &plugindir, paths) { const QString &_ixfile = plugindir + s_kpluginindex; KCompressionDevice indexFile(_ixfile, KCompressionDevice::BZip2); if (QFile::exists(_ixfile)) { qCDebug(KPACKAGE_LOG) << "kpluginindex: Using indexfile: " << _ixfile; indexFile.open(QIODevice::ReadOnly); QJsonDocument jdoc = QJsonDocument::fromBinaryData(indexFile.readAll()); indexFile.close(); QJsonArray plugins = jdoc.array(); for (QJsonArray::const_iterator iter = plugins.constBegin(); iter != plugins.constEnd(); ++iter) { const QJsonObject &obj = QJsonValue(*iter).toObject(); const QString &pluginFileName = obj.value(QStringLiteral("FileName")).toString(); const KPluginMetaData m(obj, QString(), pluginFileName); if (m.isValid() && !uniqueIds.contains(m.pluginId())) { uniqueIds << m.pluginId(); lst << m; } } } else { qCDebug(KPACKAGE_LOG) << "kpluginindex: Not cached" << plugindir; // If there's no cache file, fall back to listing the directory const QDirIterator::IteratorFlags flags = QDirIterator::Subdirectories; const QStringList nameFilters = { QStringLiteral("metadata.json"), QStringLiteral("metadata.desktop") }; QDirIterator it(plugindir, nameFilters, QDir::Files, flags); QSet dirs; while (it.hasNext()) { it.next(); const QString dir = it.fileInfo().absoluteDir().path(); if (dirs.contains(dir)) { continue; } dirs << dir; const QString metadataPath = it.fileInfo().absoluteFilePath(); const KPluginMetaData info(metadataPath); if (!info.isValid() || uniqueIds.contains(info.pluginId())) { continue; } if (packageFormat.isEmpty() || info.serviceTypes().isEmpty() || info.serviceTypes().contains(packageFormat)) { uniqueIds << info.pluginId(); lst << info; } } } } if (useRuntimeCache) { d->pluginCache.insert(cacheKey, lst); } return lst; } QList PackageLoader::findPackages(const QString &packageFormat, const QString &packageRoot, std::function filter) { QList lst; Q_FOREACH(auto const &plugin, listPackages(packageFormat, packageRoot)) { if (!filter || filter(plugin)) { lst << plugin; } } return lst; } KPackage::PackageStructure *PackageLoader::loadPackageStructure(const QString &packageFormat) { PackageStructure *structure = d->structures.value(packageFormat).data(); if (!structure) { if (packageFormat == QStringLiteral("KPackage/Generic")) { structure = new GenericPackage(); d->structures.insert(packageFormat, structure); } else if (packageFormat == QStringLiteral("KPackage/GenericQML")) { structure = new GenericQMLPackage(); d->structures.insert(packageFormat, structure); } } if (structure) { return structure; } QStringList libraryPaths; const QString subDirectory = QStringLiteral("kpackage/packagestructure"); Q_FOREACH (const QString &dir, QCoreApplication::libraryPaths()) { QString d = dir + QDir::separator() + subDirectory; if (!d.endsWith(QDir::separator())) { d += QDir::separator(); } libraryPaths << d; } QString pluginFileName; Q_FOREACH (const QString &plugindir, libraryPaths) { const QString &_ixfile = plugindir + s_kpluginindex; KCompressionDevice indexFile(_ixfile, KCompressionDevice::BZip2); if (QFile::exists(_ixfile)) { indexFile.open(QIODevice::ReadOnly); QJsonDocument jdoc = QJsonDocument::fromBinaryData(indexFile.readAll()); indexFile.close(); QJsonArray plugins = jdoc.array(); for (QJsonArray::const_iterator iter = plugins.constBegin(); iter != plugins.constEnd(); ++iter) { const QJsonObject &obj = QJsonValue(*iter).toObject(); const QString &candidate = obj.value(QStringLiteral("FileName")).toString(); const KPluginMetaData m(obj, candidate); if (m.isValid() && m.pluginId() == packageFormat) { pluginFileName = candidate; break; } } } else { QVector plugins = KPluginLoader::findPlugins(plugindir); QVectorIterator iter(plugins); while (iter.hasNext()) { auto md = iter.next(); if (md.isValid() && md.pluginId() == packageFormat) { pluginFileName = md.fileName(); break; } } } } QString error; if (!pluginFileName.isEmpty()) { KPluginLoader loader(pluginFileName); const QVariantList argsWithMetaData = QVariantList() << loader.metaData().toVariantMap(); KPluginFactory *factory = loader.factory(); if (factory) { structure = factory->create(nullptr, argsWithMetaData); if (!structure) { error = QCoreApplication::translate("", "No service matching the requirements was found"); } } } if (structure && !error.isEmpty()) { qCWarning(KPACKAGE_LOG) << i18n("Could not load installer for package of type %1. Error reported was: %2", packageFormat, error); } if (structure) { d->structures.insert(packageFormat, structure); } return structure; } void PackageLoader::addKnownPackageStructure(const QString &packageFormat, KPackage::PackageStructure *structure) { d->structures.insert(packageFormat, structure); } Package PackageLoader::internalLoadPackage(const QString &name) { Q_UNUSED(name); return Package(); } } // KPackage Namespace diff --git a/src/kpackage/private/package_p.h b/src/kpackage/private/package_p.h index c151cc4..5b4d0a7 100644 --- a/src/kpackage/private/package_p.h +++ b/src/kpackage/private/package_p.h @@ -1,113 +1,113 @@ /* * Copyright © 2009 Rob Scheepmaker * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License version 2 as * published by the Free Software Foundation * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KPACKAGE_PACKAGE_P_H #define KPACKAGE_PACKAGE_P_H #include "../package.h" #include #include #include #include #include namespace KPackage { //KPackage is is normally used on the stack, explicitly shared and isn't a QObject //however PackageJob is given a pointer, which could be deleted at any moment //leaving the PackageJob with a dangling pointer //we need some way to invalidate the Package* pointer if it gets deleted -//we can't just take a copy in the packagejob as we need to detatch and update the *original* KPackage object +//we can't just take a copy in the packagejob as we need to detach and update the *original* KPackage object //without changing anything else which happened to share the same KPackage::d //TODO KF6 - make KPackage::install()'s KJob return a new Package copy rather than modify -//an existing opbject. +//an existing object. class PackageDeletionNotifier : public QObject { Q_OBJECT public: static PackageDeletionNotifier* self(); Q_SIGNALS: void packageDeleted(Package *package); }; class ContentStructure { public: ContentStructure() : directory(false), required(false) { } ContentStructure(const ContentStructure &other) { paths = other.paths; name = other.name; mimeTypes = other.mimeTypes; directory = other.directory; required = other.required; } QString found; QStringList paths; QString name; QStringList mimeTypes; bool directory : 1; bool required : 1; }; class PackagePrivate : public QSharedData { public: PackagePrivate(); PackagePrivate(const PackagePrivate &other); ~PackagePrivate(); PackagePrivate &operator=(const PackagePrivate &rhs); void createPackageMetadata(const QString &path); QString unpack(const QString &filePath); void updateHash(const QString &basePath, const QString &subPath, const QDir &dir, QCryptographicHash &hash); QString fallbackFilePath(const QByteArray &key, const QString &filename = QString()) const; bool hasCycle(const KPackage::Package &package); bool isInsidePackageDir(const QString& canonicalPath) const; QPointer structure; QString path; QString tempRoot; QStringList contentsPrefixPaths; QString defaultPackageRoot; QHash discoveries; QHash contents; Package *fallbackPackage; QStringList mimeTypes; KPluginMetaData *metadata; QString rccPath; bool externalPaths : 1; bool valid : 1; bool checkedValid : 1; }; } #endif diff --git a/src/kpackage/private/packagejobthread.cpp b/src/kpackage/private/packagejobthread.cpp index a2cc2b1..6ad6c8d 100644 --- a/src/kpackage/private/packagejobthread.cpp +++ b/src/kpackage/private/packagejobthread.cpp @@ -1,475 +1,475 @@ /****************************************************************************** * Copyright 2007-2009 by Aaron Seigo * * Copyright 2012 Sebastian Kügler * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public License * * along with this library; see the file COPYING.LIB. If not, write to * * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301, USA. * *******************************************************************************/ #include "private/packagejobthread_p.h" #include "package.h" #include "config-package.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kpackage_debug.h" namespace KPackage { bool copyFolder(QString sourcePath, QString targetPath) { QDir source(sourcePath); if (!source.exists()) { return false; } QDir target(targetPath); if (!target.exists()) { QString targetName = target.dirName(); target.cdUp(); target.mkdir(targetName); target = QDir(targetPath); } foreach (const QString &fileName, source.entryList(QDir::Files)) { QString sourceFilePath = sourcePath + QDir::separator() + fileName; QString targetFilePath = targetPath + QDir::separator() + fileName; if (!QFile::copy(sourceFilePath, targetFilePath)) { return false; } } foreach (const QString &subFolderName, source.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) { QString sourceSubFolderPath = sourcePath + QDir::separator() + subFolderName; QString targetSubFolderPath = targetPath + QDir::separator() + subFolderName; if (!copyFolder(sourceSubFolderPath, targetSubFolderPath)) { return false; } } return true; } bool removeFolder(QString folderPath) { QDir folder(folderPath); return folder.removeRecursively(); } bool removeIndex(const QString& dir) { bool ok = true; QFileInfo fileInfo(dir, QStringLiteral("kpluginindex.json")); if (fileInfo.exists()) { if (fileInfo.isWritable()) { QFile f(fileInfo.absoluteFilePath()); if (!f.remove()) { ok = false; qCWarning(KPACKAGE_LOG) << "Cannot remove kplugin index file: " << fileInfo.absoluteFilePath(); } else { qCDebug(KPACKAGE_LOG) << "Deleted index: " << fileInfo.absoluteFilePath(); } } else { qCWarning(KPACKAGE_LOG) << "Cannot remove kplugin index file (not writable): " << fileInfo.absoluteFilePath(); ok = false; } } return ok; } Q_GLOBAL_STATIC_WITH_ARGS(QStringList, metaDataFiles, (QStringList(QLatin1String("metadata.desktop")) << QLatin1String("metadata.json"))) bool indexDirectory(const QString& dir, const QString& dest) { QVariantMap vm; vm[QStringLiteral("Version")] = QStringLiteral("1.0"); vm[QStringLiteral("Timestamp")] = QDateTime::currentMSecsSinceEpoch(); QJsonArray plugins; QDirIterator it(dir, *metaDataFiles, QDir::Files, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); const QString path = it.fileInfo().absoluteFilePath(); QJsonObject obj = KPluginMetaData(path).rawData(); obj.insert(QStringLiteral("FileName"), path); plugins.append(obj); } // Less than two plugin means it's not worth indexing if (plugins.count() < 2) { removeIndex(dir); return true; } QString destfile = dest; if (!QDir::isAbsolutePath(dest)) { destfile = dir + QLatin1Char('/') + dest; } QDir().mkpath(QFileInfo(destfile).dir().absolutePath()); KCompressionDevice file(destfile, KCompressionDevice::BZip2); if (!file.open(QIODevice::WriteOnly)) { qCWarning(KPACKAGE_LOG) << "Failed to open " << destfile; return false; } QJsonDocument jdoc; jdoc.setArray(plugins); // file.write(jdoc.toJson()); file.write(jdoc.toBinaryData()); qCWarning(KPACKAGE_LOG) << "Generated " << destfile << " (" << plugins.count() << " plugins)"; return true; } class PackageJobThreadPrivate { public: QString installPath; QString errorMessage; int errorCode; }; PackageJobThread::PackageJobThread(QObject *parent) : QThread(parent) { d = new PackageJobThreadPrivate; d->errorCode = KJob::NoError; } PackageJobThread::~PackageJobThread() { delete d; } bool PackageJobThread::install(const QString &src, const QString &dest) { bool ok = installPackage(src, dest, Install); emit installPathChanged(d->installPath); emit finished(ok, d->errorMessage); return ok; } static QString resolveHandler(const QString &scheme) { QString candidatePath = QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/kpackagehandlers/%1handler").arg(scheme); if (qEnvironmentVariableIsSet("KPACKAGE_DEP_RESOLVERS_PATH")) { candidatePath = QStringLiteral("%1/%2handler").arg(QString::fromUtf8(qgetenv("KPACKAGE_DEP_RESOLVERS_PATH")), scheme); } return QFile::exists(candidatePath) ? candidatePath : QString(); } bool PackageJobThread::installDependency(const QUrl &destUrl) { auto handler = resolveHandler(destUrl.scheme()); if (handler.isEmpty()) return false; QProcess process; process.setProgram(handler); process.setArguments({ destUrl.toString() }); process.setProcessChannelMode(QProcess::ForwardedChannels); process.start(); process.waitForFinished(); return process.exitCode() == 0; } bool PackageJobThread::installPackage(const QString &src, const QString &dest, OperationType operation) { QDir root(dest); if (!root.exists()) { QDir().mkpath(dest); if (!root.exists()) { d->errorMessage = i18n("Could not create package root directory: %1", dest); d->errorCode = Package::JobError::RootCreationError; //qCWarning(KPACKAGE_LOG) << "Could not create package root directory: " << dest; return false; } } QFileInfo fileInfo(src); if (!fileInfo.exists()) { d->errorMessage = i18n("No such file: %1", src); d->errorCode = Package::JobError::PackageFileNotFoundError; return false; } QString path; QTemporaryDir tempdir; bool archivedPackage = false; if (fileInfo.isDir()) { // we have a directory, so let's just install what is in there path = src; // make sure we end in a slash! if (!path.endsWith(QLatin1Char('/'))) { path.append(QLatin1Char('/')); } } else { KArchive *archive = nullptr; QMimeDatabase db; QMimeType mimetype = db.mimeTypeForFile(src); if (mimetype.inherits(QStringLiteral("application/zip"))) { archive = new KZip(src); } else if (mimetype.inherits(QStringLiteral("application/x-compressed-tar")) || mimetype.inherits(QStringLiteral("application/x-tar")) || mimetype.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || mimetype.inherits(QStringLiteral("application/x-xz")) || mimetype.inherits(QStringLiteral("application/x-lzma"))) { archive = new KTar(src); } else { //qCWarning(KPACKAGE_LOG) << "Could not open package file, unsupported archive format:" << src << mimetype.name(); d->errorMessage = i18n("Could not open package file, unsupported archive format: %1 %2", src, mimetype.name()); d->errorCode = Package::JobError::UnsupportedArchiveFormatError; return false; } if (!archive->open(QIODevice::ReadOnly)) { //qCWarning(KPACKAGE_LOG) << "Could not open package file:" << src; delete archive; d->errorMessage = i18n("Could not open package file: %1", src); d->errorCode = Package::JobError::PackageOpenError; return false; } archivedPackage = true; path = tempdir.path() + QLatin1Char('/'); d->installPath = path; const KArchiveDirectory *source = archive->directory(); source->copyTo(path); QStringList entries = source->entries(); if (entries.count() == 1) { const KArchiveEntry *entry = source->entry(entries[0]); if (entry->isDirectory()) { path = path + entry->name() + QLatin1Char('/'); } } delete archive; } QDir packageDir(path); QFileInfoList entries = packageDir.entryInfoList(*metaDataFiles); KPluginMetaData meta; if (!entries.isEmpty()) { const QString metadataFilePath = entries.first().filePath(); if (metadataFilePath.endsWith(QLatin1String(".desktop"))) { meta = KPluginMetaData::fromDesktopFile(metadataFilePath, {QStringLiteral(":/kservicetypes5/kpackage-generic.desktop")}); } else { QFile f(metadataFilePath); if(!f.open(QIODevice::ReadOnly)){ qCWarning(KPACKAGE_LOG) << "Couldn't open metadata file" << src << path; d->errorMessage = i18n("Could not open metadata file: %1", src); d->errorCode = Package::JobError::MetadataFileMissingError; return false; } QJsonObject metadataObject = QJsonDocument::fromJson(f.readAll()).object(); meta = KPluginMetaData(metadataObject, QString(), metadataFilePath); } } if (!meta.isValid()) { qCDebug(KPACKAGE_LOG) << "No metadata file in package" << src << path; d->errorMessage = i18n("No metadata file in package: %1", src); d->errorCode = Package::JobError::MetadataFileMissingError; return false; } QString pluginName = meta.pluginId(); qCDebug(KPACKAGE_LOG) << "pluginname: " << meta.pluginId(); if (pluginName.isEmpty()) { //qCWarning(KPACKAGE_LOG) << "Package plugin name not specified"; d->errorMessage = i18n("Package plugin name not specified: %1", src); d->errorCode = Package::JobError::PluginNameMissingError; return false; } // Ensure that package names are safe so package uninstall can't inject // bad characters into the paths used for removal. QRegExp validatePluginName(QStringLiteral("^[\\w-\\.]+$")); // Only allow letters, numbers, underscore and period. if (!validatePluginName.exactMatch(pluginName)) { //qCDebug(KPACKAGE_LOG) << "Package plugin name " << pluginName << "contains invalid characters"; d->errorMessage = i18n("Package plugin name %1 contains invalid characters", pluginName); d->errorCode = Package::JobError::PluginNameInvalidError; return false; } QString targetName = dest; if (targetName[targetName.size() - 1] != QLatin1Char('/')) { targetName.append(QLatin1Char('/')); } targetName.append(pluginName); if (QFile::exists(targetName)) { if (operation == Update) { KPluginMetaData oldMeta(targetName + QLatin1String("/metadata.desktop")); if (oldMeta.serviceTypes() != meta.serviceTypes()) { - d->errorMessage = i18n("The new package has a different type from the old version already installed.", meta.version(), meta.pluginId(), oldMeta.version()); + d->errorMessage = i18n("The new package has a different type from the old version already installed."); d->errorCode = Package::JobError::UpdatePackageTypeMismatchError; } else if (isVersionNewer(oldMeta.version(), meta.version())) { const bool ok = uninstallPackage(targetName); if (!ok) { d->errorMessage = i18n("Impossible to remove the old installation of %1 located at %2. error: %3", pluginName, targetName, d->errorMessage); d->errorCode = Package::JobError::OldVersionRemovalError; } } else { d->errorMessage = i18n("Not installing version %1 of %2. Version %3 already installed.", meta.version(), meta.pluginId(), oldMeta.version()); d->errorCode = Package::JobError::NewerVersionAlreadyInstalledError; } } else { d->errorMessage = i18n("%1 already exists", targetName); d->errorCode = Package::JobError::PackageAlreadyInstalledError; } if (d->errorCode != KJob::NoError) { d->installPath = targetName; return false; } } //install dependencies const QStringList dependencies = KPluginMetaData::readStringList(meta.rawData(), QStringLiteral("X-KPackage-Dependencies")); for(const QString &dep : dependencies) { QUrl depUrl(dep); if (!installDependency(depUrl)) { d->errorMessage = i18n("Could not install dependency: '%1'", dep); d->errorCode = Package::JobError::PackageCopyError; return false; } } if (archivedPackage) { // it's in a temp dir, so just move it over. const bool ok = copyFolder(path, targetName); removeFolder(path); if (!ok) { //qCWarning(KPACKAGE_LOG) << "Could not move package to destination:" << targetName; d->errorMessage = i18n("Could not move package to destination: %1", targetName); d->errorCode = Package::JobError::PackageMoveError; return false; } } else { // it's a directory containing the stuff, so copy the contents rather // than move them const bool ok = copyFolder(path, targetName); if (!ok) { //qCWarning(KPACKAGE_LOG) << "Could not copy package to destination:" << targetName; d->errorMessage = i18n("Could not copy package to destination: %1", targetName); d->errorCode = Package::JobError::PackageCopyError; return false; } } if (archivedPackage) { // no need to remove the temp dir (which has been successfully moved if it's an archive) tempdir.setAutoRemove(false); } indexDirectory(dest, QStringLiteral("kpluginindex.json")); d->installPath = targetName; return true; } bool PackageJobThread::update(const QString &src, const QString &dest) { bool ok = installPackage(src, dest, Update); emit installPathChanged(d->installPath); emit finished(ok, d->errorMessage); return ok; } bool PackageJobThread::uninstall(const QString &packagePath) { bool ok = uninstallPackage(packagePath); //qCDebug(KPACKAGE_LOG) << "emit installPathChanged " << d->installPath; emit installPathChanged(QString()); //qCDebug(KPACKAGE_LOG) << "Thread: installFinished" << ok; emit finished(ok, d->errorMessage); return ok; } bool PackageJobThread::uninstallPackage(const QString &packagePath) { if (!QFile::exists(packagePath)) { d->errorMessage = i18n("%1 does not exist", packagePath); d->errorCode = Package::JobError::PackageFileNotFoundError; return false; } QString pkg; QString root; { // FIXME: remove, pass in packageroot, type and pluginName separately? QStringList ps = packagePath.split(QLatin1Char('/')); int ix = ps.count() - 1; if (packagePath.endsWith(QLatin1Char('/'))) { ix = ps.count() - 2; } pkg = ps[ix]; ps.pop_back(); root = ps.join(QLatin1Char('/')); } bool ok = removeFolder(packagePath); if (!ok) { d->errorMessage = i18n("Could not delete package from: %1", packagePath); d->errorCode = Package::JobError::PackageUninstallError; return false; } indexDirectory(root, QStringLiteral("kpluginindex.json")); return true; } Package::JobError PackageJobThread::errorCode() const { return static_cast(d->errorCode); } } // namespace KPackage #include "moc_packagejobthread_p.cpp" diff --git a/src/kpackage/private/packagejobthread_p.h b/src/kpackage/private/packagejobthread_p.h index d6010f8..61346a9 100644 --- a/src/kpackage/private/packagejobthread_p.h +++ b/src/kpackage/private/packagejobthread_p.h @@ -1,78 +1,78 @@ /****************************************************************************** * Copyright 2007-2009 by Aaron Seigo * * Copyright 2012 Sebastian Kügler * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public License * * along with this library; see the file COPYING.LIB. If not, write to * * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301, USA. * *******************************************************************************/ #ifndef KPACKAGE_PACKAGEJOBTHREAD_P_H #define KPACKAGE_PACKAGEJOBTHREAD_P_H #include "kjob.h" #include "package.h" #include const static auto s_kpluginindex = QStringLiteral("kpluginindex.json"); namespace KPackage { class PackageJobThreadPrivate; bool indexDirectory(const QString& dir, const QString& dest); //true if version2 is more recent than version1 //TODO: replace with QVersionNumber when we will be able to depend from Qt 5.6 bool isVersionNewer(const QString &version1, const QString &version2); class PackageJobThread : public QThread { Q_OBJECT public: enum OperationType { Install, Update }; explicit PackageJobThread(QObject *parent = nullptr); virtual ~PackageJobThread(); bool install(const QString &src, const QString &dest); bool update(const QString &src, const QString &dest); bool uninstall(const QString &packagePath); Package::JobError errorCode() const; Q_SIGNALS: void finished(bool success, const QString &errorMessage = QString()); void percentChanged(int percent); void error(const QString &errorMessage); void installPathChanged(const QString &installPath); private: - //OperationType says wether we want to install, update or any + //OperationType says whether we want to install, update or any //new similar operation it will be expanded bool installDependency(const QUrl &src); bool installPackage(const QString &src, const QString &dest, OperationType operation); bool uninstallPackage(const QString &packagePath); PackageJobThreadPrivate *d; }; } #endif