File indexing completed on 2024-04-14 15:50:49

0001 /**
0002  * SPDX-FileCopyrightText: (C) 2006 by Sébastien Laoût <slaout@linux62.org>
0003  * SPDX-License-Identifier: GPL-2.0-or-later
0004  */
0005 
0006 #include "archive.h"
0007 
0008 #include <QDebug>
0009 #include <QGuiApplication>
0010 #include <QLocale>
0011 #include <QProgressBar>
0012 #include <QProgressDialog>
0013 #include <QStandardPaths>
0014 #include <QStringList>
0015 #include <QTemporaryDir>
0016 #include <QtCore/QDir>
0017 #include <QtCore/QList>
0018 #include <QtCore/QMap>
0019 #include <QtCore/QString>
0020 #include <QtCore/QStringList>
0021 #include <QtCore/QTextStream>
0022 #include <QtGui/QPainter>
0023 #include <QtGui/QPixmap>
0024 #include <QtXml/QDomDocument>
0025 
0026 #include <KAboutData>
0027 #include <KIconLoader>
0028 #include <KLocalizedString>
0029 #include <KMainWindow> //For Global::MainWindow()
0030 #include <KMessageBox>
0031 #include <KTar>
0032 
0033 #include "backgroundmanager.h"
0034 #include "basketfactory.h"
0035 #include "basketlistview.h"
0036 #include "basketscene.h"
0037 #include "bnpview.h"
0038 #include "common.h"
0039 #include "formatimporter.h"
0040 #include "global.h"
0041 #include "tag.h"
0042 #include "tools.h"
0043 #include "xmlwork.h"
0044 
0045 #include <array>
0046 
0047 void Archive::save(BasketScene *basket, bool withSubBaskets, const QString &destination)
0048 {
0049     QDir dir;
0050     QProgressDialog dialog;
0051     dialog.setWindowTitle(i18n("Save as Basket Archive"));
0052     dialog.setLabelText(i18n("Saving as basket archive. Please wait..."));
0053     dialog.setCancelButton(nullptr);
0054     dialog.setAutoClose(true);
0055 
0056     dialog.setRange(0, /*Preparation:*/ 1 + /*Finishing:*/ 1 + /*Basket:*/ 1 + /*SubBaskets:*/ (withSubBaskets ? Global::bnpView->basketCount(Global::bnpView->listViewItemForBasket(basket)) : 0));
0057     dialog.setValue(0);
0058     dialog.show();
0059 
0060     // Create the temporary folder:
0061     QString tempFolder = Global::savesFolder() + "temp-archive/";
0062     dir.mkdir(tempFolder);
0063 
0064     // Create the temporary archive file:
0065     QString tempDestination = tempFolder + "temp-archive.tar.gz";
0066     KTar tar(tempDestination, "application/x-gzip");
0067     tar.open(QIODevice::WriteOnly);
0068     tar.writeDir(QStringLiteral("baskets"), QString(), QString());
0069 
0070     dialog.setValue(dialog.value() + 1); // Preparation finished
0071 
0072     qDebug() << "Preparation finished out of " << dialog.maximum();
0073 
0074     // Copy the baskets data into the archive:
0075     QStringList backgrounds;
0076     Archive::saveBasketToArchive(basket, withSubBaskets, &tar, backgrounds, tempFolder, &dialog);
0077 
0078     // Create a Small baskets.xml Document:
0079     QString data;
0080     QXmlStreamWriter stream(&data);
0081     XMLWork::setupXmlStream(stream, "basketTree");
0082     Global::bnpView->saveSubHierarchy(Global::bnpView->listViewItemForBasket(basket), stream, withSubBaskets);
0083     stream.writeEndElement();
0084     stream.writeEndDocument();
0085     FileStorage::safelySaveToFile(tempFolder + "baskets.xml", data);
0086     tar.addLocalFile(tempFolder + "baskets.xml", "baskets/baskets.xml");
0087     dir.remove(tempFolder + "baskets.xml");
0088 
0089     // Save a Small tags.xml Document:
0090     QList<Tag *> tags;
0091     listUsedTags(basket, withSubBaskets, tags);
0092     Tag::saveTagsTo(tags, tempFolder + "tags.xml");
0093     tar.addLocalFile(tempFolder + "tags.xml", "tags.xml");
0094     dir.remove(tempFolder + "tags.xml");
0095 
0096     // Save Tag Emblems (in case they are loaded on a computer that do not have those icons):
0097     QString tempIconFile = tempFolder + "icon.png";
0098     for (Tag::List::iterator it = tags.begin(); it != tags.end(); ++it) {
0099         State::List states = (*it)->states();
0100         for (State::List::iterator it2 = states.begin(); it2 != states.end(); ++it2) {
0101             State *state = (*it2);
0102             QPixmap icon = KIconLoader::global()->loadIcon(state->emblem(), KIconLoader::Small, 16, KIconLoader::DefaultState, QStringList(), nullptr, true);
0103             if (!icon.isNull()) {
0104                 icon.save(tempIconFile, "PNG");
0105                 QString iconFileName = state->emblem().replace('/', '_');
0106                 tar.addLocalFile(tempIconFile, "tag-emblems/" + iconFileName);
0107             }
0108         }
0109     }
0110     dir.remove(tempIconFile);
0111 
0112     // Finish Tar.Gz Exportation:
0113     tar.close();
0114 
0115     // Computing the File Preview:
0116     BasketScene *previewBasket = basket; // FIXME: Use the first non-empty basket!
0117     // QPixmap previewPixmap(previewBasket->visibleWidth(), previewBasket->visibleHeight());
0118     QPixmap previewPixmap(previewBasket->width(), previewBasket->height());
0119     QPainter painter(&previewPixmap);
0120     // Save old state, and make the look clean ("smile, you are filmed!"):
0121     NoteSelection *selection = previewBasket->selectedNotes();
0122     previewBasket->unselectAll();
0123     Note *focusedNote = previewBasket->focusedNote();
0124     previewBasket->setFocusedNote(nullptr);
0125     previewBasket->doHoverEffects(nullptr, Note::None);
0126     // Take the screenshot:
0127     previewBasket->render(&painter);
0128     // Go back to the old look:
0129     previewBasket->selectSelection(selection);
0130     previewBasket->setFocusedNote(focusedNote);
0131     previewBasket->doHoverEffects();
0132     // End and save our splandid painting:
0133     painter.end();
0134     QImage previewImage = previewPixmap.toImage();
0135     const int PREVIEW_SIZE = 256;
0136     previewImage = previewImage.scaled(PREVIEW_SIZE, PREVIEW_SIZE, Qt::KeepAspectRatio);
0137     previewImage.save(tempFolder + "preview.png", "PNG");
0138 
0139     // Finally Save to the Real Destination file:
0140     QFile file(destination);
0141     if (file.open(QIODevice::WriteOnly)) {
0142         ulong previewSize = QFile(tempFolder + "preview.png").size();
0143         ulong archiveSize = QFile(tempDestination).size();
0144         QTextStream stream(&file);
0145         stream.setCodec("ISO-8859-1");
0146         stream << "BasKetNP:archive\n"
0147                << "version:0.6.1\n"
0148                //             << "read-compatible:0.6.1\n"
0149                //             << "write-compatible:0.6.1\n"
0150                << "preview*:" << previewSize << "\n";
0151 
0152         stream.flush();
0153         // Copy the Preview File:
0154         const unsigned long BUFFER_SIZE = 1024;
0155         char *buffer = new char[BUFFER_SIZE];
0156         long sizeRead;
0157         QFile previewFile(tempFolder + "preview.png");
0158         if (previewFile.open(QIODevice::ReadOnly)) {
0159             while ((sizeRead = previewFile.read(buffer, BUFFER_SIZE)) > 0)
0160                 file.write(buffer, sizeRead);
0161         }
0162         stream << "archive*:" << archiveSize << "\n";
0163         stream.flush();
0164 
0165         // Copy the Archive File:
0166         QFile archiveFile(tempDestination);
0167         if (archiveFile.open(QIODevice::ReadOnly)) {
0168             while ((sizeRead = archiveFile.read(buffer, BUFFER_SIZE)) > 0)
0169                 file.write(buffer, sizeRead);
0170         }
0171         // Clean Up:
0172         delete[] buffer;
0173         buffer = nullptr;
0174         file.close();
0175     }
0176 
0177     dialog.setValue(dialog.value() + 1); // Finishing finished
0178     qDebug() << "Finishing finished";
0179 
0180     // Clean Up Everything:
0181     dir.remove(tempFolder + "preview.png");
0182     dir.remove(tempDestination);
0183     dir.rmdir(tempFolder);
0184 }
0185 
0186 void Archive::saveBasketToArchive(BasketScene *basket, bool recursive, KTar *tar, QStringList &backgrounds, const QString &tempFolder, QProgressDialog *progress)
0187 {
0188     // Basket need to be loaded for tags exportation.
0189     // We load it NOW so that the progress bar really reflect the state of the exportation:
0190     if (!basket->isLoaded()) {
0191         basket->load();
0192     }
0193 
0194     QDir dir;
0195     // Save basket data:
0196     tar->addLocalDirectory(basket->fullPath(), "baskets/" + basket->folderName());
0197     // Save basket icon:
0198     QString tempIconFile = tempFolder + "icon.png";
0199     if (!basket->icon().isEmpty() && basket->icon() != "basket") {
0200         QPixmap icon = KIconLoader::global()->loadIcon(basket->icon(), KIconLoader::Small, 16, KIconLoader::DefaultState, QStringList(), /*path_store=*/nullptr, /*canReturnNull=*/true);
0201         if (!icon.isNull()) {
0202             icon.save(tempIconFile, "PNG");
0203             QString iconFileName = basket->icon().replace('/', '_');
0204             tar->addLocalFile(tempIconFile, "basket-icons/" + iconFileName);
0205         }
0206     }
0207     // Save basket background image:
0208     QString imageName = basket->backgroundImageName();
0209     if (!basket->backgroundImageName().isEmpty() && !backgrounds.contains(imageName)) {
0210         QString backgroundPath = Global::backgroundManager->pathForImageName(imageName);
0211         if (!backgroundPath.isEmpty()) {
0212             // Save the background image:
0213             tar->addLocalFile(backgroundPath, "backgrounds/" + imageName);
0214             // Save the preview image:
0215             QString previewPath = Global::backgroundManager->previewPathForImageName(imageName);
0216             if (!previewPath.isEmpty())
0217                 tar->addLocalFile(previewPath, "backgrounds/previews/" + imageName);
0218             // Save the configuration file:
0219             QString configPath = backgroundPath + ".config";
0220             if (dir.exists(configPath))
0221                 tar->addLocalFile(configPath, "backgrounds/" + imageName + ".config");
0222         }
0223         backgrounds.append(imageName);
0224     }
0225 
0226     progress->setValue(progress->value() + 1); // Basket exportation finished
0227     qDebug() << basket->basketName() << " finished";
0228 
0229     // Recursively save child baskets:
0230     BasketListViewItem *item = Global::bnpView->listViewItemForBasket(basket);
0231     if (recursive) {
0232         for (int i = 0; i < item->childCount(); i++) {
0233             saveBasketToArchive(((BasketListViewItem *)item->child(i))->basket(), recursive, tar, backgrounds, tempFolder, progress);
0234         }
0235     }
0236 }
0237 
0238 void Archive::listUsedTags(BasketScene *basket, bool recursive, QList<Tag *> &list)
0239 {
0240     basket->listUsedTags(list);
0241     BasketListViewItem *item = Global::bnpView->listViewItemForBasket(basket);
0242     if (recursive) {
0243         for (int i = 0; i < item->childCount(); i++) {
0244             listUsedTags(((BasketListViewItem *)item->child(i))->basket(), recursive, list);
0245         }
0246     }
0247 }
0248 
0249 void Archive::open(const QString &path)
0250 {
0251     // Use the temporary folder:
0252     QString tempFolder = Global::savesFolder() + "temp-archive/";
0253 
0254     switch (extractArchive(path, tempFolder, false)) {
0255     case IOErrorCode::FailedToOpenResource:
0256         KMessageBox::error(nullptr, i18n("Failed to open a file resource."), i18n("Basket Archive Error"));
0257         break;
0258     case IOErrorCode::NotABasketArchive:
0259         KMessageBox::error(nullptr, i18n("This file is not a basket archive."), i18n("Basket Archive Error"));
0260         break;
0261     case IOErrorCode::CorruptedBasketArchive:
0262         KMessageBox::error(nullptr, i18n("This file is corrupted. It can not be opened."), i18n("Basket Archive Error"));
0263         break;
0264     case IOErrorCode::DestinationExists:
0265         KMessageBox::error(nullptr, i18n("Extraction path already exists."), i18n("Basket Archive Error"));
0266         break;
0267     case IOErrorCode::IncompatibleBasketVersion:
0268         KMessageBox::error(nullptr,
0269                            i18n("This file was created with a recent version of %1."
0270                                 "Please upgrade to a newer version to be able to open that file.",
0271                                 QGuiApplication::applicationDisplayName()),
0272                            i18n("Basket Archive Error"));
0273         break;
0274     case IOErrorCode::PossiblyCompatibleBasketVersion:
0275         KMessageBox::information(nullptr,
0276                                  i18n("This file was created with a recent version of %1. "
0277                                       "It can be opened but not every information will be available to you. "
0278                                       "For instance, some notes may be missing because they are of a type only available in new versions. "
0279                                       "When saving the file back, consider to save it to another file, to preserve the original one.",
0280                                       QGuiApplication::applicationDisplayName()),
0281                                  i18n("Basket Archive Error"));
0282         [[fallthrough]];
0283     case IOErrorCode::NoError:
0284         if (Global::activeMainWindow()) {
0285             Global::activeMainWindow()->raise();
0286         }
0287         // Import the Tags:
0288 
0289         importTagEmblems(tempFolder); // Import and rename tag emblems BEFORE loading them!
0290         QMap<QString, QString> mergedStates = Tag::loadTags(tempFolder + "tags.xml");
0291         if (mergedStates.count() > 0) {
0292             Tag::saveTags();
0293         }
0294 
0295         // Import the Background Images:
0296         importArchivedBackgroundImages(tempFolder);
0297 
0298         // Import the Baskets:
0299         renameBasketFolders(tempFolder, mergedStates);
0300 
0301         Tools::deleteRecursively(tempFolder);
0302         break;
0303     }
0304 }
0305 
0306 Archive::IOErrorCode Archive::extractArchive(const QString &path, const QString &destination, const bool protectDestination)
0307 {
0308     IOErrorCode retCode = IOErrorCode::NoError;
0309 
0310     QString l_destination;
0311 
0312     // derive name of the extraction directory
0313     if (destination.isEmpty()) {
0314         // have the decoded baskets the same name as the archive
0315         l_destination = QFileInfo(path).path() + QDir::separator() + QFileInfo(path).baseName() + "-source";
0316     } else {
0317         l_destination = QDir::cleanPath(destination);
0318     }
0319 
0320     QDir dir(l_destination);
0321 
0322     // do nothing when writeProtected
0323     if (dir.exists() && protectDestination) {
0324         return IOErrorCode::DestinationExists;
0325     }
0326 
0327     // Create directory and delete its content in case it was not empty
0328     if (!dir.removeRecursively()) {
0329         return IOErrorCode::FailedToOpenResource;
0330     }
0331     dir.mkpath(QStringLiteral("."));
0332 
0333     const qint64 BUFFER_SIZE = 1024;
0334 
0335     QFile file(path);
0336     if (file.open(QIODevice::ReadOnly)) {
0337         QTextStream stream(&file);
0338         stream.setCodec("ISO-8859-1");
0339         QString line = stream.readLine();
0340         if (line != "BasKetNP:archive") {
0341             file.close();
0342             Tools::deleteRecursively(l_destination);
0343             return IOErrorCode::NotABasketArchive;
0344         }
0345         QString version;
0346         QStringList readCompatibleVersions;
0347         QStringList writeCompatibleVersions;
0348         while (!stream.atEnd()) {
0349             // Get Key/Value Pair From the Line to Read:
0350             line = stream.readLine();
0351             int index = line.indexOf(':');
0352             QString key;
0353             QString value;
0354             if (index >= 0) {
0355                 key = line.left(index);
0356                 value = line.right(line.length() - index - 1);
0357             } else {
0358                 key = line;
0359                 value = QString();
0360             }
0361             if (key == "version") {
0362                 version = value;
0363             } else if (key == "read-compatible") {
0364                 readCompatibleVersions = value.split(';');
0365             } else if (key == "write-compatible") {
0366                 writeCompatibleVersions = value.split(';');
0367             } else if (key == "preview*") {
0368                 bool ok;
0369                 const qint64 size = value.toULong(&ok);
0370                 if (!ok) {
0371                     file.close();
0372                     Tools::deleteRecursively(l_destination);
0373                     return IOErrorCode::CorruptedBasketArchive;
0374                 }
0375                 // Get the preview file:
0376                 QFile previewFile(dir.absolutePath() + QDir::separator() + "preview.png");
0377                 if (previewFile.open(QIODevice::WriteOnly)) {
0378                     std::array<char, BUFFER_SIZE> buffer{};
0379                     qint64 remainingBytes = size;
0380                     qint64 sizeRead = 0;
0381                     file.seek(stream.pos());
0382 
0383                     while ((sizeRead = file.read(buffer.data(), qMin(BUFFER_SIZE, remainingBytes))) > 0) {
0384                         previewFile.write(buffer.data(), sizeRead);
0385                         remainingBytes -= sizeRead;
0386                     }
0387                     previewFile.close();
0388                 }
0389                 stream.seek(stream.pos() + size);
0390             } else if (key == "archive*") {
0391                 if (version != "0.6.1" && readCompatibleVersions.contains("0.6.1") && !writeCompatibleVersions.contains("0.6.1")) {
0392                     retCode = IOErrorCode::PossiblyCompatibleBasketVersion;
0393                 }
0394                 if (version != "0.6.1" && !readCompatibleVersions.contains("0.6.1") && !writeCompatibleVersions.contains("0.6.1")) {
0395                     file.close();
0396                     Tools::deleteRecursively(l_destination);
0397                     return IOErrorCode::IncompatibleBasketVersion;
0398                 }
0399 
0400                 bool ok;
0401                 qint64 size = value.toULong(&ok);
0402                 if (!ok) {
0403                     file.close();
0404                     Tools::deleteRecursively(l_destination);
0405                     return IOErrorCode::CorruptedBasketArchive;
0406                 }
0407 
0408                 // Get the archive file and extract it to destination:
0409                 QTemporaryDir tempDir;
0410                 if (!tempDir.isValid()) {
0411                     return IOErrorCode::FailedToOpenResource;
0412                 }
0413                 QString tempArchive = tempDir.path() + QDir::separator() + "temp-archive.tar.gz";
0414                 QFile archiveFile(tempArchive);
0415                 file.seek(stream.pos());
0416                 if (archiveFile.open(QIODevice::WriteOnly)) {
0417                     char *buffer = new char[BUFFER_SIZE];
0418                     qint64 sizeRead;
0419                     while ((sizeRead = file.read(buffer, qMin(BUFFER_SIZE, size))) > 0) {
0420                         archiveFile.write(buffer, sizeRead);
0421                         size -= sizeRead;
0422                     }
0423                     archiveFile.close();
0424                     delete[] buffer;
0425 
0426                     // Extract the Archive:
0427                     KTar tar(tempArchive, "application/x-gzip");
0428                     tar.open(QIODevice::ReadOnly);
0429                     tar.directory()->copyTo(l_destination);
0430                     tar.close();
0431 
0432                     stream.seek(file.pos());
0433                 }
0434             } else if (key.endsWith('*')) {
0435                 // We do not know what it is, but we should read the embedded-file in
0436                 // order to discard it:
0437                 bool ok;
0438                 qint64 size = value.toULong(&ok);
0439                 if (!ok) {
0440                     file.close();
0441                     Tools::deleteRecursively(l_destination);
0442                     return IOErrorCode::CorruptedBasketArchive;
0443                 }
0444                 // Get the archive file:
0445                 char *buffer = new char[BUFFER_SIZE];
0446                 qint64 sizeRead;
0447                 while ((sizeRead = file.read(buffer, qMin(BUFFER_SIZE, size))) > 0) {
0448                     size -= sizeRead;
0449                 }
0450                 delete[] buffer;
0451             } else {
0452                 // We do not know what it is, and we do not care.
0453             }
0454             // Analyze the Value, if Understood:
0455         }
0456         file.close();
0457     }
0458 
0459     return retCode;
0460 }
0461 
0462 Archive::IOErrorCode Archive::createArchiveFromSource(const QString &sourcePath, const QString &previewImage, const QString &destination, const bool protectDestination)
0463 {
0464     QDir source(sourcePath);
0465     QFileInfo destinationFile(destination);
0466 
0467     // sourcePath must be a valid directory
0468     if (!source.exists()) {
0469         return IOErrorCode::FailedToOpenResource;
0470     }
0471 
0472     // destinationFile must not previously exist;
0473     if (destinationFile.exists() && protectDestination) {
0474         return IOErrorCode::DestinationExists;
0475     }
0476 
0477     QTemporaryDir tempDir;
0478     if (!tempDir.isValid()) {
0479         return IOErrorCode::FailedToOpenResource;
0480     }
0481 
0482     // Create the temporary archive file:
0483     QString tempDestinationFile = tempDir.path() + QDir::separator() + "temp-archive.tar.gz";
0484     KTar archive(tempDestinationFile, "application/x-gzip");
0485 
0486     // Prepare the archive for writing.
0487     if (!archive.open(QIODevice::WriteOnly)) {
0488         // Failed to open file.
0489         archive.close();
0490         Tools::deleteRecursively(tempDir.path());
0491         return IOErrorCode::FailedToOpenResource;
0492     }
0493 
0494     // Add files and directories to tar archive
0495     auto sourceFiles = source.entryList(QDir::Files);
0496     sourceFiles.removeOne("preview.png");
0497     std::for_each(sourceFiles.constBegin(), sourceFiles.constEnd(), [&](const QString &entry) {
0498         archive.addLocalFile(source.absolutePath() + QDir::separator() + entry, entry);
0499     });
0500     const auto sourceDirectories = source.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
0501     std::for_each(sourceDirectories.constBegin(), sourceDirectories.constEnd(), [&](const QString &entry) {
0502         archive.addLocalDirectory(source.absolutePath() + QDir::separator() + entry, entry);
0503     });
0504 
0505     archive.close();
0506 
0507     // use generic basket icon as preview if no valid image supplied
0508     /// \todo write a way to create preview the way it's done in Archive::save
0509     QString previewImagePath = previewImage;
0510     if (previewImage.isEmpty() && !QFileInfo(previewImage).exists()) {
0511         previewImagePath = ":/images/128-apps-org.kde.basket.png";
0512     }
0513 
0514     // Finally Save to the Real Destination file:
0515     QFile file(destination);
0516     if (file.open(QIODevice::WriteOnly)) {
0517         ulong previewSize = QFile(previewImagePath).size();
0518         ulong archiveSize = QFile(tempDestinationFile).size();
0519         QTextStream stream(&file);
0520         stream.setCodec("ISO-8859-1");
0521         stream << "BasKetNP:archive\n"
0522                << "version:0.6.1\n"
0523                //             << "read-compatible:0.6.1\n"
0524                //             << "write-compatible:0.6.1\n"
0525                << "preview*:" << previewSize << "\n";
0526 
0527         stream.flush();
0528         // Copy the Preview File:
0529         const unsigned long BUFFER_SIZE = 1024;
0530         char *buffer = new char[BUFFER_SIZE];
0531         long sizeRead;
0532         QFile previewFile(previewImagePath);
0533         if (previewFile.open(QIODevice::ReadOnly)) {
0534             while ((sizeRead = previewFile.read(buffer, BUFFER_SIZE)) > 0)
0535                 file.write(buffer, sizeRead);
0536         }
0537         stream << "archive*:" << archiveSize << "\n";
0538         stream.flush();
0539 
0540         // Copy the Archive File:
0541         QFile archiveFile(tempDestinationFile);
0542         if (archiveFile.open(QIODevice::ReadOnly)) {
0543             while ((sizeRead = archiveFile.read(buffer, BUFFER_SIZE)) > 0)
0544                 file.write(buffer, sizeRead);
0545         }
0546         // Clean Up:
0547         delete[] buffer;
0548         buffer = nullptr;
0549         file.close();
0550     }
0551 
0552     return IOErrorCode::NoError;
0553 }
0554 
0555 /**
0556  * When opening a basket archive that come from another computer,
0557  * it can contains tags that use icons (emblems) that are not present on that computer.
0558  * Fortunately, basket archives contains a copy of every used icons.
0559  * This method check for every emblems and import the missing ones.
0560  * It also modify the tags.xml copy for the emblems to point to the absolute path of the imported icons.
0561  */
0562 void Archive::importTagEmblems(const QString &extractionFolder)
0563 {
0564     QDomDocument *document = XMLWork::openFile("basketTags", extractionFolder + "tags.xml");
0565     if (document == nullptr)
0566         return;
0567     QDomElement docElem = document->documentElement();
0568 
0569     QDir dir;
0570     dir.mkdir(Global::savesFolder() + "tag-emblems/");
0571     FormatImporter copier; // Only used to copy files synchronously
0572 
0573     QDomNode node = docElem.firstChild();
0574     while (!node.isNull()) {
0575         QDomElement element = node.toElement();
0576         if ((!element.isNull()) && element.tagName() == "tag") {
0577             QDomNode subNode = element.firstChild();
0578             while (!subNode.isNull()) {
0579                 QDomElement subElement = subNode.toElement();
0580                 if ((!subElement.isNull()) && subElement.tagName() == "state") {
0581                     QString emblemName = XMLWork::getElementText(subElement, "emblem");
0582                     if (!emblemName.isEmpty()) {
0583                         QPixmap emblem = KIconLoader::global()->loadIcon(emblemName, KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, /*canReturnNull=*/true);
0584                         // The icon does not exists on that computer, import it:
0585                         if (emblem.isNull()) {
0586                             // Of the emblem path was eg. "/home/seb/emblem.png", it was exported as "tag-emblems/_home_seb_emblem.png".
0587                             // So we need to copy that image to "~/.local/share/basket/tag-emblems/emblem.png":
0588                             int slashIndex = emblemName.lastIndexOf('/');
0589                             QString emblemFileName = (slashIndex < 0 ? emblemName : emblemName.right(slashIndex - 2));
0590                             QString source = extractionFolder + "tag-emblems/" + emblemName.replace('/', '_');
0591                             QString destination = Global::savesFolder() + "tag-emblems/" + emblemFileName;
0592                             if (!dir.exists(destination) && dir.exists(source))
0593                                 copier.copyFolder(source, destination);
0594                             // Replace the emblem path in the tags.xml copy:
0595                             QDomElement emblemElement = XMLWork::getElement(subElement, "emblem");
0596                             subElement.removeChild(emblemElement);
0597                             XMLWork::addElement(*document, subElement, "emblem", destination);
0598                         }
0599                     }
0600                 }
0601                 subNode = subNode.nextSibling();
0602             }
0603         }
0604         node = node.nextSibling();
0605     }
0606     FileStorage::safelySaveToFile(extractionFolder + "tags.xml", document->toString());
0607 }
0608 
0609 void Archive::importArchivedBackgroundImages(const QString &extractionFolder)
0610 {
0611     FormatImporter copier; // Only used to copy files synchronously
0612     QString destFolder = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/basket/backgrounds/";
0613     QDir().mkpath(destFolder); // does not exist at the first run when addWelcomeBaskets is called
0614 
0615     QDir dir(extractionFolder + "backgrounds/", /*nameFilder=*/"*.png", /*sortSpec=*/QDir::Name | QDir::IgnoreCase, /*filterSpec=*/QDir::Files | QDir::NoSymLinks);
0616     QStringList files = dir.entryList();
0617     for (QStringList::Iterator it = files.begin(); it != files.end(); ++it) {
0618         QString image = *it;
0619         if (!Global::backgroundManager->exists(image)) {
0620             // Copy images:
0621             QString imageSource = extractionFolder + "backgrounds/" + image;
0622             QString imageDest = destFolder + image;
0623             copier.copyFolder(imageSource, imageDest);
0624             // Copy configuration file:
0625             QString configSource = extractionFolder + "backgrounds/" + image + ".config";
0626             QString configDest = destFolder + image;
0627             if (dir.exists(configSource))
0628                 copier.copyFolder(configSource, configDest);
0629             // Copy preview:
0630             QString previewSource = extractionFolder + "backgrounds/previews/" + image;
0631             QString previewDest = destFolder + "previews/" + image;
0632             if (dir.exists(previewSource)) {
0633                 dir.mkdir(destFolder + "previews/"); // Make sure the folder exists!
0634                 copier.copyFolder(previewSource, previewDest);
0635             }
0636             // Append image to database:
0637             Global::backgroundManager->addImage(imageDest);
0638         }
0639     }
0640 }
0641 
0642 void Archive::renameBasketFolders(const QString &extractionFolder, QMap<QString, QString> &mergedStates)
0643 {
0644     QDomDocument *doc = XMLWork::openFile("basketTree", extractionFolder + "baskets/baskets.xml");
0645     if (doc != nullptr) {
0646         QMap<QString, QString> folderMap;
0647         QDomElement docElem = doc->documentElement();
0648         QDomNode node = docElem.firstChild();
0649         renameBasketFolder(extractionFolder, node, folderMap, mergedStates);
0650         loadExtractedBaskets(extractionFolder, node, folderMap, nullptr);
0651     }
0652 }
0653 
0654 void Archive::renameBasketFolder(const QString &extractionFolder, QDomNode &basketNode, QMap<QString, QString> &folderMap, QMap<QString, QString> &mergedStates)
0655 {
0656     QDomNode n = basketNode;
0657     while (!n.isNull()) {
0658         QDomElement element = n.toElement();
0659         if ((!element.isNull()) && element.tagName() == "basket") {
0660             QString folderName = element.attribute("folderName");
0661             if (!folderName.isEmpty()) {
0662                 // Find a folder name:
0663                 QString newFolderName = BasketFactory::newFolderName();
0664                 folderMap[folderName] = newFolderName;
0665                 // Reserve the folder name:
0666                 QDir dir;
0667                 dir.mkdir(Global::basketsFolder() + newFolderName);
0668                 // Rename the merged tag ids:
0669                 //              if (mergedStates.count() > 0) {
0670                 renameMergedStatesAndBasketIcon(extractionFolder + "baskets/" + folderName + ".basket", mergedStates, extractionFolder);
0671                 //              }
0672                 // Child baskets:
0673                 QDomNode node = element.firstChild();
0674                 renameBasketFolder(extractionFolder, node, folderMap, mergedStates);
0675             }
0676         }
0677         n = n.nextSibling();
0678     }
0679 }
0680 
0681 void Archive::renameMergedStatesAndBasketIcon(const QString &fullPath, QMap<QString, QString> &mergedStates, const QString &extractionFolder)
0682 {
0683     QDomDocument *doc = XMLWork::openFile("basket", fullPath);
0684     if (doc == nullptr)
0685         return;
0686     QDomElement docElem = doc->documentElement();
0687     QDomElement properties = XMLWork::getElement(docElem, "properties");
0688     importBasketIcon(properties, extractionFolder);
0689     QDomElement notes = XMLWork::getElement(docElem, "notes");
0690     if (mergedStates.count() > 0)
0691         renameMergedStates(notes, mergedStates);
0692     FileStorage::safelySaveToFile(fullPath, /*"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + */ doc->toString());
0693 }
0694 
0695 void Archive::importBasketIcon(QDomElement properties, const QString &extractionFolder)
0696 {
0697     QString iconName = XMLWork::getElementText(properties, "icon");
0698     if (!iconName.isEmpty() && iconName != "basket") {
0699         QPixmap icon = KIconLoader::global()->loadIcon(iconName, KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, /*canReturnNull=*/true);
0700         // The icon does not exists on that computer, import it:
0701         if (icon.isNull()) {
0702             QDir dir;
0703             dir.mkdir(Global::savesFolder() + "basket-icons/");
0704             FormatImporter copier; // Only used to copy files synchronously
0705             // Of the icon path was eg. "/home/seb/icon.png", it was exported as "basket-icons/_home_seb_icon.png".
0706             // So we need to copy that image to "~/.local/share/basket/basket-icons/icon.png":
0707             int slashIndex = iconName.lastIndexOf('/');
0708             QString iconFileName = (slashIndex < 0 ? iconName : iconName.right(slashIndex - 2));
0709             QString source = extractionFolder + "basket-icons/" + iconName.replace('/', '_');
0710             QString destination = Global::savesFolder() + "basket-icons/" + iconFileName;
0711             if (!dir.exists(destination))
0712                 copier.copyFolder(source, destination);
0713             // Replace the emblem path in the tags.xml copy:
0714             QDomElement iconElement = XMLWork::getElement(properties, "icon");
0715             properties.removeChild(iconElement);
0716             QDomDocument document = properties.ownerDocument();
0717             XMLWork::addElement(document, properties, "icon", destination);
0718         }
0719     }
0720 }
0721 
0722 void Archive::renameMergedStates(QDomNode notes, QMap<QString, QString> &mergedStates)
0723 {
0724     QDomNode n = notes.firstChild();
0725     while (!n.isNull()) {
0726         QDomElement element = n.toElement();
0727         if (!element.isNull()) {
0728             if (element.tagName() == "group") {
0729                 renameMergedStates(n, mergedStates);
0730             } else if (element.tagName() == "note") {
0731                 QString tags = XMLWork::getElementText(element, "tags");
0732                 if (!tags.isEmpty()) {
0733                     QStringList tagNames = tags.split(';');
0734                     for (QStringList::Iterator it = tagNames.begin(); it != tagNames.end(); ++it) {
0735                         QString &tag = *it;
0736                         if (mergedStates.contains(tag)) {
0737                             tag = mergedStates[tag];
0738                         }
0739                     }
0740                     QString newTags = tagNames.join(";");
0741                     QDomElement tagsElement = XMLWork::getElement(element, "tags");
0742                     element.removeChild(tagsElement);
0743                     QDomDocument document = element.ownerDocument();
0744                     XMLWork::addElement(document, element, "tags", newTags);
0745                 }
0746             }
0747         }
0748         n = n.nextSibling();
0749     }
0750 }
0751 
0752 void Archive::loadExtractedBaskets(const QString &extractionFolder, QDomNode &basketNode, QMap<QString, QString> &folderMap, BasketScene *parent)
0753 {
0754     bool basketSetAsCurrent = (parent != nullptr);
0755     QDomNode n = basketNode;
0756     while (!n.isNull()) {
0757         QDomElement element = n.toElement();
0758         if ((!element.isNull()) && element.tagName() == "basket") {
0759             QString folderName = element.attribute("folderName");
0760             if (!folderName.isEmpty()) {
0761                 // Move the basket folder to its destination, while renaming it uniquely:
0762                 QString newFolderName = folderMap[folderName];
0763                 FormatImporter copier;
0764                 // The folder has been "reserved" by creating it. Avoid asking the user to override:
0765                 QDir dir;
0766                 dir.rmdir(Global::basketsFolder() + newFolderName);
0767                 copier.moveFolder(extractionFolder + "baskets/" + folderName, Global::basketsFolder() + newFolderName);
0768                 // Append and load the basket in the tree:
0769                 BasketScene *basket = Global::bnpView->loadBasket(newFolderName);
0770                 BasketListViewItem *basketItem = Global::bnpView->appendBasket(basket, (basket && parent ? Global::bnpView->listViewItemForBasket(parent) : nullptr));
0771                 basketItem->setExpanded(!XMLWork::trueOrFalse(element.attribute("folded", "false"), false));
0772                 QDomElement properties = XMLWork::getElement(element, "properties");
0773                 importBasketIcon(properties, extractionFolder); // Rename the icon fileName if necessary
0774                 basket->loadProperties(properties);
0775                 // Open the first basket of the archive:
0776                 if (!basketSetAsCurrent) {
0777                     Global::bnpView->setCurrentBasket(basket);
0778                     basketSetAsCurrent = true;
0779                 }
0780                 QDomNode node = element.firstChild();
0781                 loadExtractedBaskets(extractionFolder, node, folderMap, basket);
0782             }
0783         }
0784         n = n.nextSibling();
0785     }
0786 }