File indexing completed on 2024-05-12 16:39:40

0001 /* This file is part of the KDE project
0002    Copyright (C) 2003 Lucijan Busch <lucijan@gmx.at>
0003    Copyright (C) 2003-2016 Jarosław Staniek <staniek@kde.org>
0004 
0005    This library is free software; you can redistribute it and/or
0006    modify it under the terms of the GNU Library General Public
0007    License as published by the Free Software Foundation; either
0008    version 2 of the License, or (at your option) any later version.
0009 
0010    This library is distributed in the hope that it will be useful,
0011    but WITHOUT ANY WARRANTY; without even the implied warranty of
0012    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0013    Library General Public License for more details.
0014 
0015    You should have received a copy of the GNU Library General Public License
0016    along with this library; see the file COPYING.LIB.  If not, write to
0017    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0018  * Boston, MA 02110-1301, USA.
0019 */
0020 
0021 #include "kexiprojectdata.h"
0022 #include <kexiutils/utils.h>
0023 #include <kexi_global.h>
0024 
0025 #include <KDbUtils>
0026 #include <KDbDriverManager>
0027 #include <KDbConnectionData>
0028 #include <KDbDriverMetaData>
0029 
0030 #include <KConfig>
0031 #include <KConfigGroup>
0032 
0033 #include <QDir>
0034 #include <QStringList>
0035 #include <QDebug>
0036 
0037 //! Version of the KexiProjectData format.
0038 #define KEXIPROJECTDATA_FORMAT 3
0039 /* CHANGELOG:
0040  v1: initial version
0041  v2: "encryptedPassword" field added.
0042      For backward compatibility, it is not used if the connection data has been loaded from
0043      a file saved with version 1. In such cases unencrypted "password" field is used.
0044  v3: "name" for shortcuts to file-based databases is a full file path.
0045      If the file is within the user's home directory, the dir is replaced with $HOME,
0046      e.g. name=$HOME/mydb.kexi. Not compatible with earlier versions but in these
0047      versions only filename was stored so the file was generally inaccessible anyway.
0048      "lastOpened" field added of type date/time (ISO format).
0049 */
0050 
0051 //! @internal
0052 class KexiProjectDataPrivate
0053 {
0054 public:
0055     KexiProjectDataPrivate()
0056             : userMode(false)
0057             , readOnly(false) {}
0058 
0059     KDbConnectionData connData;
0060     QDateTime lastOpened;
0061     bool userMode;
0062     bool readOnly;
0063 };
0064 
0065 //---------------------------------------
0066 
0067 KexiProjectData::KexiProjectData()
0068         : QObject(0)
0069         , KDbObject()
0070         , formatVersion(0)
0071         , d(new KexiProjectDataPrivate())
0072 {
0073     setObjectName("KexiProjectData");
0074 }
0075 
0076 KexiProjectData::KexiProjectData(
0077     const KDbConnectionData &cdata, const QString& dbname, const QString& caption)
0078         : QObject(0)
0079         , KDbObject()
0080         , formatVersion(0)
0081         , d(new KexiProjectDataPrivate())
0082 {
0083     setObjectName("KexiProjectData");
0084     d->connData = cdata;
0085     setDatabaseName(cdata.databaseName().isEmpty() ? dbname : cdata.databaseName());
0086     setCaption(caption);
0087 }
0088 
0089 KexiProjectData::KexiProjectData(const KexiProjectData& pdata)
0090         : QObject(0)
0091         , KDbObject()
0092         , KDbResultable(pdata)
0093         , d(new KexiProjectDataPrivate())
0094 {
0095     setObjectName("KexiProjectData");
0096     *this = pdata;
0097     autoopenObjects = pdata.autoopenObjects;
0098 }
0099 
0100 KexiProjectData::~KexiProjectData()
0101 {
0102     delete d;
0103 }
0104 
0105 KexiProjectData& KexiProjectData::operator=(const KexiProjectData & pdata)
0106 {
0107     static_cast<KDbObject&>(*this) = static_cast<const KDbObject&>(pdata);
0108     //deep copy
0109     autoopenObjects = pdata.autoopenObjects;
0110     formatVersion = pdata.formatVersion;
0111     *d = *pdata.d;
0112     return *this;
0113 }
0114 
0115 KDbConnectionData* KexiProjectData::connectionData()
0116 {
0117     return &d->connData;
0118 }
0119 
0120 const KDbConnectionData* KexiProjectData::connectionData() const
0121 {
0122     return &d->connData;
0123 }
0124 
0125 QString KexiProjectData::databaseName() const
0126 {
0127     return KDbObject::name();
0128 }
0129 
0130 void KexiProjectData::setDatabaseName(const QString& dbName)
0131 {
0132     //qDebug() << dbName;
0133     //qDebug() << *this;
0134     KDbObject::setName(dbName);
0135 }
0136 
0137 bool KexiProjectData::userMode() const
0138 {
0139     return d->userMode;
0140 }
0141 
0142 QDateTime KexiProjectData::lastOpened() const
0143 {
0144     return d->lastOpened;
0145 }
0146 
0147 void KexiProjectData::setLastOpened(const QDateTime& lastOpened)
0148 {
0149     d->lastOpened = lastOpened;
0150 
0151 }
0152 QString KexiProjectData::description() const
0153 {
0154     return KDbObject::description();
0155 }
0156 
0157 void KexiProjectData::setDescription(const QString& desc)
0158 {
0159     return KDbObject::setDescription(desc);
0160 }
0161 
0162 // static
0163 KLocalizedString KexiProjectData::infoString(const QString &databaseName,
0164                                     const KDbConnectionData &data)
0165 {
0166     if (data.databaseName().isEmpty()) {
0167         //server-based
0168         return kxi18nc("@info database connection",
0169                        "<resource>%1</resource> (connection <resource>%2</resource>)")
0170                 .subs(databaseName).subs(data.toUserVisibleString());
0171     }
0172     //file-based
0173     return kxi18nc("@info database name",
0174                    "<resource>%1</resource>").subs(data.databaseName());
0175 }
0176 
0177 KLocalizedString KexiProjectData::infoString() const
0178 {
0179     return infoString(databaseName(), d->connData);
0180 }
0181 
0182 void KexiProjectData::setReadOnly(bool set)
0183 {
0184     d->readOnly = set;
0185 }
0186 
0187 bool KexiProjectData::isReadOnly() const
0188 {
0189     return d->readOnly;
0190 }
0191 
0192 bool KexiProjectData::load(const QString& fileName, QString* _groupKey)
0193 {
0194     //! @todo how about readOnly arg?
0195     KConfig config(fileName, KConfig::SimpleConfig);
0196     KConfigGroup cg = config.group("File Information");
0197     int _formatVersion = cg.readEntry("version", KEXIPROJECTDATA_FORMAT);
0198 
0199     QString groupKey;
0200     if (!_groupKey || _groupKey->isEmpty()) {
0201         QStringList groups(config.groupList());
0202         foreach(const QString &s, groups) {
0203             if (s.toLower() != "file information") {
0204                 groupKey = s;
0205                 break;
0206             }
0207         }
0208         if (groupKey.isEmpty()) {
0209             m_result = KDbResult(xi18n("File <filename>%1</filename> contains no connection information.",
0210                                        fileName));
0211             return false;
0212         }
0213         if (_groupKey)
0214             *_groupKey = groupKey;
0215     } else {
0216         if (!config.hasGroup(*_groupKey)) {
0217             m_result = KDbResult(xi18n("File <filename>%1</filename> does not contain group <resource>%2</resource>.",
0218                                        fileName, *_groupKey));
0219             return false;
0220         }
0221         groupKey = *_groupKey;
0222     }
0223 
0224     cg = config.group(groupKey);
0225     QString type(cg.readEntry("type", "database").toLower());
0226 
0227     bool isDatabaseShortcut;
0228     if (type == "database") {
0229         isDatabaseShortcut = true;
0230     } else if (type == "connection") {
0231         isDatabaseShortcut = false;
0232     } else {
0233         m_result = KDbResult(xi18n("Invalid value <resource>%1</resource> type specified in group "
0234                                    "<resource>%2</resource> of file <filename>%3</filename>.",
0235                                    type, groupKey, fileName));
0236         return false;
0237     }
0238 
0239     const QString driverName = cg.readEntry("engine").toLower();
0240     if (driverName.isEmpty()) {
0241         m_result = KDbResult(xi18n("No valid \"engine\" field specified in group <resource>%1</resource> "
0242                                    "of file <filename>%2</filename>.", groupKey, fileName));
0243         return false;
0244     }
0245 
0246     // verification OK, now applying the values:
0247     // -- "engine" is backward compatible simple name, not a driver ID
0248     d->connData.setDriverId(QString::fromLatin1("org.kde.kdb.") + driverName);
0249     formatVersion = _formatVersion;
0250     d->connData.setHostName(cg.readEntry("server")); //empty allowed, means localhost
0251     if (isDatabaseShortcut) {
0252         KDbDriverManager driverManager;
0253         const KDbDriverMetaData *driverMetaData = driverManager.driverMetaData(d->connData.driverId());
0254         if (!driverMetaData) {
0255              m_result = driverManager.result();
0256             return false;
0257         }
0258         const bool fileBased = driverMetaData->isFileBased()
0259                 && driverMetaData->id() == d->connData.driverId();
0260         setCaption(cg.readEntry("caption"));
0261         setDescription(cg.readEntry("comment"));
0262         d->connData.setCaption(QString()); /* connection name is not specified... */
0263         d->connData.setDescription(QString());
0264         if (fileBased) {
0265             QString fn(cg.readEntry("name"));
0266             if (!fn.isEmpty()) {
0267                 const QString homeVar("$HOME");
0268                 if (fn.startsWith(homeVar)) {
0269                     QString home(QDir::homePath());
0270                     if (home.endsWith('/')) {
0271                         home.chop(1);
0272                     }
0273                     fn = home + fn.mid(homeVar.length());
0274                 }
0275                 d->connData.setDatabaseName(fn);
0276                 setDatabaseName(d->connData.databaseName());
0277             }
0278         }
0279         else {
0280             setDatabaseName(cg.readEntry("name"));
0281         }
0282     } else { // connection
0283         d->connData.setDatabaseName(QString());
0284         setCaption(QString());
0285         d->connData.setCaption(cg.readEntry("caption"));
0286         setDescription(QString());
0287         d->connData.setDescription(cg.readEntry("comment"));
0288         setDatabaseName(QString());   /* db name is not specified... */
0289     }
0290     d->connData.setPort(cg.readEntry("port", 0));
0291     d->connData.setUseLocalSocketFile(cg.readEntry("useLocalSocketFile", true));
0292     d->connData.setLocalSocketFileName(cg.readEntry("localSocketFile"));
0293     d->connData.setSavePassword(cg.hasKey("password") || cg.hasKey("encryptedPassword"));
0294     if (formatVersion >= 2) {
0295         QString password = cg.readEntry("encryptedPassword");
0296         if (!KDbUtils::simpleDecrypt(&password)) {
0297             qWarning() << "Encrypted password in connection file" << fileName << "cannot be decrypted so won't be used. "
0298                           "Delete \"encryptedPassword\" line or correct it to fix this issue.";
0299             password.clear();
0300         }
0301         d->connData.setPassword(password);
0302     }
0303     if (!cg.hasKey("encryptedPassword")) {//no "encryptedPassword", for compatibility
0304         //UNSAFE
0305         d->connData.setPassword(cg.readEntry("password"));
0306     }
0307     d->connData.setUserName(cg.readEntry("user"));
0308     QString lastOpenedStr(cg.readEntry("lastOpened"));
0309     if (!lastOpenedStr.isEmpty()) {
0310         QDateTime lastOpened(QDateTime::fromString(lastOpenedStr, Qt::ISODate));
0311         if (lastOpened.isValid()) {
0312             setLastOpened(lastOpened);
0313         }
0314     }
0315     /*! @todo add "options=", eg. as string list? */
0316     return true;
0317 }
0318 
0319 //! @return a simple driver name (as used by Kexi <= 2.x) for KDb's driver ID
0320 //! or empty string if matching name not found.
0321 //! Example: "sqlite" name is returned for "org.kde.kdb.sqlite" ID.
0322 static QString driverIdToKexi2DriverName(const QString &driverId)
0323 {
0324     QString prefix = "org.kde.kdb.";
0325     if (!driverId.startsWith(prefix)) {
0326         return QString();
0327     }
0328     QString suffix = driverId.mid(prefix.length());
0329     if (suffix == "sqlite"
0330         || suffix == "mysql"
0331         || suffix == "postgresql"
0332         || suffix == "xbase"
0333         || suffix == "sybase")
0334     {
0335         return suffix;
0336     }
0337     return QString();
0338 }
0339 
0340 bool KexiProjectData::save(const QString& fileName, bool savePassword,
0341                            QString* _groupKey, bool overwriteFirstGroup)
0342 {
0343     //! @todo how about readOnly arg?
0344     KConfig config(fileName, KConfig::SimpleConfig);
0345     KConfigGroup cg = config.group("File Information");
0346 
0347     int realFormatVersion = formatVersion;
0348     if (realFormatVersion == 0) /* 0 means "default version"*/
0349         realFormatVersion = KEXIPROJECTDATA_FORMAT;
0350     cg.writeEntry("version", realFormatVersion);
0351 
0352     const bool thisIsConnectionData = databaseName().isEmpty();
0353 
0354     //use or find a nonempty group key
0355     QString groupKey;
0356     if (_groupKey && !_groupKey->isEmpty()) {
0357         groupKey = *_groupKey;
0358     } else {
0359         QString groupPrefix;
0360         const QStringList groups(config.groupList());
0361         if (overwriteFirstGroup && !groups.isEmpty()) {
0362             foreach(const QString &s, groups) {
0363                 if (s.toLower() != "file information") {
0364                     groupKey = s;
0365                     break;
0366                 }
0367             }
0368         }
0369 
0370         if (groupKey.isEmpty()) {
0371             //find a new unique name
0372             if (thisIsConnectionData)
0373                 groupPrefix = "Connection%1"; //do not i18n!
0374             else
0375                 groupPrefix = "Database%1"; //do not i18n!
0376 
0377             int number = 1;
0378             while (config.hasGroup(groupPrefix.arg(number))) //a new group key couldn't exist
0379                 number++;
0380             groupKey = groupPrefix.arg(number);
0381         }
0382         if (_groupKey) //return this one (generated or found)
0383             *_groupKey = groupKey;
0384     }
0385 
0386     config.group(groupKey).deleteGroup();
0387     cg = config.group(groupKey);
0388     KDbDriverManager manager;
0389     const KDbDriverMetaData *metaData = manager.driverMetaData(d->connData.driverId());
0390     if (!metaData) {
0391         m_result = manager.result();
0392         return false;
0393     }
0394     const bool fileBased = metaData->isFileBased();
0395     if (thisIsConnectionData) {
0396         cg.writeEntry("type", "connection");
0397         if (!d->connData.caption().isEmpty())
0398             cg.writeEntry("caption", d->connData.caption());
0399         if (!d->connData.description().isEmpty())
0400             cg.writeEntry("comment", d->connData.description());
0401     } else { //database
0402         cg.writeEntry("type", "database");
0403         if (!caption().isEmpty())
0404             cg.writeEntry("caption", caption());
0405         if (fileBased) {
0406             QString fn(d->connData.databaseName());
0407             if (!QDir::homePath().isEmpty() && fn.startsWith(QDir::homePath())) {
0408                 // replace prefix if == $HOME
0409                 fn = fn.mid(QDir::homePath().length());
0410                 if (!fn.startsWith('/'))
0411                     fn.prepend('/');
0412                 fn.prepend(QLatin1String("$HOME"));
0413             }
0414             cg.writeEntry("name", fn);
0415         }
0416         else { // server-based
0417             cg.writeEntry("name", databaseName());
0418         }
0419         if (!description().isEmpty())
0420             cg.writeEntry("comment", description());
0421     }
0422 
0423     QString engine = driverIdToKexi2DriverName(d->connData.driverId());
0424     if (engine.isEmpty()) {
0425         engine = d->connData.driverId();
0426     }
0427     cg.writeEntry("engine", engine);
0428     if (!fileBased) {
0429         if (!d->connData.hostName().isEmpty())
0430             cg.writeEntry("server", d->connData.hostName());
0431 
0432         if (d->connData.port() != 0)
0433             cg.writeEntry("port", int(d->connData.port()));
0434         cg.writeEntry("useLocalSocketFile", d->connData.useLocalSocketFile());
0435         if (!d->connData.localSocketFileName().isEmpty())
0436             cg.writeEntry("localSocketFile", d->connData.localSocketFileName());
0437         if (!d->connData.userName().isEmpty())
0438             cg.writeEntry("user", d->connData.userName());
0439     }
0440 
0441     if (savePassword || d->connData.savePassword()) {
0442         if (realFormatVersion < 2) {
0443             cg.writeEntry("password", d->connData.password());
0444         } else {
0445             QString encryptedPassword = d->connData.password();
0446             KDbUtils::simpleCrypt(&encryptedPassword);
0447             cg.writeEntry("encryptedPassword", encryptedPassword);
0448             encryptedPassword.fill(' '); //for security
0449         }
0450     }
0451 
0452     if (lastOpened().isValid()) {
0453         cg.writeEntry("lastOpened", lastOpened().toString(Qt::ISODate));
0454     }
0455 
0456     /*! @todo add "options=", eg. as string list? */
0457     if (!cg.sync()) {
0458         m_result = KDbResult("Could not write project file.");
0459         return false;
0460     }
0461     return true;
0462 }
0463 
0464 KEXICORE_EXPORT QDebug operator<<(QDebug dbg, const KexiProjectData& data)
0465 {
0466     dbg.space() << "KexiProjectData" << "databaseName=" << data.databaseName()
0467         << "lastOpened=" << data.lastOpened() << "description=" << data.description()
0468         << "connectionData=(";
0469     if (data.connectionData()) {
0470         dbg.nospace() << *data.connectionData();
0471     }
0472     dbg.nospace() << ")";
0473     return dbg.space();
0474 }