File indexing completed on 2024-06-02 05:12:11

0001 /*
0002     SPDX-FileCopyrightText: 2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0003     SPDX-FileCopyrightText: 2021 Dawid Wróbel <me@dawidwrobel.com>
0004     SPDX-FileCopyrightText: 2022 Thomas Baumgart <tbaumgart@kde.org>
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "xmlstorage.h"
0009 
0010 #include <memory>
0011 #include <config-kmymoney.h>
0012 
0013 // ----------------------------------------------------------------------------
0014 // QT Includes
0015 
0016 #include <QTimer>
0017 #include <QFile>
0018 #include <QTemporaryFile>
0019 #include <QFileDialog>
0020 #include <QRegularExpression>
0021 #include <QRegularExpressionMatch>
0022 
0023 // ----------------------------------------------------------------------------
0024 // KDE Includes
0025 
0026 #include <KPluginFactory>
0027 #include <KActionCollection>
0028 #include <KLocalizedString>
0029 #include <KMessageBox>
0030 #include <KCompressionDevice>
0031 #include <KIO/StoredTransferJob>
0032 #include <KBackup>
0033 
0034 // ----------------------------------------------------------------------------
0035 // Project Includes
0036 
0037 #include "appinterface.h"
0038 #include "icons.h"
0039 #include "kgpgfile.h"
0040 #include "kgpgkeyselectiondlg.h"
0041 #include "kmymoneyenums.h"
0042 #include "kmymoneysettings.h"
0043 #include "kmymoneyutils.h"
0044 #include "mymoneyanonwriter.h"
0045 #include "mymoneyexception.h"
0046 #include "mymoneyfile.h"
0047 #include "mymoneystoragebin.h"
0048 #include "mymoneyxmlreader.h"
0049 #include "mymoneyxmlwriter.h"
0050 #include "viewinterface.h"
0051 
0052 #include "kmmyesno.h"
0053 
0054 using namespace Icons;
0055 
0056 static constexpr KCompressionDevice::CompressionType const& COMPRESSION_TYPE = KCompressionDevice::GZip;
0057 // static constexpr char recoveryKeyId[] = "0xD2B08440";
0058 static constexpr char recoveryKeyId[] = "59B0F826D2B08440";
0059 
0060 // define the default period to warn about an expiring recoverkey to 30 days
0061 // but allows to override this setting during build time
0062 #ifndef RECOVER_KEY_EXPIRATION_WARNING
0063 #define RECOVER_KEY_EXPIRATION_WARNING 30
0064 #endif
0065 
0066 XMLStorage::XMLStorage(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args) :
0067     KMyMoneyPlugin::Plugin(parent, metaData, args)
0068 {
0069     // For information, announce that we have been loaded.
0070     qDebug("Plugins: xmlstorage loaded");
0071     checkRecoveryKeyValidity();
0072 }
0073 
0074 XMLStorage::~XMLStorage()
0075 {
0076     qDebug("Plugins: xmlstorage unloaded");
0077 }
0078 
0079 bool XMLStorage::open(const QUrl &url)
0080 {
0081     fileUrl.clear();
0082 
0083     if (url.scheme() == QLatin1String("sql"))
0084         return false;
0085 
0086     QString fileName;
0087     auto downloadedFile = false;
0088     if (url.isLocalFile()) {
0089         fileName = url.toLocalFile();
0090     } else {
0091         fileName = KMyMoneyUtils::downloadFile(url);
0092         downloadedFile = true;
0093     }
0094 
0095     if (!KMyMoneyUtils::fileExists(QUrl::fromLocalFile(fileName)))
0096         throw MYMONEYEXCEPTION(QString::fromLatin1("Error opening the file.\n"
0097                                "Requested file: '%1'.\n"
0098                                "Downloaded file: '%2'").arg(qPrintable(url.url()), fileName));
0099 
0100 
0101     QFile file(fileName);
0102     if (!file.open(QIODevice::ReadOnly))
0103         throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot read the file: %1").arg(fileName));
0104 
0105     QByteArray qbaFileHeader(2, '\0');
0106     const auto sFileToShort = QString::fromLatin1("File %1 is too short.").arg(fileName);
0107     if (file.read(qbaFileHeader.data(), 2) != 2)
0108         throw MYMONEYEXCEPTION(sFileToShort);
0109 
0110     file.close();
0111 
0112     // There's a problem with the KFilterDev and KGPGFile classes:
0113     // One supports the at(n) member but not ungetch() together with
0114     // read() and the other does not provide an at(n) method but
0115     // supports read() that considers the ungetch() buffer. QFile
0116     // supports everything so this is not a problem. We solve the problem
0117     // for now by keeping track of which method can be used.
0118     auto haveAt = true;
0119     auto isEncrypted = false;
0120 
0121     QIODevice* qfile = nullptr;
0122     QString sFileHeader(qbaFileHeader);
0123     if (sFileHeader == QString("\037\213")) {        // gzipped?
0124         qfile = new KCompressionDevice(fileName, COMPRESSION_TYPE);
0125     } else if (sFileHeader == QString("--") ||        // PGP ASCII armored?
0126                sFileHeader == QString("\205\001") ||  // PGP binary?
0127                sFileHeader == QString("\205\002")) {  // PGP binary?
0128 #ifdef ENABLE_GPG
0129         if (KGPGFile::GPGAvailable()) {
0130             qfile = new KGPGFile(fileName);
0131             haveAt = false;
0132             isEncrypted = true;
0133         } else
0134 #endif
0135         {
0136             throw MYMONEYEXCEPTION(QString::fromLatin1("GPG is not available for decryption of file <b>%1</b>").arg(fileName));
0137         }
0138     } else {
0139         // we can't use file directly, as we delete qfile later on
0140         qfile = new QFile(file.fileName());
0141     }
0142 
0143     if (!qfile->open(QIODevice::ReadOnly)) {
0144         delete qfile;
0145         throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot read the file: %1").arg(fileName));
0146     }
0147 
0148     qbaFileHeader.resize(8);
0149     if (qfile->read(qbaFileHeader.data(), 8) != 8)
0150         throw MYMONEYEXCEPTION(sFileToShort);
0151 
0152     if (haveAt)
0153         qfile->seek(0);
0154     else
0155         ungetString(qfile, qbaFileHeader.data(), 8);
0156 
0157     // Ok, we got the first block of 8 bytes. Read in the two
0158     // unsigned long int's by preserving endianness. This is
0159     // achieved by reading them through a QDataStream object
0160     qint32 magic0, magic1;
0161     QDataStream s(&qbaFileHeader, QIODevice::ReadOnly);
0162     s >> magic0;
0163     s >> magic1;
0164 
0165     // If both magic numbers match (we actually read in the
0166     // text 'KMyMoney' then we assume a binary file and
0167     // construct a reader for it. Otherwise, we construct
0168     // an XML reader object.
0169     //
0170     // The expression magic0 < 30 is only used to create
0171     // a binary reader if we assume an old binary file. This
0172     // should be removed at some point. An alternative is to
0173     // check the beginning of the file against an pattern
0174     // of the XML file (e.g. '?<xml' ).
0175     if ((magic0 == MAGIC_0_50 && magic1 == MAGIC_0_51) ||
0176             magic0 < 30) {
0177         // we do not support this file format anymore
0178         throw MYMONEYEXCEPTION(QString::fromLatin1("<qt>File <b>%1</b> contains the old binary format used by KMyMoney. Please use an older version of KMyMoney (0.8.x) that still supports this format to convert it to the new XML based format.</qt>").arg(fileName));
0179     }
0180 
0181     // Scan the first 70 bytes to see if we find something
0182     // we know. For now, we support our own XML format and
0183     // GNUCash XML format. If the file is smaller, then it
0184     // contains no valid data and we reject it anyway.
0185     qbaFileHeader.resize(70);
0186     if (qfile->read(qbaFileHeader.data(), 70) != 70)
0187         throw MYMONEYEXCEPTION(sFileToShort);
0188 
0189     if (haveAt)
0190         qfile->seek(0);
0191     else
0192         ungetString(qfile, qbaFileHeader.data(), 70);
0193 
0194     static const QRegularExpression kmyexp(QLatin1String("<!DOCTYPE KMYMONEY-FILE>"));
0195     QByteArray txt(qbaFileHeader, 70);
0196     const auto docType(kmyexp.match(txt));
0197     if (!docType.hasMatch())
0198         return false;
0199 
0200     MyMoneyXmlReader reader;
0201     reader.setFile(MyMoneyFile::instance());
0202     reader.read(qfile);
0203 
0204     qfile->close();
0205     delete qfile;
0206 
0207     // if a temporary file was downloaded, then it will be removed
0208     // with the next call. Otherwise, it stays untouched on the local
0209     // filesystem.
0210     if (downloadedFile)
0211         QFile::remove(fileName);
0212 
0213     // make sure we setup the encryption key correctly
0214     if (isEncrypted) {
0215         if (MyMoneyFile::instance()->value("kmm-encryption-key").isEmpty()) {
0216             // encapsulate transactions to the engine to be able to commit/rollback
0217             MyMoneyFileTransaction ft;
0218             MyMoneyFile::instance()->setValue("kmm-encryption-key", KMyMoneySettings::gpgRecipientList().join(","));
0219             ft.commit();
0220         }
0221     }
0222 
0223     fileUrl = url;
0224     //write the directory used for this file as the default one for next time.
0225     appInterface()->writeLastUsedDir(url.toDisplayString(QUrl::RemoveFilename | QUrl::PreferLocalFile | QUrl::StripTrailingSlash));
0226 
0227     return true;
0228 }
0229 
0230 QUrl XMLStorage::openUrl() const
0231 {
0232     return fileUrl;
0233 }
0234 
0235 bool XMLStorage::save(const QUrl &url)
0236 {
0237     QString filename = url.toLocalFile();
0238 
0239     if (!appInterface()->fileOpen()) {
0240         KMessageBox::error(nullptr, i18n("Tried to access a file when it has not been opened"));
0241         return false;
0242     }
0243 
0244     std::unique_ptr<MyMoneyXmlWriter> storageWriter;
0245 
0246     // If this file ends in ".ANON.XML" then this should be written using the
0247     // anonymous writer.
0248     bool plaintext = filename.right(4).toLower() == ".xml";
0249     if (filename.right(9).toLower() == ".anon.xml")
0250         storageWriter = std::make_unique<MyMoneyAnonWriter>();
0251     else
0252         storageWriter = std::make_unique<MyMoneyXmlWriter>();
0253 
0254     QString keyList;
0255     if (!appInterface()->filenameURL().isEmpty())
0256         keyList = MyMoneyFile::instance()->value("kmm-encryption-key");
0257     if (keyList.isEmpty())
0258         keyList = m_encryptionKeys;
0259 
0260     // actually, url should be the parameter to this function
0261     // but for now, this would involve too many changes
0262     auto rc = true;
0263     try {
0264         if (! url.isValid()) {
0265             throw MYMONEYEXCEPTION(QString::fromLatin1("Malformed URL '%1'").arg(url.url()));
0266         }
0267 
0268         if (url.isLocalFile()) {
0269             filename = url.toLocalFile();
0270             try {
0271                 const unsigned int nbak = KMyMoneySettings::autoBackupCopies();
0272                 if (nbak) {
0273                     KBackup::numberedBackupFile(filename, QString(), QStringLiteral("~"), nbak);
0274                 }
0275                 saveToLocalFile(filename, storageWriter.get(), plaintext, keyList);
0276             } catch (const MyMoneyException &e) {
0277                 qWarning("Unable to write changes to: %s\nReason: %s", qPrintable(filename), e.what());
0278                 throw;
0279             }
0280         } else {
0281             // obtain a temporary name for the local destination
0282             // using QTemporaryFile. As long as the object is
0283             // not destroyed, the file remains opened, which causes
0284             // problems on MS-Windows if you want to e.g. rename it.
0285             // Since we just need the name at this point, we simply
0286             // create the object, take the name (which is only available
0287             // once the file is opened) and destroy the object (which
0288             // closes the file on the filesystem) to avoid such problems.
0289             const auto tmpfile = new QTemporaryFile;
0290             tmpfile->open();
0291             const auto fileName = tmpfile->fileName();
0292             delete tmpfile;
0293 
0294             saveToLocalFile(fileName, storageWriter.get(), plaintext, keyList);
0295 
0296             Q_CONSTEXPR int permission = -1;
0297             QFile file(fileName);
0298             file.open(QIODevice::ReadOnly);
0299             KIO::StoredTransferJob *putjob = KIO::storedPut(file.readAll(), url, permission, KIO::JobFlag::Overwrite);
0300             if (!putjob->exec()) {
0301                 throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to upload to '%1'.<br />%2").arg(url.toDisplayString(), putjob->errorString()));
0302             }
0303             file.close();
0304         }
0305     } catch (const MyMoneyException &e) {
0306         KMessageBox::error(nullptr, QString::fromLatin1(e.what()));
0307         MyMoneyFile::instance()->setDirty();
0308         rc = false;
0309     }
0310     return rc;
0311 }
0312 
0313 bool XMLStorage::saveAs()
0314 {
0315     auto rc = false;
0316     QStringList m_additionalGpgKeys;
0317     m_encryptionKeys.clear();
0318 
0319     QString selectedKeyName;
0320 #ifdef ENABLE_GPG
0321     if (KGPGFile::GPGAvailable() && KMyMoneySettings::writeDataEncrypted()) {
0322         // fill the secret key list and combo box
0323         QStringList keyList;
0324         KGPGFile::secretKeyList(keyList);
0325 
0326         QPointer<KGpgKeySelectionDlg> dlg = new KGpgKeySelectionDlg(nullptr);
0327         dlg->setSecretKeys(keyList, KMyMoneySettings::gpgRecipient());
0328         dlg->setAdditionalKeys(KMyMoneySettings::gpgRecipientList());
0329         rc = dlg->exec();
0330         if ((rc == QDialog::Accepted) && (dlg != 0)) {
0331             m_additionalGpgKeys = dlg->additionalKeys();
0332             selectedKeyName = dlg->secretKey();
0333         }
0334         delete dlg;
0335         if (rc != QDialog::Accepted) {
0336             return rc;
0337         }
0338     }
0339 #endif
0340 
0341     QString prevDir; // don't prompt file name if not a native file
0342     if (appInterface()->isNativeFile())
0343         prevDir = appInterface()->readLastUsedDir();
0344 
0345     QPointer<QFileDialog> dlg =
0346         new QFileDialog(nullptr,
0347                         i18n("Save As"),
0348                         prevDir,
0349                         QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.kmy"), i18nc("KMyMoney (Filefilter)", "KMyMoney files"))
0350                             + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.anon.xml"), i18nc("Anonymous (Filefilter)", "Anonymous files"))
0351                             + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.xml"), i18nc("XML (Filefilter)", "XML files"))
0352                             + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*"), i18nc("All files (Filefilter)", "All files")));
0353     dlg->setAcceptMode(QFileDialog::AcceptSave);
0354     connect(dlg, &QFileDialog::filterSelected, this, [&](const QString txt) {
0355         // for some reason, txt sometimes contains the filter expression only
0356         // e.g. "*.xml" and in some others it contains the full text with
0357         // the filter expression appended in parenthesis e.g.
0358         // "KMyMoney files (*.xml)". The following logic extracts the
0359         // filter and sets the default suffix based on it.
0360         QRegularExpression filter(QStringLiteral("\\*\\.(?<extension>[a-z\\.]+)"));
0361         const auto match = filter.match(txt);
0362         if (match.hasMatch()) {
0363             dlg->setDefaultSuffix(match.captured(QStringLiteral("extension")));
0364         } else {
0365             dlg->setDefaultSuffix(QString());
0366         }
0367 
0368     });
0369 
0370     if (dlg->exec() == QDialog::Accepted && dlg != 0) {
0371         QUrl newURL = dlg->selectedUrls().first();
0372         if (!newURL.fileName().isEmpty()) {
0373             QString newName = newURL.toDisplayString(QUrl::PreferLocalFile);
0374 
0375             // append extension if not present
0376             if (!newName.endsWith(QLatin1String(".kmy"), Qt::CaseInsensitive) &&
0377                     !newName.endsWith(QLatin1String(".xml"), Qt::CaseInsensitive))
0378                 newName.append(QLatin1String(".kmy"));
0379             newURL = QUrl::fromUserInput(newName);
0380 
0381             // If this is the anonymous file export, just save it, don't actually take the
0382             // name, or remember it! Don't even try to encrypt it
0383             if (newName.endsWith(QLatin1String(".anon.xml"), Qt::CaseInsensitive))
0384                 rc = save(newURL);
0385             else {
0386                 appInterface()->writeFilenameURL(newURL);
0387                 static const QRegularExpression keyExp(QLatin1String(".* \\((.*)\\)"));
0388                 const auto key(keyExp.match(selectedKeyName));
0389                 if (key.hasMatch()) {
0390                     m_encryptionKeys = key.captured(1);
0391                     if (!m_additionalGpgKeys.isEmpty()) {
0392                         if (!m_encryptionKeys.isEmpty())
0393                             m_encryptionKeys.append(QLatin1Char(','));
0394                         m_encryptionKeys.append(m_additionalGpgKeys.join(QLatin1Char(',')));
0395                     }
0396                 }
0397                 // clear out any existing keys so that the new ones will be used
0398                 MyMoneyFileTransaction ft;
0399                 try {
0400                     MyMoneyFile::instance()->deletePair("kmm-encryption-key");
0401                     ft.commit();
0402                 } catch (MyMoneyException&) {
0403                     ; // do nothing
0404                 }
0405                 rc = save(newURL);
0406 
0407                 appInterface()->addToRecentFiles(newURL);
0408                 //write the directory used for this file as the default one for next time.
0409                 appInterface()->writeLastUsedDir(newURL.toDisplayString(QUrl::RemoveFilename | QUrl::PreferLocalFile | QUrl::StripTrailingSlash));
0410                 appInterface()->writeLastUsedFile(newName);
0411             }
0412         }
0413     }
0414     (*appInterface()->progressCallback())(0,0, i18nc("Application is ready to use", "Ready."));
0415     delete dlg;
0416     return rc;
0417 }
0418 
0419 eKMyMoney::StorageType XMLStorage::storageType() const
0420 {
0421     return eKMyMoney::StorageType::XML;
0422 }
0423 
0424 QString XMLStorage::fileExtension() const
0425 {
0426     return i18n("KMyMoney files (*.kmy *.xml)");
0427 }
0428 
0429 void XMLStorage::ungetString(QIODevice *qfile, char *buf, int len)
0430 {
0431     buf = &buf[len-1];
0432     while (len--) {
0433         qfile->ungetChar(*buf--);
0434     }
0435 }
0436 
0437 void XMLStorage::saveToLocalFile(const QString& localFile, MyMoneyXmlWriter* pWriter, bool plaintext, const QString& keyList)
0438 {
0439 #ifndef ENABLE_GPG
0440     Q_UNUSED(keyList)
0441 #else
0442     // Check GPG encryption
0443     bool encryptFile = true;
0444     bool encryptRecover = false;
0445     if (!keyList.isEmpty()) {
0446         if (!KGPGFile::GPGAvailable()) {
0447             KMessageBox::error(nullptr, i18n("GPG does not seem to be installed on your system. Please make sure that GPG can be found using the standard search path. This time, encryption is disabled."), i18n("GPG not found"));
0448             encryptFile = false;
0449         } else {
0450             if (KMyMoneySettings::encryptRecover()) {
0451                 encryptRecover = true;
0452                 if (!KGPGFile::keyAvailable(QString(recoveryKeyId))) {
0453                     KMessageBox::error(nullptr, i18n("<p>You have selected to encrypt your data also with the KMyMoney recover key, but the key with id</p><p><center><b>%1</b></center></p><p>has not been found in your keyring at this time. Please make sure to import this key into your keyring. You can find it on the <a href=\"https://kmymoney.org/\">KMyMoney web-site</a>. This time your data will not be encrypted with the KMyMoney recover key.</p>", QString(recoveryKeyId)), i18n("GPG Key not found"));
0454                     encryptRecover = false;
0455                 }
0456             }
0457 
0458             const auto keys = keyList.split(',', Qt::SkipEmptyParts);
0459             for (const QString& key : qAsConst(keys)) {
0460                 if (!KGPGFile::keyAvailable(key)) {
0461                     KMessageBox::error(nullptr, i18n("<p>You have specified to encrypt your data for the user-id</p><p><center><b>%1</b>.</center></p><p>Unfortunately, a valid key for this user-id was not found in your keyring. Please make sure to import a valid key for this user-id. This time, encryption is disabled.</p>", key), i18n("GPG Key not found"));
0462                     encryptFile = false;
0463                     break;
0464                 }
0465             }
0466 
0467             if (encryptFile == true) {
0468                 QString msg = i18n("<p>You have configured to save your data in encrypted form using GPG. Make sure you understand that you might lose all your data if you encrypt it, but cannot decrypt it later on. If unsure, answer <b>No</b>.</p>");
0469                 if (KMessageBox::questionTwoActions(nullptr, msg, i18n("Store GPG encrypted"), KMMYesNo::yes(), KMMYesNo::no(), "StoreEncrypted")
0470                     == KMessageBox::SecondaryAction) {
0471                     encryptFile = false;
0472                 }
0473             }
0474         }
0475     }
0476 #endif
0477     // Permissions to apply to new file
0478     QFileDevice::Permissions fmode = QFileDevice::ReadUser | QFileDevice::WriteUser;
0479 
0480     // Create a temporary file if needed
0481     QString writeFile = localFile;
0482     if (QFile::exists(localFile)) {
0483         QTemporaryFile tmpFile(writeFile);
0484         tmpFile.open();
0485         writeFile = tmpFile.fileName();
0486         // Since file is going to be replaced, stash the original permissions so they can be restored
0487         fmode = QFile::permissions(localFile);
0488     }
0489 
0490     QSignalBlocker blockMyMoneyFile(MyMoneyFile::instance());
0491 
0492     MyMoneyFileTransaction ft;
0493     MyMoneyFile::instance()->deletePair("kmm-encryption-key");
0494     std::unique_ptr<QIODevice> device;
0495 
0496 #ifdef ENABLE_GPG
0497     if (!keyList.isEmpty() && encryptFile && !plaintext) {
0498         std::unique_ptr<KGPGFile> kgpg = std::unique_ptr<KGPGFile>(new KGPGFile{writeFile});
0499         if (kgpg) {
0500             const auto keys = keyList.split(',', Qt::SkipEmptyParts);
0501             for (const QString& key : qAsConst(keys)) {
0502                 kgpg->addRecipient(key.toLatin1());
0503             }
0504 
0505             if (encryptRecover) {
0506                 kgpg->addRecipient(recoveryKeyId);
0507             }
0508             MyMoneyFile::instance()->setValue("kmm-encryption-key", keyList);
0509             device = std::unique_ptr<decltype(device)::element_type>(kgpg.release());
0510         }
0511     } else
0512 #endif
0513     {
0514         QFile *file = new QFile(writeFile);
0515         // The second parameter of KCompressionDevice means that KCompressionDevice will delete the QFile object
0516         device = std::unique_ptr<decltype(device)::element_type>(new KCompressionDevice{file, true, (plaintext) ? KCompressionDevice::None : COMPRESSION_TYPE});
0517     }
0518 
0519     ft.commit();
0520 
0521     if (!device || !device->open(QIODevice::WriteOnly)) {
0522         throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to open file '%1' for writing.").arg(localFile));
0523     }
0524 
0525     pWriter->setFile(MyMoneyFile::instance());
0526     const auto xmlWrittenOk = pWriter->write(device.get());
0527     device->close();
0528 
0529     if (!xmlWrittenOk) {
0530         throw MYMONEYEXCEPTION(QString::fromLatin1("XML write failure while writing to '%1'").arg(localFile));
0531     }
0532 
0533     // Check for errors if possible, only possible for KGPGFile
0534     QFileDevice *fileDevice = qobject_cast<QFileDevice*>(device.get());
0535     if (fileDevice && fileDevice->error() != QFileDevice::NoError) {
0536         throw MYMONEYEXCEPTION(QString::fromLatin1("Failure while writing to '%1'").arg(localFile));
0537     }
0538 
0539     if (writeFile != localFile) {
0540         // This simple comparison is possible because the strings are equal if no temporary file was created.
0541         // If a temporary file was created, it is made in a way that the name is definitely different. So no
0542         // symlinks etc. have to be evaluated.
0543 
0544         // on Windows QTemporaryFile does not release file handle even after close()
0545         // so QFile::rename(writeFile, localFile) will fail since Windows does not allow moving files in use
0546         // as a workaround QFile::copy is used instead of QFile::rename below
0547         // writeFile (i.e. tmpFile) will be deleted by QTemporaryFile dtor when it falls out of scope
0548         if (!QFile::remove(localFile)) {
0549             throw MYMONEYEXCEPTION(QString::fromLatin1("Failure while removing '%1'").arg(localFile));
0550         }
0551         if (!QFile::rename(writeFile, localFile)) {
0552             throw MYMONEYEXCEPTION(QString::fromLatin1("Failure while renaming '%1' to '%2'").arg(writeFile, localFile));
0553         }
0554     }
0555     QFile::setPermissions(localFile, fmode);
0556 }
0557 
0558 void XMLStorage::checkRecoveryKeyValidity()
0559 {
0560 #ifdef ENABLE_GPG
0561     // check if the recovery key is still valid or expires soon
0562 
0563     if (KMyMoneySettings::writeDataEncrypted() && KMyMoneySettings::encryptRecover()) {
0564         if (KGPGFile::GPGAvailable()) {
0565             KGPGFile file;
0566             QDateTime expirationDate = file.keyExpires(QLatin1String(recoveryKeyId));
0567             if (expirationDate.isValid() && QDateTime::currentDateTime().daysTo(expirationDate) <= RECOVER_KEY_EXPIRATION_WARNING) {
0568                 bool skipMessage = false;
0569 
0570                 //get global config object for our app.
0571                 KSharedConfigPtr kconfig = KSharedConfig::openConfig();
0572                 KConfigGroup grp;
0573                 QDate lastWarned;
0574                 if (kconfig) {
0575                     grp = kconfig->group("General Options");
0576                     lastWarned = grp.readEntry("LastRecoverKeyExpirationWarning", QDate());
0577                     if (QDate::currentDate() == lastWarned) {
0578                         skipMessage = true;
0579                     }
0580                 }
0581                 if (!skipMessage) {
0582                     if (kconfig) {
0583                         grp.writeEntry("LastRecoverKeyExpirationWarning", QDate::currentDate());
0584                     }
0585                     KMessageBox::information(nullptr, i18np("You have configured KMyMoney to use GPG to protect your data and to encrypt your data also with the KMyMoney recover key. This key is about to expire in %1 day. Please update the key from a keyserver using your GPG frontend (e.g. KGPG).", "You have configured KMyMoney to use GPG to protect your data and to encrypt your data also with the KMyMoney recover key. This key is about to expire in %1 days. Please update the key from a keyserver using your GPG frontend (e.g. KGPG).", QDateTime::currentDateTime().daysTo(expirationDate)), i18n("Recover key expires soon"));
0586                 }
0587             }
0588         }
0589     }
0590 #endif
0591 }
0592 
0593 K_PLUGIN_CLASS_WITH_JSON(XMLStorage, "xmlstorage.json")
0594 
0595 #include "xmlstorage.moc"