Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qhelpsearchindexwriter.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
6#include "qhelp_global.h"
9
10#include <QtTools/qttools-config.h>
11#include <QtCore/qdatastream.h>
12#include <QtCore/qdatetime.h>
13#include <QtCore/qdir.h>
14#include <QtCore/qstringconverter.h>
15#include <QtCore/qtextstream.h>
16#include <QtCore/qurl.h>
17#include <QtCore/qvariant.h>
18#if QT_CONFIG(fullqthelp)
19# include <QtGui/qtextdocument.h>
20#endif
21#include <QtSql/qsqldatabase.h>
22#include <QtSql/qsqldriver.h>
23#include <QtSql/qsqlerror.h>
24#include <QtSql/qsqlquery.h>
25
27
28using namespace Qt::StringLiterals;
29
30namespace fulltextsearch {
31
32const char FTS_DB_NAME[] = "fts";
33
34class Writer
35{
36public:
37 Writer(const QString &path);
39
40 bool tryInit(bool reindex);
41 void flush();
42
43 void removeNamespace(const QString &namespaceName);
44 bool hasNamespace(const QString &namespaceName);
45 void insertDoc(const QString &namespaceName,
46 const QString &attributes,
47 const QString &url,
48 const QString &title,
49 const QString &contents);
52
53private:
54 void init(bool reindex);
55 bool hasDB();
56
57 const QString m_dbDir;
58 QString m_uniqueId;
59
60 bool m_needOptimize = false;
61 QSqlDatabase m_db;
62 QVariantList m_namespaces;
63 QVariantList m_attributes;
64 QVariantList m_urls;
65 QVariantList m_titles;
66 QVariantList m_contents;
67};
68
69Writer::Writer(const QString &path)
70 : m_dbDir(path)
71{
72 QDir().mkpath(m_dbDir);
73 m_uniqueId = QHelpGlobal::uniquifyConnectionName("QHelpWriter"_L1, this);
74 m_db = QSqlDatabase::addDatabase("QSQLITE"_L1, m_uniqueId);
75 const QString dbPath = m_dbDir + u'/' + QLatin1StringView(FTS_DB_NAME);
76 m_db.setDatabaseName(dbPath);
77 if (!m_db.open()) {
78 const QString &error = QHelpSearchIndexWriter::tr(
79 "Cannot open database \"%1\" using connection \"%2\": %3")
80 .arg(dbPath, m_uniqueId, m_db.lastError().text());
81 qWarning("%s", qUtf8Printable(error));
82 m_db = {};
83 QSqlDatabase::removeDatabase(m_uniqueId);
84 m_uniqueId.clear();
85 } else {
87 }
88}
89
90bool Writer::tryInit(bool reindex)
91{
92 if (!m_db.isValid())
93 return true;
94
95 QSqlQuery query(m_db);
96 // HACK: we try to perform any modifying command just to check if
97 // we don't get SQLITE_BUSY code (SQLITE_BUSY is defined to 5 in sqlite driver)
98 if (!query.exec("CREATE TABLE foo ();"_L1) && query.lastError().nativeErrorCode() == "5"_L1) // db is locked
99 return false;
100
101 // HACK: clear what we have created
102 query.exec("DROP TABLE foo;"_L1);
103
104 init(reindex);
105 return true;
106}
107
108bool Writer::hasDB()
109{
110 if (!m_db.isValid())
111 return false;
112
113 QSqlQuery query(m_db);
114 query.prepare("SELECT id FROM info LIMIT 1"_L1);
115 query.exec();
116 return query.next();
117}
118
119void Writer::init(bool reindex)
120{
121 if (!m_db.isValid())
122 return;
123
124 QSqlQuery query(m_db);
125
126 if (reindex && hasDB()) {
127 m_needOptimize = true;
128
129 query.exec("DROP TABLE titles;"_L1);
130 query.exec("DROP TABLE contents;"_L1);
131 query.exec("DROP TABLE info;"_L1);
132 }
133
134 query.exec("CREATE TABLE info (id INTEGER PRIMARY KEY, namespace, attributes, url, title, data);"_L1);
135
136 query.exec("CREATE VIRTUAL TABLE titles USING fts5("
137 "namespace UNINDEXED, attributes UNINDEXED, "
138 "url UNINDEXED, title, "
139 "tokenize = 'porter unicode61', content = 'info', content_rowid='id');"_L1);
140 query.exec("CREATE TRIGGER titles_insert AFTER INSERT ON info BEGIN "
141 "INSERT INTO titles(rowid, namespace, attributes, url, title) "
142 "VALUES(new.id, new.namespace, new.attributes, new.url, new.title); "
143 "END;"_L1);
144 query.exec("CREATE TRIGGER titles_delete AFTER DELETE ON info BEGIN "
145 "INSERT INTO titles(titles, rowid, namespace, attributes, url, title) "
146 "VALUES('delete', old.id, old.namespace, old.attributes, old.url, old.title); "
147 "END;"_L1);
148 query.exec("CREATE TRIGGER titles_update AFTER UPDATE ON info BEGIN "
149 "INSERT INTO titles(titles, rowid, namespace, attributes, url, title) "
150 "VALUES('delete', old.id, old.namespace, old.attributes, old.url, old.title); "
151 "INSERT INTO titles(rowid, namespace, attributes, url, title) "
152 "VALUES(new.id, new.namespace, new.attributes, new.url, new.title); "
153 "END;"_L1);
154
155 query.exec("CREATE VIRTUAL TABLE contents USING fts5("
156 "namespace UNINDEXED, attributes UNINDEXED, "
157 "url UNINDEXED, title, data, "
158 "tokenize = 'porter unicode61', content = 'info', content_rowid='id');"_L1);
159 query.exec("CREATE TRIGGER contents_insert AFTER INSERT ON info BEGIN "
160 "INSERT INTO contents(rowid, namespace, attributes, url, title, data) "
161 "VALUES(new.id, new.namespace, new.attributes, new.url, new.title, new.data); "
162 "END;"_L1);
163 query.exec("CREATE TRIGGER contents_delete AFTER DELETE ON info BEGIN "
164 "INSERT INTO contents(contents, rowid, namespace, attributes, url, title, data) "
165 "VALUES('delete', old.id, old.namespace, old.attributes, old.url, old.title, old.data); "
166 "END;"_L1);
167 query.exec("CREATE TRIGGER contents_update AFTER UPDATE ON info BEGIN "
168 "INSERT INTO contents(contents, rowid, namespace, attributes, url, title, data) "
169 "VALUES('delete', old.id, old.namespace, old.attributes, old.url, old.title, old.data); "
170 "INSERT INTO contents(rowid, namespace, attributes, url, title, data) "
171 "VALUES(new.id, new.namespace, new.attributes, new.url, new.title, new.data); "
172 "END;"_L1);
173}
174
176{
177 if (m_db.isValid())
178 m_db.close();
179 m_db = {};
180 if (!m_uniqueId.isEmpty())
181 QSqlDatabase::removeDatabase(m_uniqueId);
182}
183
185{
186 if (!m_db.isValid())
187 return;
188
189 QSqlQuery query(m_db);
190 query.prepare("INSERT INTO info (namespace, attributes, url, title, data) VALUES (?, ?, ?, ?, ?)"_L1);
191 query.addBindValue(m_namespaces);
192 query.addBindValue(m_attributes);
193 query.addBindValue(m_urls);
194 query.addBindValue(m_titles);
195 query.addBindValue(m_contents);
196 query.execBatch();
197
198 m_namespaces.clear();
199 m_attributes.clear();
200 m_urls.clear();
201 m_titles.clear();
202 m_contents.clear();
203}
204
205void Writer::removeNamespace(const QString &namespaceName)
206{
207 if (!m_db.isValid() || !hasNamespace(namespaceName)) // no data to delete
208 return;
209
210 m_needOptimize = true;
211 QSqlQuery query(m_db);
212 query.prepare("DELETE FROM info WHERE namespace = ?"_L1);
213 query.addBindValue(namespaceName);
214 query.exec();
215}
216
217bool Writer::hasNamespace(const QString &namespaceName)
218{
219 if (!m_db.isValid())
220 return false;
221
222 QSqlQuery query(m_db);
223 query.prepare("SELECT id FROM info WHERE namespace = ? LIMIT 1"_L1);
224 query.addBindValue(namespaceName);
225 query.exec();
226 return query.next();
227}
228
229void Writer::insertDoc(const QString &namespaceName,
230 const QString &attributes,
231 const QString &url,
232 const QString &title,
233 const QString &contents)
234{
235 m_namespaces.append(namespaceName);
236 m_attributes.append(attributes);
237 m_urls.append(url);
238 m_titles.append(title);
239 m_contents.append(contents);
240}
241
243{
244 if (!m_db.isValid())
245 return;
246
247 m_needOptimize = false;
248 if (m_db.driver()->hasFeature(QSqlDriver::Transactions))
249 m_db.transaction();
250}
251
253{
254 if (!m_db.isValid())
255 return;
256
257 QSqlQuery query(m_db);
258
259 if (m_needOptimize) {
260 query.exec("INSERT INTO titles(titles) VALUES('rebuild')"_L1);
261 query.exec("INSERT INTO contents(contents) VALUES('rebuild')"_L1);
262 }
263
264 if (m_db.driver()->hasFeature(QSqlDriver::Transactions))
265 m_db.commit();
266
267 if (m_needOptimize)
268 query.exec("VACUUM"_L1);
269}
270
271QHelpSearchIndexWriter::~QHelpSearchIndexWriter()
272{
273 m_mutex.lock();
274 this->m_cancel = true;
275 m_mutex.unlock();
276 wait();
277}
278
280{
281 QMutexLocker lock(&m_mutex);
282 m_cancel = true;
283}
284
285void QHelpSearchIndexWriter::updateIndex(const QString &collectionFile,
286 const QString &indexFilesFolder, bool reindex)
287{
288 wait();
289 QMutexLocker lock(&m_mutex);
290
291 m_cancel = false;
292 m_reindex = reindex;
293 m_collectionFile = collectionFile;
294 m_indexFilesFolder = indexFilesFolder;
295
296 lock.unlock();
297
298 start(QThread::LowestPriority);
299}
300
301static const char IndexedNamespacesKey[] = "FTS5IndexedNamespaces";
302
303static QMap<QString, QDateTime> readIndexMap(const QHelpEngineCore &engine)
304{
305 QMap<QString, QDateTime> indexMap;
306 QDataStream dataStream(
307 engine.customValue(QLatin1StringView(IndexedNamespacesKey)).toByteArray());
308 dataStream >> indexMap;
309 return indexMap;
310}
311
319
324
326{
327 QMutexLocker lock(&m_mutex);
328
329 if (m_cancel)
330 return;
331
332 const bool reindex(m_reindex);
333 const QString collectionFile(m_collectionFile);
334 const QString indexPath(m_indexFilesFolder);
335
336 lock.unlock();
337
338 QHelpEngineCore engine(collectionFile, nullptr);
339 if (!engine.setupData())
340 return;
341
342 if (reindex)
343 clearIndexMap(&engine);
344
345 emit indexingStarted();
346
347 Writer writer(indexPath);
348
349 while (!writer.tryInit(reindex)) {
350 lock.relock();
351 if (m_cancel) {
352 emit indexingFinished();
353 return;
354 }
355 lock.unlock();
356 sleep(1);
357 }
358
359 const QStringList &registeredDocs = engine.registeredDocumentations();
360 QMap<QString, QDateTime> indexMap = readIndexMap(engine);
361
362 if (!reindex) {
363 for (const QString &namespaceName : registeredDocs) {
364 const auto it = indexMap.constFind(namespaceName);
365 if (it != indexMap.constEnd()) {
366 const QString path = engine.documentationFileName(namespaceName);
367 if (*it < QFileInfo(path).lastModified()) {
368 // Remove some outdated indexed stuff
369 indexMap.erase(it);
370 writer.removeNamespace(namespaceName);
371 } else if (!writer.hasNamespace(namespaceName)) {
372 // No data in fts db for namespace.
373 // The namespace could have been removed from fts db
374 // or the whole fts db have been removed
375 // without removing it from indexMap.
376 indexMap.erase(it);
377 }
378 } else {
379 // Needed in case namespaceName was removed from indexMap
380 // without removing it from fts db.
381 // May happen when e.g. qch file was removed manually
382 // without removing fts db.
383 writer.removeNamespace(namespaceName);
384 }
385 // TODO: we may also detect if there are any other data
386 // and remove it
387 }
388 } else {
389 indexMap.clear();
390 }
391
392 auto it = indexMap.begin();
393 while (it != indexMap.end()) {
394 if (!registeredDocs.contains(it.key())) {
395 writer.removeNamespace(it.key());
396 it = indexMap.erase(it);
397 } else {
398 ++it;
399 }
400 }
401
402 for (const QString &namespaceName : registeredDocs) {
403 lock.relock();
404 if (m_cancel) {
405 // store what we have done so far
406 writeIndexMap(&engine, indexMap);
407 writer.endTransaction();
408 emit indexingFinished();
409 return;
410 }
411 lock.unlock();
412
413 // if indexed, continue
414 if (indexMap.contains(namespaceName))
415 continue;
416
417 const QString fileName = engine.documentationFileName(namespaceName);
418 QHelpDBReader reader(fileName, QHelpGlobal::uniquifyConnectionName(
419 fileName, this), nullptr);
420 if (!reader.init())
421 continue;
422
423 const QString virtualFolder = reader.virtualFolder();
424
425 const QList<QStringList> &attributeSets =
426 engine.filterAttributeSets(namespaceName);
427
428 for (const QStringList &attributes : attributeSets) {
429 const QString &attributesString = attributes.join(u'|');
430
431 bool canceled = false;
432 reader.filesData(attributes, {"html"_L1, "htm"_L1, "txt"_L1},
433 [&](const QString &file, const QByteArray &data) {
434 lock.relock();
435 if (m_cancel) {
436 canceled = true;
437 return false;
438 }
439 lock.unlock();
440
441 if (data.isEmpty())
442 return true;
443
444 QUrl url;
445 url.setScheme("qthelp"_L1);
446 url.setAuthority(namespaceName);
447 url.setPath(u'/' + virtualFolder + u'/' + file);
448
449 if (url.hasFragment())
450 url.setFragment({});
451
452 const QString &fullFileName = url.toString();
453 if (!fullFileName.endsWith(".html"_L1) && !fullFileName.endsWith(".htm"_L1)
454 && !fullFileName.endsWith(".txt"_L1)) {
455 return true;
456 }
457
458 QTextStream s(data);
459 auto encoding = QStringDecoder::encodingForHtml(data);
460 if (encoding)
461 s.setEncoding(*encoding);
462
463 const QString &text = s.readAll();
464 if (text.isEmpty())
465 return true;
466
467 QString title;
468 QString contents;
469 if (fullFileName.endsWith(".txt"_L1)) {
470 title = fullFileName.mid(fullFileName.lastIndexOf(u'/') + 1);
471 contents = text.toHtmlEscaped();
472 } else {
473#if QT_CONFIG(fullqthelp) && QT_CONFIG(texthtmlparser)
474 QTextDocument doc;
475 doc.setHtml(text);
476
477 title = doc.metaInformation(QTextDocument::DocumentTitle).toHtmlEscaped();
478 contents = doc.toPlainText().toHtmlEscaped();
479#else
480 return;
481#endif
482 }
483
484 writer.insertDoc(namespaceName, attributesString, fullFileName, title, contents);
485 return true;
486 });
487
488 if (canceled) {
489 // store what we have done so far
490 writeIndexMap(&engine, indexMap);
491 writer.endTransaction();
492 emit indexingFinished();
493 return;
494 }
495 }
496 writer.flush();
497 const QString &path = engine.documentationFileName(namespaceName);
498 indexMap.insert(namespaceName, QFileInfo(path).lastModified());
499 }
500
501 writeIndexMap(&engine, indexMap);
502
503 writer.endTransaction();
504 emit indexingFinished();
505}
506
507} // namespace fulltextsearch
508
509QT_END_NAMESPACE
The QHelpEngineCore class provides the core functionality of the help system.
void updateIndex(const QString &collectionFile, const QString &indexFilesFolder, bool reindex)
void removeNamespace(const QString &namespaceName)
void insertDoc(const QString &namespaceName, const QString &attributes, const QString &url, const QString &title, const QString &contents)
bool hasNamespace(const QString &namespaceName)
Combined button and popup list for selecting options.
static bool writeIndexMap(QHelpEngineCore *engine, const QMap< QString, QDateTime > &indexMap)
static const char IndexedNamespacesKey[]
static bool clearIndexMap(QHelpEngineCore *engine)
static QMap< QString, QDateTime > readIndexMap(const QHelpEngineCore &engine)