File indexing completed on 2024-04-14 03:54:17

0001 /*
0002     SPDX-FileCopyrightText: 2008 Aaron Seigo <aseigo@kde.org>
0003     SPDX-FileCopyrightText: 2012-2017 Sebastian Kügler <sebas@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "kpackagetool.h"
0009 
0010 #include <KAboutData>
0011 #include <KLocalizedString>
0012 #include <KShell>
0013 #include <QDebug>
0014 
0015 #include <KJob>
0016 #include <kpackage/package.h>
0017 #include <kpackage/packageloader.h>
0018 #include <kpackage/packagestructure.h>
0019 #include <kpackage/private/utils.h>
0020 
0021 #include <QCommandLineParser>
0022 #include <QDir>
0023 #include <QFileInfo>
0024 #include <QList>
0025 #include <QMap>
0026 #include <QRegularExpression>
0027 #include <QStandardPaths>
0028 #include <QStringList>
0029 #include <QTimer>
0030 #include <QUrl>
0031 #include <QXmlStreamWriter>
0032 
0033 #include <iomanip>
0034 #include <iostream>
0035 
0036 #include "options.h"
0037 
0038 #include "../kpackage/config-package.h"
0039 
0040 #include "kpackage_debug.h"
0041 
0042 Q_GLOBAL_STATIC_WITH_ARGS(QTextStream, cout, (stdout))
0043 Q_GLOBAL_STATIC_WITH_ARGS(QTextStream, cerr, (stderr))
0044 
0045 namespace KPackage
0046 {
0047 class PackageToolPrivate
0048 {
0049 public:
0050     QString packageRoot;
0051     QString packageFile;
0052     QString package;
0053     QString kpackageType = QStringLiteral("KPackage/Generic");
0054     KPluginMetaData metadata;
0055     QString installPath;
0056     void output(const QString &msg);
0057     QStringList packages(const QString &type, const QString &path = QString());
0058     void renderTypeTable(const QMap<QString, QString> &plugins);
0059     void listTypes();
0060     void coutput(const QString &msg);
0061     void cerror(const QString &msg);
0062     QCommandLineParser *parser = nullptr;
0063 };
0064 
0065 PackageTool::PackageTool(int &argc, char **argv, QCommandLineParser *parser)
0066     : QCoreApplication(argc, argv)
0067 {
0068     d = new PackageToolPrivate;
0069     d->parser = parser;
0070     QTimer::singleShot(0, this, &PackageTool::runMain);
0071 }
0072 
0073 PackageTool::~PackageTool()
0074 {
0075     delete d;
0076 }
0077 
0078 void PackageTool::runMain()
0079 {
0080     if (d->parser->isSet(Options::hash())) {
0081         const QString path = d->parser->value(Options::hash());
0082         KPackage::PackageStructure structure;
0083         KPackage::Package package(&structure);
0084         package.setPath(path);
0085         const QString hash = QString::fromLocal8Bit(package.cryptographicHash(QCryptographicHash::Sha1));
0086         if (hash.isEmpty()) {
0087             d->coutput(i18n("Failed to generate a Package hash for %1", path));
0088             exit(9);
0089         } else {
0090             d->coutput(i18n("SHA1 hash for Package at %1: '%2'", package.path(), hash));
0091             exit(0);
0092         }
0093         return;
0094     }
0095 
0096     if (d->parser->isSet(Options::listTypes())) {
0097         d->listTypes();
0098         exit(0);
0099         return;
0100     }
0101 
0102     if (d->parser->isSet(Options::type())) {
0103         d->kpackageType = d->parser->value(Options::type());
0104     }
0105     d->packageRoot = KPackage::PackageLoader::self()->loadPackage(d->kpackageType).defaultPackageRoot();
0106 
0107     if (d->parser->isSet(Options::remove())) {
0108         d->package = d->parser->value(Options::remove());
0109     } else if (d->parser->isSet(Options::upgrade())) {
0110         d->package = d->parser->value(Options::upgrade());
0111     } else if (d->parser->isSet(Options::install())) {
0112         d->package = d->parser->value(Options::install());
0113     } else if (d->parser->isSet(Options::show())) {
0114         d->package = d->parser->value(Options::show());
0115     } else if (d->parser->isSet(Options::appstream())) {
0116         d->package = d->parser->value(Options::appstream());
0117     }
0118 
0119     if (!QDir::isAbsolutePath(d->package)) {
0120         d->packageFile = QDir(QDir::currentPath() + QLatin1Char('/') + d->package).absolutePath();
0121         d->packageFile = QFileInfo(d->packageFile).canonicalFilePath();
0122         if (d->parser->isSet(Options::upgrade())) {
0123             d->package = d->packageFile;
0124         }
0125     } else {
0126         d->packageFile = d->package;
0127     }
0128 
0129     if (!PackageLoader::self()->loadPackageStructure(d->kpackageType)) {
0130         qWarning() << "Package type" << d->kpackageType << "not found";
0131     }
0132 
0133     if (d->parser->isSet(Options::show())) {
0134         const QString pluginName = d->package;
0135         showPackageInfo(pluginName);
0136         return;
0137     } else if (d->parser->isSet(Options::appstream())) {
0138         const QString pluginName = d->package;
0139         showAppstreamInfo(pluginName);
0140         return;
0141     }
0142 
0143     if (d->parser->isSet(Options::list())) {
0144         QString packageRoot = resolvePackageRootWithOptions();
0145         d->coutput(i18n("Listing KPackageType: %1 in %2", d->kpackageType, packageRoot));
0146         listPackages(d->kpackageType, packageRoot);
0147         exit(0);
0148     } else {
0149         // install, remove or upgrade
0150         d->packageRoot = resolvePackageRootWithOptions();
0151 
0152         if (d->parser->isSet(Options::remove()) || d->parser->isSet(Options::upgrade())) {
0153             QString pkgPath;
0154             KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(d->kpackageType);
0155             pkg.setPath(d->package);
0156             if (pkg.isValid()) {
0157                 pkgPath = pkg.path();
0158                 if (pkgPath.isEmpty() && !d->packageFile.isEmpty()) {
0159                     pkgPath = d->packageFile;
0160                 }
0161             }
0162             if (pkgPath.isEmpty()) {
0163                 pkgPath = d->package;
0164             }
0165             QString _p = d->packageRoot;
0166             if (!_p.endsWith(QLatin1Char('/'))) {
0167                 _p.append(QLatin1Char('/'));
0168             }
0169             _p.append(d->package);
0170 
0171             if (!d->parser->isSet(Options::type())) {
0172                 d->kpackageType = readKPackageType(pkg.metadata());
0173             }
0174 
0175             QString pluginName;
0176             if (pkg.metadata().isValid()) {
0177                 d->metadata = pkg.metadata();
0178                 if (!d->metadata.isValid()) {
0179                     pluginName = d->package;
0180                 } else if (!d->metadata.isValid() && d->metadata.pluginId().isEmpty()) {
0181                     // plugin id given in command line
0182                     pluginName = d->package;
0183                 } else {
0184                     // Parameter was a plasma package, get plugin id from the package
0185                     pluginName = d->metadata.pluginId();
0186                 }
0187             }
0188             QStringList installed = d->packages(d->kpackageType);
0189 
0190             // Uninstalling ...
0191             if (installed.contains(pluginName)) { // Assume it's a plugin id
0192                 KPackage::PackageJob *uninstallJob = KPackage::PackageJob::uninstall(d->kpackageType, pluginName, d->packageRoot);
0193                 connect(uninstallJob, &KPackage::PackageJob::finished, this, [uninstallJob, this]() {
0194                     packageUninstalled(uninstallJob);
0195                 });
0196                 return;
0197             } else {
0198                 d->coutput(i18n("Error: Plugin %1 is not installed.", pluginName));
0199                 exit(2);
0200             }
0201         }
0202         if (d->parser->isSet(Options::install())) {
0203             auto installJob = KPackage::PackageJob::install(d->kpackageType, d->packageFile, d->packageRoot);
0204             connect(installJob, &KPackage::PackageJob::finished, this, [installJob, this]() {
0205                 packageInstalled(installJob);
0206             });
0207             return;
0208         }
0209         if (d->package.isEmpty()) {
0210             qWarning() << i18nc(
0211                 "No option was given, this is the error message telling the user he needs at least one, do not translate install, remove, upgrade nor list",
0212                 "One of install, remove, upgrade or list is required.");
0213             exit(6);
0214         }
0215     }
0216 }
0217 
0218 void PackageToolPrivate::coutput(const QString &msg)
0219 {
0220     *cout << msg << '\n';
0221     (*cout).flush();
0222 }
0223 
0224 void PackageToolPrivate::cerror(const QString &msg)
0225 {
0226     *cerr << msg << '\n';
0227     (*cerr).flush();
0228 }
0229 
0230 QStringList PackageToolPrivate::packages(const QString &type, const QString &path)
0231 {
0232     QStringList result;
0233     const QList<KPluginMetaData> dataList = KPackage::PackageLoader::self()->listPackages(type, path);
0234     for (const KPluginMetaData &data : dataList) {
0235         if (!result.contains(data.pluginId())) {
0236             result << data.pluginId();
0237         }
0238     }
0239     return result;
0240 }
0241 
0242 void PackageTool::showPackageInfo(const QString &pluginName)
0243 {
0244     KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(d->kpackageType);
0245     pkg.setDefaultPackageRoot(d->packageRoot);
0246 
0247     if (QFile::exists(d->packageFile)) {
0248         pkg.setPath(d->packageFile);
0249     } else {
0250         pkg.setPath(pluginName);
0251     }
0252 
0253     KPluginMetaData i = pkg.metadata();
0254     if (!i.isValid()) {
0255         *cerr << i18n("Error: Can't find plugin metadata: %1\n", pluginName);
0256         exit(3);
0257         return;
0258     }
0259     d->coutput(i18n("Showing info for package: %1", pluginName));
0260     d->coutput(i18n("  Name       : %1", i.name()));
0261     d->coutput(i18n("  Description: %1", i.description()));
0262     d->coutput(i18n("  Plugin     : %1", i.pluginId()));
0263     auto const authors = i.authors();
0264     QStringList authorNames;
0265     for (const KAboutPerson &author : authors) {
0266         authorNames << author.name();
0267     }
0268     d->coutput(i18n("  Author     : %1", authorNames.join(QLatin1String(", "))));
0269     d->coutput(i18n("  Path       : %1", pkg.path()));
0270 
0271     exit(0);
0272 }
0273 
0274 bool translateKPluginToAppstream(const QString &tagName,
0275                                  const QString &configField,
0276                                  const QJsonObject &configObject,
0277                                  QXmlStreamWriter &writer,
0278                                  bool canEndWithDot)
0279 {
0280     const QRegularExpression rx(QStringLiteral("%1\\[(.*)\\]").arg(configField));
0281     const QJsonValue native = configObject.value(configField);
0282     if (native.isUndefined()) {
0283         return false;
0284     }
0285 
0286     QString content = native.toString();
0287     if (!canEndWithDot && content.endsWith(QLatin1Char('.'))) {
0288         content.chop(1);
0289     }
0290     writer.writeTextElement(tagName, content);
0291     for (auto it = configObject.begin(), itEnd = configObject.end(); it != itEnd; ++it) {
0292         const auto match = rx.match(it.key());
0293 
0294         if (match.hasMatch()) {
0295             QString content = it->toString();
0296             if (!canEndWithDot && content.endsWith(QLatin1Char('.'))) {
0297                 content.chop(1);
0298             }
0299 
0300             writer.writeStartElement(tagName);
0301             writer.writeAttribute(QStringLiteral("xml:lang"), match.captured(1));
0302             writer.writeCharacters(content);
0303             writer.writeEndElement();
0304         }
0305     }
0306     return true;
0307 }
0308 
0309 void PackageTool::showAppstreamInfo(const QString &pluginName)
0310 {
0311     KPluginMetaData i;
0312     // if the path passed is an absolute path, and a metadata file is found under it, use that metadata file to generate the appstream info.
0313     // This can happen in the case an application wanting to support kpackage based extensions includes in the same project both the packagestructure plugin and
0314     // the packages themselves. In that case at build time the packagestructure plugin wouldn't be installed yet
0315 
0316     if (QFile::exists(pluginName + QStringLiteral("/metadata.json"))) {
0317         i = KPluginMetaData::fromJsonFile(pluginName + QStringLiteral("/metadata.json"));
0318     } else {
0319         KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(d->kpackageType);
0320 
0321         pkg.setDefaultPackageRoot(d->packageRoot);
0322 
0323         if (QFile::exists(d->packageFile)) {
0324             pkg.setPath(d->packageFile);
0325         } else {
0326             pkg.setPath(pluginName);
0327         }
0328 
0329         i = pkg.metadata();
0330     }
0331 
0332     if (!i.isValid()) {
0333         *cerr << i18n("Error: Can't find plugin metadata: %1\n", pluginName);
0334         std::exit(3);
0335         return;
0336     }
0337     QString parentApp = i.value(QLatin1String("X-KDE-ParentApp"));
0338 
0339     if (i.value(QStringLiteral("NoDisplay"), false)) {
0340         std::exit(0);
0341     }
0342 
0343     QXmlStreamAttributes componentAttributes;
0344     if (!parentApp.isEmpty()) {
0345         componentAttributes << QXmlStreamAttribute(QLatin1String("type"), QLatin1String("addon"));
0346     }
0347 
0348     // Compatibility: without appstream-metainfo-output argument we print the XML output to STDOUT
0349     // with the argument we'll print to the defined path.
0350     // TODO: in KF6 we should switch to argument-only.
0351     QIODevice *outputDevice = cout->device();
0352     std::unique_ptr<QFile> outputFile;
0353     const auto outputPath = d->parser->value(Options::appstreamOutput());
0354     if (!outputPath.isEmpty()) {
0355         auto outputUrl = QUrl::fromUserInput(outputPath);
0356         outputFile.reset(new QFile(outputUrl.toLocalFile()));
0357         if (!outputFile->open(QFile::WriteOnly | QFile::Text)) {
0358             *cerr << "Failed to open output file for writing.";
0359             exit(1);
0360         }
0361         outputDevice = outputFile.get();
0362     }
0363 
0364     if (i.description().isEmpty()) {
0365         *cerr << "Error: description missing, will result in broken appdata field as <summary/> is mandatory at " << QFileInfo(i.fileName()).absoluteFilePath();
0366         std::exit(10);
0367     }
0368 
0369     QXmlStreamWriter writer(outputDevice);
0370     writer.setAutoFormatting(true);
0371     writer.writeStartDocument();
0372     writer.writeStartElement(QStringLiteral("component"));
0373     writer.writeAttributes(componentAttributes);
0374 
0375     writer.writeTextElement(QStringLiteral("id"), i.pluginId());
0376     if (!parentApp.isEmpty()) {
0377         writer.writeTextElement(QStringLiteral("extends"), parentApp);
0378     }
0379 
0380     const QJsonObject rootObject = i.rawData()[QStringLiteral("KPlugin")].toObject();
0381     translateKPluginToAppstream(QStringLiteral("name"), QStringLiteral("Name"), rootObject, writer, false);
0382     translateKPluginToAppstream(QStringLiteral("summary"), QStringLiteral("Description"), rootObject, writer, false);
0383     if (!i.website().isEmpty()) {
0384         writer.writeStartElement(QStringLiteral("url"));
0385         writer.writeAttribute(QStringLiteral("type"), QStringLiteral("homepage"));
0386         writer.writeCharacters(i.website());
0387         writer.writeEndElement();
0388     }
0389 
0390     if (i.pluginId().startsWith(QLatin1String("org.kde."))) {
0391         writer.writeStartElement(QStringLiteral("url"));
0392         writer.writeAttribute(QStringLiteral("type"), QStringLiteral("donation"));
0393         writer.writeCharacters(QStringLiteral("https://www.kde.org/donate.php?app=%1").arg(i.pluginId()));
0394         writer.writeEndElement();
0395     }
0396 
0397     const auto authors = i.authors();
0398     if (!authors.isEmpty()) {
0399         QStringList authorsText;
0400         authorsText.reserve(authors.size());
0401         for (const auto &author : authors) {
0402             authorsText += QStringLiteral("%1").arg(author.name());
0403         }
0404         writer.writeStartElement(QStringLiteral("developer"));
0405         writer.writeAttribute(QStringLiteral("id"), QStringLiteral("kde.org"));
0406         writer.writeTextElement(QStringLiteral("name"), authorsText.join(QStringLiteral(", ")));
0407         writer.writeEndElement();
0408     }
0409 
0410     if (!i.iconName().isEmpty()) {
0411         writer.writeStartElement(QStringLiteral("icon"));
0412         writer.writeAttribute(QStringLiteral("type"), QStringLiteral("stock"));
0413         writer.writeCharacters(i.iconName());
0414         writer.writeEndElement();
0415     }
0416     writer.writeTextElement(QStringLiteral("project_license"), KAboutLicense::byKeyword(i.license()).spdx());
0417     writer.writeTextElement(QStringLiteral("metadata_license"), QStringLiteral("CC0-1.0"));
0418     writer.writeEndElement();
0419     writer.writeEndDocument();
0420 
0421     exit(0);
0422 }
0423 
0424 QString PackageTool::resolvePackageRootWithOptions()
0425 {
0426     QString packageRoot;
0427     if (d->parser->isSet(Options::packageRoot()) && d->parser->isSet(Options::global())) {
0428         qWarning() << i18nc("The user entered conflicting options packageroot and global, this is the error message telling the user he can use only one",
0429                             "The packageroot and global options conflict with each other, please select only one.");
0430         ::exit(7);
0431     } else if (d->parser->isSet(Options::packageRoot())) {
0432         packageRoot = d->parser->value(Options::packageRoot());
0433         // qDebug() << "(set via arg) d->packageRoot is: " << d->packageRoot;
0434     } else if (d->parser->isSet(Options::global())) {
0435         auto const paths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, d->packageRoot, QStandardPaths::LocateDirectory);
0436         if (!paths.isEmpty()) {
0437             packageRoot = paths.last();
0438         }
0439     } else {
0440         packageRoot = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + d->packageRoot;
0441     }
0442     return packageRoot;
0443 }
0444 
0445 void PackageTool::listPackages(const QString &kpackageType, const QString &path)
0446 {
0447     QStringList list = d->packages(kpackageType, path);
0448     list.sort();
0449     for (const QString &package : std::as_const(list)) {
0450         d->coutput(package);
0451     }
0452     exit(0);
0453 }
0454 
0455 void PackageToolPrivate::renderTypeTable(const QMap<QString, QString> &plugins)
0456 {
0457     const QString nameHeader = i18n("KPackage Structure Name");
0458     const QString pathHeader = i18n("Path");
0459     int nameWidth = nameHeader.length();
0460     int pathWidth = pathHeader.length();
0461 
0462     QMapIterator<QString, QString> pluginIt(plugins);
0463     while (pluginIt.hasNext()) {
0464         pluginIt.next();
0465         if (pluginIt.key().length() > nameWidth) {
0466             nameWidth = pluginIt.key().length();
0467         }
0468 
0469         if (pluginIt.value().length() > pathWidth) {
0470             pathWidth = pluginIt.value().length();
0471         }
0472     }
0473 
0474     std::cout << nameHeader.toLocal8Bit().constData() << std::setw(nameWidth - nameHeader.length() + 2) << ' ' << pathHeader.toLocal8Bit().constData()
0475               << std::setw(pathWidth - pathHeader.length() + 2) << ' ' << std::endl;
0476     std::cout << std::setfill('-') << std::setw(nameWidth) << '-' << "  " << std::setw(pathWidth) << '-' << "  " << std::endl;
0477     std::cout << std::setfill(' ');
0478 
0479     pluginIt.toFront();
0480     while (pluginIt.hasNext()) {
0481         pluginIt.next();
0482         std::cout << pluginIt.key().toLocal8Bit().constData() << std::setw(nameWidth - pluginIt.key().length() + 2) << ' '
0483                   << pluginIt.value().toLocal8Bit().constData() << std::setw(pathWidth - pluginIt.value().length() + 2) << std::endl;
0484     }
0485 }
0486 
0487 void PackageToolPrivate::listTypes()
0488 {
0489     coutput(i18n("Package types that are installable with this tool:"));
0490     coutput(i18n("Built in:"));
0491 
0492     QMap<QString, QString> builtIns;
0493     builtIns.insert(i18n("KPackage/Generic"), QStringLiteral(KPACKAGE_RELATIVE_DATA_INSTALL_DIR "/packages/"));
0494     builtIns.insert(i18n("KPackage/GenericQML"), QStringLiteral(KPACKAGE_RELATIVE_DATA_INSTALL_DIR "/genericqml/"));
0495 
0496     renderTypeTable(builtIns);
0497 
0498     const QList<KPluginMetaData> offers = KPluginMetaData::findPlugins(QStringLiteral("kf6/packagestructure"));
0499 
0500     if (!offers.isEmpty()) {
0501         std::cout << std::endl;
0502         coutput(i18n("Provided by plugins:"));
0503 
0504         QMap<QString, QString> plugins;
0505         for (const KPluginMetaData &info : offers) {
0506             const QString type = readKPackageType(info);
0507             if (type.isEmpty()) {
0508                 continue;
0509             }
0510             KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(type);
0511             plugins.insert(type, pkg.defaultPackageRoot());
0512         }
0513 
0514         renderTypeTable(plugins);
0515     }
0516 }
0517 
0518 void PackageTool::packageInstalled(KPackage::PackageJob *job)
0519 {
0520     bool success = (job->error() == KJob::NoError);
0521     int exitcode = 0;
0522     if (success) {
0523         if (d->parser->isSet(Options::upgrade())) {
0524             d->coutput(i18n("Successfully upgraded %1", job->package().path()));
0525         } else {
0526             d->coutput(i18n("Successfully installed %1", job->package().path()));
0527         }
0528     } else {
0529         d->cerror(i18n("Error: Installation of %1 failed: %2", d->packageFile, job->errorText()));
0530         exitcode = 4;
0531     }
0532     exit(exitcode);
0533 }
0534 
0535 void PackageTool::packageUninstalled(KPackage::PackageJob *job)
0536 {
0537     bool success = (job->error() == KJob::NoError);
0538     int exitcode = 0;
0539     if (success) {
0540         if (d->parser->isSet(Options::upgrade())) {
0541             d->coutput(i18n("Upgrading package from file: %1", d->packageFile));
0542             auto installJob = KPackage::PackageJob::install(d->kpackageType, d->packageFile, d->packageRoot);
0543             connect(installJob, &KPackage::PackageJob::finished, this, [installJob, this]() {
0544                 packageInstalled(installJob);
0545             });
0546             return;
0547         }
0548         d->coutput(i18n("Successfully uninstalled %1", job->package().path()));
0549     } else {
0550         d->cerror(i18n("Error: Uninstallation of %1 failed: %2", d->packageFile, job->errorText()));
0551         exitcode = 7;
0552     }
0553     exit(exitcode);
0554 }
0555 
0556 } // namespace KPackage
0557 
0558 #include "moc_kpackagetool.cpp"