File indexing completed on 2024-05-12 04:37:48

0001 /*
0002     SPDX-FileCopyrightText: 2007 Alexander Dymo <adymo@kdevelop.org>
0003     SPDX-FileCopyrightText: 2012 Miha Čančula <miha@noughmad.eu>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "templatesmodel.h"
0009 
0010 #include "templatepreviewicon.h"
0011 #include <debug.h>
0012 #include <interfaces/icore.h>
0013 
0014 #include <KConfig>
0015 #include <KTar>
0016 #include <KZip>
0017 #include <KConfigGroup>
0018 #include <KLocalizedString>
0019 
0020 #include <QMimeType>
0021 #include <QMimeDatabase>
0022 #include <QFileInfo>
0023 #include <QDir>
0024 #include <QStandardPaths>
0025 #include <QTemporaryDir>
0026 
0027 using namespace KDevelop;
0028 
0029 class KDevelop::TemplatesModelPrivate
0030 {
0031 public:
0032     explicit TemplatesModelPrivate(const QString& typePrefix);
0033 
0034     QString typePrefix;
0035 
0036     QStringList searchPaths;
0037 
0038     QMap<QString, QStandardItem*> templateItems;
0039 
0040     /**
0041      * Extracts description files from all available template archives and saves them to a location
0042      * determined by descriptionResourceSuffix().
0043      **/
0044     void extractTemplateDescriptions();
0045 
0046     /**
0047      * Creates a model item for the template @p name in category @p category
0048      *
0049      * @param name the name of the new template
0050      * @param category the category of the new template
0051      * @param parent the parent item
0052      * @return the created item
0053      **/
0054     QStandardItem* createItem(const QString& name, const QString& category, QStandardItem* parent);
0055 
0056     enum ResourceType
0057     {
0058         Description,
0059         Template,
0060         Preview
0061     };
0062     QString resourceFilter(ResourceType type, const QString& suffix = QString()) const
0063     {
0064         QString filter = typePrefix;
0065         switch (type) {
0066         case Description:
0067             filter += QLatin1String("template_descriptions/");
0068             break;
0069         case Template:
0070             filter += QLatin1String("templates/");
0071             break;
0072         case Preview:
0073             filter += QLatin1String("template_previews/");
0074             break;
0075         }
0076         return filter + suffix;
0077     }
0078 };
0079 
0080 TemplatesModelPrivate::TemplatesModelPrivate(const QString& _typePrefix)
0081     : typePrefix(_typePrefix)
0082 {
0083     if (!typePrefix.endsWith(QLatin1Char('/'))) {
0084         typePrefix.append(QLatin1Char('/'));
0085     }
0086 }
0087 
0088 TemplatesModel::TemplatesModel(const QString& typePrefix, QObject* parent)
0089     : QStandardItemModel(parent)
0090     , d_ptr(new TemplatesModelPrivate(typePrefix))
0091 {
0092 }
0093 
0094 TemplatesModel::~TemplatesModel() = default;
0095 
0096 void TemplatesModel::refresh()
0097 {
0098     Q_D(TemplatesModel);
0099 
0100     clear();
0101     d->templateItems.clear();
0102     d->templateItems[QString()] = invisibleRootItem();
0103     d->extractTemplateDescriptions();
0104 
0105     QStringList templateArchives;
0106     for (const QString& archivePath : qAsConst(d->searchPaths)) {
0107         const QStringList files = QDir(archivePath).entryList(QDir::Files);
0108         for (const QString& file : files) {
0109             templateArchives.append(archivePath + file);
0110         }
0111     }
0112 
0113     QStringList templateDescriptions;
0114     const QStringList templatePaths =
0115     {QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + d->resourceFilter(
0116          TemplatesModelPrivate::Description)};
0117     for (const QString& templateDescription : templatePaths) {
0118         const QStringList files = QDir(templateDescription).entryList(QDir::Files);
0119         for (const QString& file : files) {
0120             templateDescriptions.append(templateDescription + file);
0121         }
0122     }
0123 
0124     for (const QString& templateDescription : qAsConst(templateDescriptions)) {
0125         QFileInfo fi(templateDescription);
0126         bool archiveFound = false;
0127         for (const QString& templateArchive : qAsConst(templateArchives)) {
0128             if (QFileInfo(templateArchive).baseName() == fi.baseName()) {
0129                 archiveFound = true;
0130 
0131                 KConfig templateConfig(templateDescription);
0132                 KConfigGroup general(&templateConfig, "General");
0133                 QString name = general.readEntry("Name");
0134                 QString category = general.readEntry("Category");
0135                 QString comment = general.readEntry("Comment");
0136                 TemplatePreviewIcon icon(general.readEntry("Icon"), templateArchive, d->resourceFilter(
0137                         TemplatesModelPrivate::Preview));
0138 
0139                 QStandardItem* templateItem = d->createItem(name, category, invisibleRootItem());
0140                 templateItem->setData(templateDescription, DescriptionFileRole);
0141                 templateItem->setData(templateArchive, ArchiveFileRole);
0142                 templateItem->setData(comment, CommentRole);
0143                 templateItem->setData(QVariant::fromValue<TemplatePreviewIcon>(icon), PreviewIconRole);
0144             }
0145         }
0146 
0147         if (!archiveFound) {
0148             // Template file doesn't exist anymore, so remove the description
0149             // saves us the extra lookups for templateExists on the next run
0150             QFile(templateDescription).remove();
0151         }
0152     }
0153 }
0154 
0155 QStandardItem* TemplatesModelPrivate::createItem(const QString& name, const QString& category, QStandardItem* parent)
0156 {
0157     const QStringList path = category.split(QLatin1Char('/'));
0158 
0159     QStringList currentPath;
0160     currentPath.reserve(path.size());
0161     for (const QString& entry : path) {
0162         currentPath << entry;
0163         if (!templateItems.contains(currentPath.join(QLatin1Char('/')))) {
0164             auto* item = new QStandardItem(entry);
0165             item->setEditable(false);
0166             parent->appendRow(item);
0167             templateItems[currentPath.join(QLatin1Char('/'))] = item;
0168             parent = item;
0169         } else {
0170             parent = templateItems[currentPath.join(QLatin1Char('/'))];
0171         }
0172     }
0173 
0174     auto* templateItem = new QStandardItem(name);
0175     templateItem->setEditable(false);
0176     parent->appendRow(templateItem);
0177     return templateItem;
0178 }
0179 
0180 void TemplatesModelPrivate::extractTemplateDescriptions()
0181 {
0182     QStringList templateArchives;
0183     searchPaths << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, resourceFilter(
0184                                                  Template), QStandardPaths::LocateDirectory);
0185     searchPaths.removeDuplicates();
0186     for (const QString& archivePath : qAsConst(searchPaths)) {
0187         const QStringList files = QDir(archivePath).entryList(QDir::Files);
0188         for (const QString& file : files) {
0189             if (file.endsWith(QLatin1String(".zip")) || file.endsWith(QLatin1String(".tar.bz2"))) {
0190                 QString archfile = archivePath + file;
0191                 templateArchives.append(archfile);
0192             }
0193         }
0194     }
0195 
0196     QString localDescriptionsDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char(
0197         '/') + resourceFilter(Description);
0198 
0199     QDir dir(localDescriptionsDir);
0200     if (!dir.exists())
0201         dir.mkpath(QStringLiteral("."));
0202 
0203     for (const QString& archName : qAsConst(templateArchives)) {
0204         qCDebug(LANGUAGE) << "processing template" << archName;
0205 
0206         QScopedPointer<KArchive> templateArchive;
0207         if (QFileInfo(archName).completeSuffix() == QLatin1String("zip")) {
0208             templateArchive.reset(new KZip(archName));
0209         } else
0210         {
0211             templateArchive.reset(new KTar(archName));
0212         }
0213 
0214         if (templateArchive->open(QIODevice::ReadOnly)) {
0215             /*
0216              * This class looks for template description files in the following order
0217              *
0218              * - "basename.kdevtemplate"
0219              * - "*.kdevtemplate"
0220              * - "basename.desktop"
0221              * - "*.desktop"
0222              *
0223              * This is done because application templates can contain .desktop files used by the application
0224              * so the kdevtemplate suffix must have priority.
0225              */
0226             QFileInfo templateInfo(archName);
0227             QString suffix = QStringLiteral(".kdevtemplate");
0228             const KArchiveEntry* templateEntry =
0229                 templateArchive->directory()->entry(templateInfo.baseName() + suffix);
0230 
0231             if (!templateEntry || !templateEntry->isFile()) {
0232                 /*
0233                  * First, if the .kdevtemplate file is not found by name,
0234                  * we check all the files in the archive for any .kdevtemplate file
0235                  *
0236                  * This is needed because kde-files.org renames downloaded files
0237                  */
0238                 const auto dirEntries = templateArchive->directory()->entries();
0239                 for (const QString& entryName : dirEntries) {
0240                     if (entryName.endsWith(suffix)) {
0241                         templateEntry = templateArchive->directory()->entry(entryName);
0242                         break;
0243                     }
0244                 }
0245             }
0246 
0247             if (!templateEntry || !templateEntry->isFile()) {
0248                 suffix = QStringLiteral(".desktop");
0249                 templateEntry = templateArchive->directory()->entry(templateInfo.baseName() + suffix);
0250             }
0251 
0252             if (!templateEntry || !templateEntry->isFile()) {
0253                 const auto dirEntries = templateArchive->directory()->entries();
0254                 for (const QString& entryName : dirEntries) {
0255                     if (entryName.endsWith(suffix)) {
0256                         templateEntry = templateArchive->directory()->entry(entryName);
0257                         break;
0258                     }
0259                 }
0260             }
0261             if (!templateEntry || !templateEntry->isFile()) {
0262                 qCDebug(LANGUAGE) << "template" << archName << "does not contain .kdevtemplate or .desktop file";
0263                 continue;
0264             }
0265             const auto* templateFile = static_cast<const KArchiveFile*>(templateEntry);
0266 
0267             qCDebug(LANGUAGE) << "copy template description to" << localDescriptionsDir;
0268             const QString descriptionFileName = templateInfo.baseName() + suffix;
0269             if (templateFile->name() == descriptionFileName) {
0270                 templateFile->copyTo(localDescriptionsDir);
0271             } else {
0272                 // Rename the extracted description
0273                 // so that its basename matches the basename of the template archive
0274                 // Use temporary dir to not overwrite other files with same name
0275                 QTemporaryDir dir;
0276                 templateFile->copyTo(dir.path());
0277                 const QString destinationPath = localDescriptionsDir + descriptionFileName;
0278                 QFile::remove(destinationPath);
0279                 QFile::rename(dir.path() + QLatin1Char('/') + templateFile->name(), destinationPath);
0280             }
0281         } else
0282         {
0283             qCWarning(LANGUAGE) << "could not open template" << archName << ':' << templateArchive->errorString();
0284         }
0285     }
0286 }
0287 
0288 QModelIndexList TemplatesModel::templateIndexes(const QString& fileName) const
0289 {
0290     Q_D(const TemplatesModel);
0291 
0292     QFileInfo info(fileName);
0293     QString description =
0294         QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0295                                d->resourceFilter(TemplatesModelPrivate::Description,
0296                                                  info.baseName() + QLatin1String(".kdevtemplate")));
0297     if (description.isEmpty()) {
0298         description =
0299             QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0300                                    d->resourceFilter(TemplatesModelPrivate::Description,
0301                                                      info.baseName() + QLatin1String(".desktop")));
0302     }
0303 
0304     QModelIndexList indexes;
0305 
0306     if (!description.isEmpty()) {
0307         KConfig templateConfig(description);
0308         KConfigGroup general(&templateConfig, "General");
0309         const QStringList categories = general.readEntry("Category").split(QLatin1Char('/'));
0310 
0311         QStringList levels;
0312         levels.reserve(categories.size());
0313         for (const QString& category : categories) {
0314             levels << category;
0315             indexes << d->templateItems[levels.join(QLatin1Char('/'))]->index();
0316         }
0317 
0318         if (!indexes.isEmpty()) {
0319             QString name = general.readEntry("Name");
0320             QStandardItem* categoryItem = d->templateItems[levels.join(QLatin1Char('/'))];
0321             for (int i = 0; i < categoryItem->rowCount(); ++i) {
0322                 QStandardItem* templateItem = categoryItem->child(i);
0323                 if (templateItem->text() == name) {
0324                     indexes << templateItem->index();
0325                     break;
0326                 }
0327             }
0328         }
0329     }
0330 
0331     return indexes;
0332 }
0333 
0334 QString TemplatesModel::typePrefix() const
0335 {
0336     Q_D(const TemplatesModel);
0337 
0338     return d->typePrefix;
0339 }
0340 
0341 void TemplatesModel::addDataPath(const QString& path)
0342 {
0343     Q_D(TemplatesModel);
0344 
0345     QString realpath = path + d->resourceFilter(TemplatesModelPrivate::Template);
0346     d->searchPaths.append(realpath);
0347 }
0348 
0349 QString TemplatesModel::loadTemplateFile(const QString& fileName)
0350 {
0351     Q_D(TemplatesModel);
0352 
0353     QString saveLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') +
0354                            d->resourceFilter(TemplatesModelPrivate::Template);
0355 
0356     QDir dir(saveLocation);
0357     if (!dir.exists())
0358         dir.mkpath(QStringLiteral("."));
0359 
0360     QFileInfo info(fileName);
0361     QString destination = saveLocation + info.baseName();
0362 
0363     QMimeType mimeType = QMimeDatabase().mimeTypeForFile(fileName);
0364     qCDebug(LANGUAGE) << "Loaded file" << fileName << "with type" << mimeType.name();
0365 
0366     if (mimeType.name() == QLatin1String("application/x-desktop")) {
0367         qCDebug(LANGUAGE) << "Loaded desktop file" << info.absoluteFilePath() << ", compressing";
0368 #ifdef Q_WS_WIN
0369         destination += ".zip";
0370         KZip archive(destination);
0371 #else
0372         destination += QLatin1String(".tar.bz2");
0373         KTar archive(destination, QStringLiteral("application/x-bzip"));
0374 #endif //Q_WS_WIN
0375 
0376         archive.open(QIODevice::WriteOnly);
0377 
0378         QDir dir(info.absoluteDir());
0379         const auto dirEntryInfos = dir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
0380         for (const QFileInfo& entry : dirEntryInfos) {
0381             if (entry.isFile()) {
0382                 archive.addLocalFile(entry.absoluteFilePath(), entry.fileName());
0383             } else if (entry.isDir()) {
0384                 archive.addLocalDirectory(entry.absoluteFilePath(), entry.fileName());
0385             }
0386         }
0387 
0388         archive.close();
0389     } else
0390     {
0391         qCDebug(LANGUAGE) << "Copying" << fileName << "to" << saveLocation;
0392         QFile::copy(fileName, saveLocation + info.fileName());
0393     }
0394 
0395     refresh();
0396 
0397     return destination;
0398 }
0399 
0400 #include "moc_templatesmodel.cpp"