1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the Qt Assistant of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39 
40 #include "qhelpdbreader_p.h"
41 #include "qhelp_global.h"
42 
43 #include <QtCore/QVariant>
44 #include <QtCore/QVector>
45 #include <QtCore/QFile>
46 #include <QtSql/QSqlError>
47 #include <QtSql/QSqlQuery>
48 
49 QT_BEGIN_NAMESPACE
50 
QHelpDBReader(const QString & dbName)51 QHelpDBReader::QHelpDBReader(const QString &dbName)
52     : QObject(nullptr),
53       m_dbName(dbName),
54       m_uniqueId(QHelpGlobal::uniquifyConnectionName(QLatin1String("QHelpDBReader"),
55                                                      this))
56 {
57 }
58 
QHelpDBReader(const QString & dbName,const QString & uniqueId,QObject * parent)59 QHelpDBReader::QHelpDBReader(const QString &dbName, const QString &uniqueId,
60                            QObject *parent)
61     : QObject(parent),
62       m_dbName(dbName),
63       m_uniqueId(uniqueId)
64 {
65 }
66 
~QHelpDBReader()67 QHelpDBReader::~QHelpDBReader()
68 {
69     if (m_initDone) {
70         delete m_query;
71         QSqlDatabase::removeDatabase(m_uniqueId);
72     }
73 }
74 
init()75 bool QHelpDBReader::init()
76 {
77     if (m_initDone)
78         return true;
79 
80     if (!QFile::exists(m_dbName))
81         return false;
82 
83     if (!initDB()) {
84         QSqlDatabase::removeDatabase(m_uniqueId);
85         return false;
86     }
87 
88     m_initDone = true;
89     m_query = new QSqlQuery(QSqlDatabase::database(m_uniqueId));
90 
91     return true;
92 }
93 
initDB()94 bool QHelpDBReader::initDB()
95 {
96     QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), m_uniqueId);
97     db.setConnectOptions(QLatin1String("QSQLITE_OPEN_READONLY"));
98     db.setDatabaseName(m_dbName);
99     if (!db.open()) {
100         /*: The placeholders are: %1 - The name of the database which cannot be opened
101                                   %2 - The unique id for the connection
102                                   %3 - The actual error string */
103         m_error = tr("Cannot open database \"%1\" \"%2\": %3").arg(m_dbName, m_uniqueId, db.lastError().text());
104         return false;
105     }
106     return true;
107 }
108 
namespaceName() const109 QString QHelpDBReader::namespaceName() const
110 {
111     if (!m_namespace.isEmpty())
112         return m_namespace;
113     if (m_query) {
114         m_query->exec(QLatin1String("SELECT Name FROM NamespaceTable"));
115         if (m_query->next())
116             m_namespace = m_query->value(0).toString();
117     }
118     return m_namespace;
119 }
120 
virtualFolder() const121 QString QHelpDBReader::virtualFolder() const
122 {
123     if (m_query) {
124         m_query->exec(QLatin1String("SELECT Name FROM FolderTable WHERE Id=1"));
125         if (m_query->next())
126             return m_query->value(0).toString();
127     }
128     return QString();
129 }
130 
version() const131 QString QHelpDBReader::version() const
132 {
133     const QString versionString = metaData(QLatin1String("version")).toString();
134     if (versionString.isEmpty())
135         return qtVersionHeuristic();
136     return versionString;
137 }
138 
qtVersionHeuristic() const139 QString QHelpDBReader::qtVersionHeuristic() const
140 {
141     const QString nameSpace = namespaceName();
142     if (!nameSpace.startsWith(QLatin1String("org.qt-project.")))
143         return QString();
144 
145     // We take the namespace tail, starting from the last letter in namespace name.
146     // We drop any non digit characters.
147     const QChar dot(QLatin1Char('.'));
148     QString tail;
149     for (int i = nameSpace.count(); i > 0; --i) {
150         const QChar c = nameSpace.at(i - 1);
151         if (c.isDigit() || c == dot)
152             tail.prepend(c);
153 
154         if (c.isLetter())
155             break;
156     }
157 
158     if (!tail.startsWith(dot) && tail.count(dot) == 1) {
159         // The org.qt-project.qtquickcontrols2.5120 case,
160         // tail = 2.5120 here. We need to cut "2." here.
161         const int dotIndex = tail.indexOf(dot);
162         if (dotIndex > 0)
163             tail = tail.mid(dotIndex);
164     }
165 
166     // Drop beginning dots
167     while (tail.startsWith(dot))
168         tail = tail.mid(1);
169 
170     // Drop ending dots
171     while (tail.endsWith(dot))
172         tail.chop(1);
173 
174     if (tail.count(dot) == 0) {
175         if (tail.count() > 5)
176             return tail;
177 
178         // When we have 3 digits, we split it like: ABC -> A.B.C
179         // When we have 4 digits, we split it like: ABCD -> A.BC.D
180         // When we have 5 digits, we split it like: ABCDE -> A.BC.DE
181         const int major = tail.left(1).toInt();
182         const int minor = tail.count() == 3
183                 ? tail.mid(1, 1).toInt() : tail.mid(1, 2).toInt();
184         const int patch = tail.count() == 5
185                 ? tail.right(2).toInt() : tail.right(1).toInt();
186 
187         return QString::fromUtf8("%1.%2.%3").arg(major).arg(minor).arg(patch);
188     }
189 
190     return tail;
191 }
192 
isAttributeUsed(QSqlQuery * query,const QString & tableName,int attributeId)193 static bool isAttributeUsed(QSqlQuery *query, const QString &tableName, int attributeId)
194 {
195     query->prepare(QString::fromLatin1("SELECT FilterAttributeId "
196                      "FROM %1 "
197                      "WHERE FilterAttributeId = ? "
198                      "LIMIT 1").arg(tableName));
199     query->bindValue(0, attributeId);
200     query->exec();
201     return query->next(); // if we got a result it means it was used
202 }
203 
filterDataCount(QSqlQuery * query,const QString & tableName)204 static int filterDataCount(QSqlQuery *query, const QString &tableName)
205 {
206     query->exec(QString::fromLatin1("SELECT COUNT(*) FROM"
207               "(SELECT DISTINCT * FROM %1)").arg(tableName));
208     query->next();
209     return query->value(0).toInt();
210 }
211 
indexTable() const212 QHelpDBReader::IndexTable QHelpDBReader::indexTable() const
213 {
214     IndexTable table;
215     if (!m_query)
216         return table;
217 
218     QMap<int, QString> attributeIds;
219     m_query->exec(QLatin1String("SELECT DISTINCT Id, Name FROM FilterAttributeTable ORDER BY Id"));
220     while (m_query->next())
221         attributeIds.insert(m_query->value(0).toInt(), m_query->value(1).toString());
222 
223     // Maybe some are unused and specified erroneously in the named filter only,
224     // like it was in case of qtlocation.qch <= qt 5.9
225     QVector<int> usedAttributeIds;
226     for (auto it = attributeIds.cbegin(), end = attributeIds.cend(); it != end; ++it) {
227         const int attributeId = it.key();
228         if (isAttributeUsed(m_query, QLatin1String("IndexFilterTable"), attributeId)
229                 || isAttributeUsed(m_query, QLatin1String("ContentsFilterTable"), attributeId)
230                 || isAttributeUsed(m_query, QLatin1String("FileFilterTable"), attributeId)) {
231             usedAttributeIds.append(attributeId);
232         }
233     }
234 
235     bool legacy = false;
236     m_query->exec(QLatin1String("SELECT * FROM pragma_table_info('IndexTable') "
237                                 "WHERE name='ContextName'"));
238     if (m_query->next())
239         legacy = true;
240 
241     const QString identifierColumnName = legacy
242             ? QLatin1String("ContextName")
243             : QLatin1String("Identifier");
244 
245     const int usedAttributeCount = usedAttributeIds.count();
246 
247     QMap<int, IndexItem> idToIndexItem;
248 
249     m_query->exec(QString::fromLatin1("SELECT Name, %1, FileId, Anchor, Id "
250                                       "FROM IndexTable "
251                                       "ORDER BY Id").arg(identifierColumnName));
252     while (m_query->next()) {
253         IndexItem indexItem;
254         indexItem.name       = m_query->value(0).toString();
255         indexItem.identifier = m_query->value(1).toString();
256         indexItem.fileId     = m_query->value(2).toInt();
257         indexItem.anchor     = m_query->value(3).toString();
258         const int indexId    = m_query->value(4).toInt();
259 
260         idToIndexItem.insert(indexId, indexItem);
261     }
262 
263     QMap<int, FileItem> idToFileItem;
264     QMap<int, int> originalFileIdToNewFileId;
265 
266     int filesCount = 0;
267     m_query->exec(QLatin1String("SELECT "
268                                     "FileNameTable.FileId, "
269                                     "FileNameTable.Name, "
270                                     "FileNameTable.Title "
271                                 "FROM FileNameTable, FolderTable "
272                                 "WHERE FileNameTable.FolderId = FolderTable.Id "
273                                 "ORDER BY FileId"));
274     while (m_query->next()) {
275         const int fileId = m_query->value(0).toInt();
276         FileItem fileItem;
277         fileItem.name   = m_query->value(1).toString();
278         fileItem.title  = m_query->value(2).toString();
279 
280         idToFileItem.insert(fileId, fileItem);
281         originalFileIdToNewFileId.insert(fileId, filesCount);
282         ++filesCount;
283     }
284 
285     QMap<int, ContentsItem> idToContentsItem;
286 
287     m_query->exec(QLatin1String("SELECT Data, Id "
288                                 "FROM ContentsTable "
289                                 "ORDER BY Id"));
290     while (m_query->next()) {
291         ContentsItem contentsItem;
292         contentsItem.data    = m_query->value(0).toByteArray();
293         const int contentsId = m_query->value(1).toInt();
294 
295         idToContentsItem.insert(contentsId, contentsItem);
296     }
297 
298     bool optimized = true;
299 
300     if (usedAttributeCount) {
301         // May optimize only when all usedAttributes are attached to every
302         // index and file. It means the number of rows in the
303         // IndexTable multiplied by number of used attributes
304         // must equal the number of rows inside IndexFilterTable
305         // (yes, we have a combinatorial explosion of data in IndexFilterTable,
306         // which we want to optimize). The same with FileNameTable and
307         // FileFilterTable.
308 
309         const bool mayOptimizeIndexTable
310                 = filterDataCount(m_query, QLatin1String("IndexFilterTable"))
311                 == idToIndexItem.count() * usedAttributeCount;
312         const bool mayOptimizeFileTable
313                 = filterDataCount(m_query, QLatin1String("FileFilterTable"))
314                 == idToFileItem.count() * usedAttributeCount;
315         const bool mayOptimizeContentsTable
316                 = filterDataCount(m_query, QLatin1String("ContentsFilterTable"))
317                 == idToContentsItem.count() * usedAttributeCount;
318         optimized = mayOptimizeIndexTable && mayOptimizeFileTable && mayOptimizeContentsTable;
319 
320         if (!optimized) {
321             m_query->exec(QLatin1String(
322                               "SELECT "
323                                   "IndexFilterTable.IndexId, "
324                                   "FilterAttributeTable.Name "
325                               "FROM "
326                                   "IndexFilterTable, "
327                                   "FilterAttributeTable "
328                               "WHERE "
329                                   "IndexFilterTable.FilterAttributeId = FilterAttributeTable.Id"));
330             while (m_query->next()) {
331                 const int indexId = m_query->value(0).toInt();
332                 auto it = idToIndexItem.find(indexId);
333                 if (it != idToIndexItem.end())
334                     it.value().filterAttributes.append(m_query->value(1).toString());
335             }
336 
337             m_query->exec(QLatin1String(
338                               "SELECT "
339                                   "FileFilterTable.FileId, "
340                                   "FilterAttributeTable.Name "
341                               "FROM "
342                                   "FileFilterTable, "
343                                   "FilterAttributeTable "
344                               "WHERE "
345                                   "FileFilterTable.FilterAttributeId = FilterAttributeTable.Id"));
346             while (m_query->next()) {
347                 const int fileId = m_query->value(0).toInt();
348                 auto it = idToFileItem.find(fileId);
349                 if (it != idToFileItem.end())
350                     it.value().filterAttributes.append(m_query->value(1).toString());
351             }
352 
353             m_query->exec(QLatin1String(
354                               "SELECT "
355                                   "ContentsFilterTable.ContentsId, "
356                                   "FilterAttributeTable.Name "
357                               "FROM "
358                                   "ContentsFilterTable, "
359                                   "FilterAttributeTable "
360                               "WHERE "
361                                   "ContentsFilterTable.FilterAttributeId = FilterAttributeTable.Id"));
362             while (m_query->next()) {
363                 const int contentsId = m_query->value(0).toInt();
364                 auto it = idToContentsItem.find(contentsId);
365                 if (it != idToContentsItem.end())
366                     it.value().filterAttributes.append(m_query->value(1).toString());
367             }
368         }
369     }
370 
371     // reindex fileId references
372     for (auto it = idToIndexItem.cbegin(), end = idToIndexItem.cend(); it != end; ++it) {
373         IndexItem item = it.value();
374         item.fileId = originalFileIdToNewFileId.value(item.fileId);
375         table.indexItems.append(item);
376     }
377 
378     table.fileItems = idToFileItem.values();
379     table.contentsItems = idToContentsItem.values();
380 
381     if (optimized) {
382         for (int attributeId : usedAttributeIds)
383             table.usedFilterAttributes.append(attributeIds.value(attributeId));
384     }
385 
386     return table;
387 }
388 
filterAttributeSets() const389 QList<QStringList> QHelpDBReader::filterAttributeSets() const
390 {
391     QList<QStringList> result;
392     if (m_query) {
393         m_query->exec(QLatin1String(
394                   "SELECT "
395                       "FileAttributeSetTable.Id, "
396                       "FilterAttributeTable.Name "
397                   "FROM "
398                       "FileAttributeSetTable, "
399                       "FilterAttributeTable "
400                   "WHERE FileAttributeSetTable.FilterAttributeId = FilterAttributeTable.Id "
401                   "ORDER BY FileAttributeSetTable.Id"));
402         int oldId = -1;
403         while (m_query->next()) {
404             const int id = m_query->value(0).toInt();
405             if (id != oldId) {
406                 result.append(QStringList());
407                 oldId = id;
408             }
409             result.last().append(m_query->value(1).toString());
410         }
411     }
412     return result;
413 }
414 
fileData(const QString & virtualFolder,const QString & filePath) const415 QByteArray QHelpDBReader::fileData(const QString &virtualFolder,
416                                    const QString &filePath) const
417 {
418     QByteArray ba;
419     if (virtualFolder.isEmpty() || filePath.isEmpty() || !m_query)
420         return ba;
421 
422     namespaceName();
423     m_query->prepare(QLatin1String(
424                     "SELECT "
425                         "FileDataTable.Data "
426                     "FROM "
427                         "FileDataTable, "
428                         "FileNameTable, "
429                         "FolderTable, "
430                         "NamespaceTable "
431                     "WHERE FileDataTable.Id = FileNameTable.FileId "
432                     "AND (FileNameTable.Name = ? OR FileNameTable.Name = ?) "
433                     "AND FileNameTable.FolderId = FolderTable.Id "
434                     "AND FolderTable.Name = ? "
435                     "AND FolderTable.NamespaceId = NamespaceTable.Id "
436                     "AND NamespaceTable.Name = ?"));
437     m_query->bindValue(0, filePath);
438     m_query->bindValue(1, QString(QLatin1String("./") + filePath));
439     m_query->bindValue(2, virtualFolder);
440     m_query->bindValue(3, m_namespace);
441     m_query->exec();
442     if (m_query->next() && m_query->isValid())
443         ba = qUncompress(m_query->value(0).toByteArray());
444     return ba;
445 }
446 
customFilters() const447 QStringList QHelpDBReader::customFilters() const
448 {
449     QStringList lst;
450     if (m_query) {
451         m_query->exec(QLatin1String("SELECT Name FROM FilterNameTable"));
452         while (m_query->next())
453             lst.append(m_query->value(0).toString());
454     }
455     return lst;
456 }
457 
filterAttributes(const QString & filterName) const458 QStringList QHelpDBReader::filterAttributes(const QString &filterName) const
459 {
460     QStringList lst;
461     if (m_query) {
462         if (filterName.isEmpty()) {
463             m_query->prepare(QLatin1String("SELECT Name FROM FilterAttributeTable"));
464         } else {
465             m_query->prepare(QLatin1String(
466                      "SELECT "
467                          "FilterAttributeTable.Name "
468                      "FROM "
469                          "FilterAttributeTable, "
470                          "FilterTable, "
471                          "FilterNameTable "
472                      "WHERE FilterNameTable.Name = ? "
473                     "AND FilterNameTable.Id = FilterTable.NameId "
474                     "AND FilterTable.FilterAttributeId = FilterAttributeTable.Id"));
475             m_query->bindValue(0, filterName);
476         }
477         m_query->exec();
478         while (m_query->next())
479             lst.append(m_query->value(0).toString());
480     }
481     return lst;
482 }
483 
filesData(const QStringList & filterAttributes,const QString & extensionFilter) const484 QMap<QString, QByteArray> QHelpDBReader::filesData(
485         const QStringList &filterAttributes,
486         const QString &extensionFilter) const
487 {
488     QMap<QString, QByteArray> result;
489     if (!m_query)
490         return result;
491 
492     QString query;
493     QString extension;
494     if (!extensionFilter.isEmpty())
495         extension = QString(QLatin1String("AND FileNameTable.Name "
496                                           "LIKE \'%.%1\'")).arg(extensionFilter);
497 
498     if (filterAttributes.isEmpty()) {
499         query = QString(QLatin1String("SELECT "
500                                           "FileNameTable.Name, "
501                                           "FileDataTable.Data "
502                                       "FROM "
503                                           "FolderTable, "
504                                           "FileNameTable, "
505                                           "FileDataTable "
506                                       "WHERE FileDataTable.Id = FileNameTable.FileId "
507                                       "AND FileNameTable.FolderId = FolderTable.Id %1"))
508             .arg(extension);
509     } else {
510         for (int i = 0; i < filterAttributes.count(); ++i) {
511             if (i > 0)
512                 query.append(QLatin1String(" INTERSECT "));
513             query.append(QString(QLatin1String(
514                                      "SELECT "
515                                          "FileNameTable.Name, "
516                                          "FileDataTable.Data "
517                                      "FROM "
518                                          "FolderTable, "
519                                          "FileNameTable, "
520                                          "FileDataTable, "
521                                          "FileFilterTable, "
522                                          "FilterAttributeTable "
523                                      "WHERE FileDataTable.Id = FileNameTable.FileId "
524                                      "AND FileNameTable.FolderId = FolderTable.Id "
525                                      "AND FileNameTable.FileId = FileFilterTable.FileId "
526                                      "AND FileFilterTable.FilterAttributeId = FilterAttributeTable.Id "
527                                      "AND FilterAttributeTable.Name = \'%1\' %2"))
528                          .arg(quote(filterAttributes.at(i)))
529                          .arg(extension));
530         }
531     }
532     m_query->exec(query);
533     while (m_query->next())
534         result.insert(m_query->value(0).toString(), qUncompress(m_query->value(1).toByteArray()));
535 
536     return result;
537 }
538 
metaData(const QString & name) const539 QVariant QHelpDBReader::metaData(const QString &name) const
540 {
541     QVariant v;
542     if (!m_query)
543         return v;
544 
545     m_query->prepare(QLatin1String("SELECT COUNT(Value), Value FROM MetaDataTable "
546         "WHERE Name=?"));
547     m_query->bindValue(0, name);
548     if (m_query->exec() && m_query->next()
549         && m_query->value(0).toInt() == 1)
550         v = m_query->value(1);
551     return v;
552 }
553 
quote(const QString & string) const554 QString QHelpDBReader::quote(const QString &string) const
555 {
556     QString s = string;
557     s.replace(QLatin1Char('\''), QLatin1String("\'\'"));
558     return s;
559 }
560 
561 QT_END_NAMESPACE
562