Revision control

++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
++ // Qt-Security score:insignificant reason:build-tool
++
++ #include "helpgenerator.h"
++ #include "qhelpprojectdata_p.h"
++ #include <qhelp_global.h>
++
++ #include <QtCore/QtMath>
++ #include <QtCore/QMap>
++ #include <QtCore/QFile>
++ #include <QtCore/QFileInfo>
++ #include <QtCore/QDir>
++ #include <QtCore/QDebug>
++ #include <QtCore/QRegularExpression>
++ #include <QtCore/QSet>
++ #include <QtCore/QMap>
++ #include <QtCore/QVariant>
++ #include <QtCore/QDateTime>
++ #include <QtCore/QStringConverter>
++ #include <QtCore/QDataStream>
++ #include <QtSql/QSqlQuery>
++
++ #include <stdio.h>
++
++ QT_BEGIN_NAMESPACE
++
++ class HelpGeneratorPrivate : public QObject
++ {
++ Q_OBJECT
++
++ public:
++ HelpGeneratorPrivate(QObject *parent = nullptr) : QObject(parent) {}
++
++ bool generate(QHelpProjectData *helpData,
++ const QString &outputFileName);
++ bool checkLinks(const QHelpProjectData &helpData);
++ QString error() const;
++
++ Q_SIGNALS:
++ void statusChanged(const QString &msg);
++ void progressChanged(double progress);
++ void warning(const QString &msg);
++
++ private:
++ struct FileNameTableData
++ {
++ QString name;
++ int fileId;
++ QString title;
++ };
++
++ void writeTree(QDataStream &s, QHelpDataContentItem *item, int depth);
++ bool createTables();
++ bool insertFileNotFoundFile();
++ bool registerCustomFilter(const QString &filterName,
++ const QStringList &filterAttribs, bool forceUpdate = false);
++ bool registerVirtualFolder(const QString &folderName, const QString &ns);
++ bool insertFilterAttributes(const QStringList &attributes);
++ bool insertKeywords(const QList<QHelpDataIndexItem> &keywords,
++ const QStringList &filterAttributes);
++ bool insertFiles(const QStringList &files, const QString &rootPath,
++ const QStringList &filterAttributes);
++ bool insertContents(const QByteArray &ba,
++ const QStringList &filterAttributes);
++ bool insertMetaData(const QMap<QString, QVariant> &metaData);
++ void cleanupDB();
++ void setupProgress(QHelpProjectData *helpData);
++ void addProgress(double step);
++
++ QString m_error;
++ QSqlQuery *m_query = nullptr;
++
++ int m_namespaceId = -1;
++ int m_virtualFolderId = -1;
++
++ QMap<QString, int> m_fileMap;
++ QMap<int, QSet<int> > m_fileFilterMap;
++
++ double m_progress;
++ double m_oldProgress;
++ double m_contentStep;
++ double m_fileStep;
++ double m_indexStep;
++ };
++
++ /*!
++ Takes the \a helpData and generates a new documentation
++ set from it. The Qt compressed help file is written to \a
++ outputFileName. Returns true on success, otherwise false.
++ */
++ bool HelpGeneratorPrivate::generate(QHelpProjectData *helpData,
++ const QString &outputFileName)
++ {
++ emit progressChanged(0);
++ m_error.clear();
++ if (!helpData || helpData->namespaceName().isEmpty()) {
++ m_error = tr("Invalid help data.");
++ return false;
++ }
++
++ QString outFileName = outputFileName;
++ if (outFileName.isEmpty()) {
++ m_error = tr("No output file name specified.");
++ return false;
++ }
++
++ QFileInfo fi(outFileName);
++ if (fi.exists()) {
++ if (!fi.dir().remove(fi.fileName())) {
++ m_error = tr("The file %1 cannot be overwritten.").arg(outFileName);
++ return false;
++ }
++ }
++
++ setupProgress(helpData);
++
++ emit statusChanged(tr("Building up file structure..."));
++ bool openingOk = true;
++ {
++ QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), QLatin1String("builder"));
++ db.setDatabaseName(outFileName);
++ openingOk = db.open();
++ if (openingOk)
++ m_query = new QSqlQuery(db);
++ }
++
++ if (!openingOk) {
++ m_error = tr("Cannot open data base file %1.").arg(outFileName);
++ cleanupDB();
++ return false;
++ }
++
++ m_query->exec(QLatin1String("PRAGMA synchronous=OFF"));
++ m_query->exec(QLatin1String("PRAGMA cache_size=3000"));
++
++ addProgress(1.0);
++ createTables();
++ insertFileNotFoundFile();
++ insertMetaData(helpData->metaData());
++
++ if (!registerVirtualFolder(helpData->virtualFolder(), helpData->namespaceName())) {
++ m_error = tr("Cannot register namespace \"%1\".").arg(helpData->namespaceName());
++ cleanupDB();
++ return false;
++ }
++ addProgress(1.0);
++
++ emit statusChanged(tr("Insert custom filters..."));
++ for (const QHelpDataCustomFilter &f : helpData->customFilters()) {
++ if (!registerCustomFilter(f.name, f.filterAttributes, true)) {
++ cleanupDB();
++ return false;
++ }
++ }
++ addProgress(1.0);
++
++ int i = 1;
++ for (const QHelpDataFilterSection &fs : helpData->filterSections()) {
++ emit statusChanged(tr("Insert help data for filter section (%1 of %2)...")
++ .arg(i++).arg(helpData->filterSections().size()));
++ insertFilterAttributes(fs.filterAttributes());
++ QByteArray ba;
++ QDataStream s(&ba, QIODevice::WriteOnly);
++ for (QHelpDataContentItem *itm : fs.contents())
++ writeTree(s, itm, 0);
++ if (!insertFiles(fs.files(), helpData->rootPath(), fs.filterAttributes())
++ || !insertContents(ba, fs.filterAttributes())
++ || !insertKeywords(fs.indices(), fs.filterAttributes())) {
++ cleanupDB();
++ return false;
++ }
++ }
++
++ cleanupDB();
++ emit progressChanged(100);
++ emit statusChanged(tr("Documentation successfully generated."));
++ return true;
++ }
++
++ void HelpGeneratorPrivate::setupProgress(QHelpProjectData *helpData)
++ {
++ m_progress = 0;
++ m_oldProgress = 0;
++
++ int numberOfFiles = 0;
++ int numberOfIndices = 0;
++ for (const QHelpDataFilterSection &fs : helpData->filterSections()) {
++ numberOfFiles += fs.files().size();
++ numberOfIndices += fs.indices().size();
++ }
++ // init 2%
++ // filters 1%
++ // contents 10%
++ // files 60%
++ // indices 27%
++ m_contentStep = 10.0 / qMax(helpData->customFilters().size(), 1);
++ m_fileStep = 60.0 / qMax(numberOfFiles, 1);
++ m_indexStep = 27.0 / qMax(numberOfIndices, 1);
++ }
++
++ void HelpGeneratorPrivate::addProgress(double step)
++ {
++ m_progress += step;
++ if ((m_progress - m_oldProgress) >= 1.0 && m_progress <= 100.0) {
++ m_oldProgress = m_progress;
++ emit progressChanged(qCeil(m_progress));
++ }
++ }
++
++ void HelpGeneratorPrivate::cleanupDB()
++ {
++ if (m_query) {
++ m_query->clear();
++ delete m_query;
++ m_query = nullptr;
++ }
++ QSqlDatabase::removeDatabase(QLatin1String("builder"));
++ }
++
++ void HelpGeneratorPrivate::writeTree(QDataStream &s, QHelpDataContentItem *item, int depth)
++ {
++ s << depth;
++ s << item->reference();
++ s << item->title();
++ for (QHelpDataContentItem *i : item->children())
++ writeTree(s, i, depth + 1);
++ }
++
++ /*!
++ Returns the last error message.
++ */
++ QString HelpGeneratorPrivate::error() const
++ {
++ return m_error;
++ }
++
++ bool HelpGeneratorPrivate::createTables()
++ {
++ if (!m_query)
++ return false;
++
++ m_query->exec(QLatin1String("SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\'"
++ "AND Name=\'NamespaceTable\'"));
++ m_query->next();
++ if (m_query->value(0).toInt() > 0) {
++ m_error = tr("Some tables already exist.");
++ return false;
++ }
++
++ const QStringList tables = QStringList()
++ << QLatin1String("CREATE TABLE NamespaceTable ("
++ "Id INTEGER PRIMARY KEY,"
++ "Name TEXT )")
++ << QLatin1String("CREATE TABLE FilterAttributeTable ("
++ "Id INTEGER PRIMARY KEY, "
++ "Name TEXT )")
++ << QLatin1String("CREATE TABLE FilterNameTable ("
++ "Id INTEGER PRIMARY KEY, "
++ "Name TEXT )")
++ << QLatin1String("CREATE TABLE FilterTable ("
++ "NameId INTEGER, "
++ "FilterAttributeId INTEGER )")
++ << QLatin1String("CREATE TABLE IndexTable ("
++ "Id INTEGER PRIMARY KEY, "
++ "Name TEXT, "
++ "Identifier TEXT, "
++ "NamespaceId INTEGER, "
++ "FileId INTEGER, "
++ "Anchor TEXT )")
++ << QLatin1String("CREATE TABLE IndexFilterTable ("
++ "FilterAttributeId INTEGER, "
++ "IndexId INTEGER )")
++ << QLatin1String("CREATE TABLE ContentsTable ("
++ "Id INTEGER PRIMARY KEY, "
++ "NamespaceId INTEGER, "
++ "Data BLOB )")
++ << QLatin1String("CREATE TABLE ContentsFilterTable ("
++ "FilterAttributeId INTEGER, "
++ "ContentsId INTEGER )")
++ << QLatin1String("CREATE TABLE FileAttributeSetTable ("
++ "Id INTEGER, "
++ "FilterAttributeId INTEGER )")
++ << QLatin1String("CREATE TABLE FileDataTable ("
++ "Id INTEGER PRIMARY KEY, "
++ "Data BLOB )")
++ << QLatin1String("CREATE TABLE FileFilterTable ("
++ "FilterAttributeId INTEGER, "
++ "FileId INTEGER )")
++ << QLatin1String("CREATE TABLE FileNameTable ("
++ "FolderId INTEGER, "
++ "Name TEXT, "
++ "FileId INTEGER, "
++ "Title TEXT )")
++ << QLatin1String("CREATE TABLE FolderTable("
++ "Id INTEGER PRIMARY KEY, "
++ "Name Text, "
++ "NamespaceID INTEGER )")
++ << QLatin1String("CREATE TABLE MetaDataTable("
++ "Name Text, "
++ "Value BLOB )");
++
++ for (const QString &q : tables) {
++ if (!m_query->exec(q)) {
++ m_error = tr("Cannot create tables.");
++ return false;
++ }
++ }
++
++ m_query->exec(QLatin1String("INSERT INTO MetaDataTable VALUES('qchVersion', '1.0')"));
++
++ return true;
++ }
++
++ bool HelpGeneratorPrivate::insertFileNotFoundFile()
++ {
++ if (!m_query)
++ return false;
++
++ m_query->exec(QLatin1String("SELECT id FROM FileNameTable WHERE Name=\'\'"));
++ if (m_query->next() && m_query->isValid())
++ return true;
++
++ m_query->prepare(QLatin1String("INSERT INTO FileDataTable VALUES (Null, ?)"));
++ m_query->bindValue(0, QByteArray());
++ if (!m_query->exec())
++ return false;
++
++ const int fileId = m_query->lastInsertId().toInt();
++ m_query->prepare(QLatin1String("INSERT INTO FileNameTable (FolderId, Name, FileId, Title) "
++ " VALUES (0, '', ?, '')"));
++ m_query->bindValue(0, fileId);
++ if (fileId > -1 && m_query->exec()) {
++ m_fileMap.insert({}, fileId);
++ return true;
++ }
++ return false;
++ }
++
++ bool HelpGeneratorPrivate::registerVirtualFolder(const QString &folderName, const QString &ns)
++ {
++ if (!m_query || folderName.isEmpty() || ns.isEmpty())
++ return false;
++
++ m_query->prepare(QLatin1String("SELECT Id FROM FolderTable WHERE Name=?"));
++ m_query->bindValue(0, folderName);
++ m_query->exec();
++ m_query->next();
++ if (m_query->isValid() && m_query->value(0).toInt() > 0)
++ return true;
++
++ m_namespaceId = -1;
++ m_query->prepare(QLatin1String("SELECT Id FROM NamespaceTable WHERE Name=?"));
++ m_query->bindValue(0, ns);
++ m_query->exec();
++ while (m_query->next()) {
++ m_namespaceId = m_query->value(0).toInt();
++ break;
++ }
++
++ if (m_namespaceId < 0) {
++ m_query->prepare(QLatin1String("INSERT INTO NamespaceTable VALUES(NULL, ?)"));
++ m_query->bindValue(0, ns);
++ if (m_query->exec())
++ m_namespaceId = m_query->lastInsertId().toInt();
++ }
++
++ if (m_namespaceId > 0) {
++ m_query->prepare(QLatin1String("SELECT Id FROM FolderTable WHERE Name=?"));
++ m_query->bindValue(0, folderName);
++ m_query->exec();
++ while (m_query->next())
++ m_virtualFolderId = m_query->value(0).toInt();
++
++ if (m_virtualFolderId > 0)
++ return true;
++
++ m_query->prepare(QLatin1String("INSERT INTO FolderTable (NamespaceId, Name) "
++ "VALUES (?, ?)"));
++ m_query->bindValue(0, m_namespaceId);
++ m_query->bindValue(1, folderName);
++ if (m_query->exec()) {
++ m_virtualFolderId = m_query->lastInsertId().toInt();
++ return m_virtualFolderId > 0;
++ }
++ }
++ m_error = tr("Cannot register virtual folder.");
++ return false;
++ }
++
++ bool HelpGeneratorPrivate::insertFiles(const QStringList &files, const QString &rootPath,
++ const QStringList &filterAttributes)
++ {
++ if (!m_query)
++ return false;
++
++ emit statusChanged(tr("Insert files..."));
++ QSet<int> filterAtts;
++ for (const QString &filterAtt : filterAttributes) {
++ m_query->prepare(QLatin1String("SELECT Id FROM FilterAttributeTable "
++ "WHERE Name=?"));
++ m_query->bindValue(0, filterAtt);
++ m_query->exec();
++ if (m_query->next())
++ filterAtts.insert(m_query->value(0).toInt());
++ }
++
++ int filterSetId = -1;
++ m_query->exec(QLatin1String("SELECT MAX(Id) FROM FileAttributeSetTable"));
++ if (m_query->next())
++ filterSetId = m_query->value(0).toInt();
++ if (filterSetId < 0)
++ return false;
++ ++filterSetId;
++ QList<int> attValues = filterAtts.values();
++ std::sort(attValues.begin(), attValues.end());
++ for (int attId : std::as_const(attValues)) {
++ m_query->prepare(QLatin1String("INSERT INTO FileAttributeSetTable "
++ "VALUES(?, ?)"));
++ m_query->bindValue(0, filterSetId);
++ m_query->bindValue(1, attId);
++ m_query->exec();
++ }
++
++ int tableFileId = 1;
++ m_query->exec(QLatin1String("SELECT MAX(Id) FROM FileDataTable"));
++ if (m_query->next())
++ tableFileId = m_query->value(0).toInt() + 1;
++
++ QString title;
++ QString charSet;
++ QList<QByteArray> fileDataList;
++ QMap<int, QSet<int> > tmpFileFilterMap;
++ QList<FileNameTableData> fileNameDataList;
++
++ int i = 0;
++ for (const QString &file : files) {
++ const QString fileName = QDir::cleanPath(file);
++
++ QFile fi(rootPath + QDir::separator() + fileName);
++ if (!fi.exists()) {
++ emit warning(tr("The file %1 does not exist, skipping it...")
++ .arg(QDir::cleanPath(rootPath + QDir::separator() + fileName)));
++ continue;
++ }
++
++ if (!fi.open(QIODevice::ReadOnly)) {
++ emit warning(tr("Cannot open file %1, skipping it...")
++ .arg(QDir::cleanPath(rootPath + QDir::separator() + fileName)));
++ continue;
++ }
++
++ QByteArray data = fi.readAll();
++ if (fileName.endsWith(QLatin1String(".html"))
++ || fileName.endsWith(QLatin1String(".htm"))) {
++ auto encoding = QStringDecoder::encodingForHtml(data);
++ if (!encoding)
++ encoding = QStringDecoder::Utf8;
++ title = QHelpGlobal::documentTitle(QStringDecoder(*encoding)(data));
++ } else {
++ title = fileName.mid(fileName.lastIndexOf(QLatin1Char('/')) + 1);
++ }
++
++ int fileId = -1;
++ const auto &it = m_fileMap.constFind(fileName);
++ if (it == m_fileMap.cend()) {
++ fileDataList.append(qCompress(data));
++
++ FileNameTableData fileNameData;
++ fileNameData.name = fileName;
++ fileNameData.fileId = tableFileId;
++ fileNameData.title = title;
++ fileNameDataList.append(fileNameData);
++
++ m_fileMap.insert(fileName, tableFileId);
++ m_fileFilterMap.insert(tableFileId, filterAtts);
++ tmpFileFilterMap.insert(tableFileId, filterAtts);
++
++ ++tableFileId;
++ } else {
++ fileId = it.value();
++ QSet<int> &fileFilterSet = m_fileFilterMap[fileId];
++ QSet<int> &tmpFileFilterSet = tmpFileFilterMap[fileId];
++ for (int filter : std::as_const(filterAtts)) {
++ if (!fileFilterSet.contains(filter)
++ && !tmpFileFilterSet.contains(filter)) {
++ fileFilterSet.insert(filter);
++ tmpFileFilterSet.insert(filter);
++ }
++ }
++ }
++ }
++
++ if (!tmpFileFilterMap.isEmpty()) {
++ m_query->exec(QLatin1String("BEGIN"));
++ for (auto it = tmpFileFilterMap.cbegin(), end = tmpFileFilterMap.cend(); it != end; ++it) {
++ QList<int> filterValues = it.value().values();
++ std::sort(filterValues.begin(), filterValues.end());
++ for (int fv : std::as_const(filterValues)) {
++ m_query->prepare(QLatin1String("INSERT INTO FileFilterTable "
++ "VALUES(?, ?)"));
++ m_query->bindValue(0, fv);
++ m_query->bindValue(1, it.key());
++ m_query->exec();
++ }
++ }
++
++ for (const QByteArray &fileData : std::as_const(fileDataList)) {
++ m_query->prepare(QLatin1String("INSERT INTO FileDataTable VALUES "
++ "(Null, ?)"));
++ m_query->bindValue(0, fileData);
++ m_query->exec();
++ if (++i % 20 == 0)
++ addProgress(m_fileStep * 20.0);
++ }
++
++ for (const FileNameTableData &fnd : std::as_const(fileNameDataList)) {
++ m_query->prepare(QLatin1String("INSERT INTO FileNameTable "
++ "(FolderId, Name, FileId, Title) VALUES (?, ?, ?, ?)"));
++ m_query->bindValue(0, 1);
++ m_query->bindValue(1, fnd.name);
++ m_query->bindValue(2, fnd.fileId);
++ m_query->bindValue(3, fnd.title);
++ m_query->exec();
++ }
++ m_query->exec(QLatin1String("COMMIT"));
++ }
++
++ m_query->exec(QLatin1String("SELECT MAX(Id) FROM FileDataTable"));
++ if (m_query->next()
++ && m_query->value(0).toInt() == tableFileId - 1) {
++ addProgress(m_fileStep*(i % 20));
++ return true;
++ }
++ return false;
++ }
++
++ bool HelpGeneratorPrivate::registerCustomFilter(const QString &filterName,
++ const QStringList &filterAttribs, bool forceUpdate)
++ {
++ if (!m_query)
++ return false;
++
++ m_query->exec(QLatin1String("SELECT Id, Name FROM FilterAttributeTable"));
++ QStringList idsToInsert = filterAttribs;
++ QMap<QString, int> attributeMap;
++ while (m_query->next()) {
++ attributeMap.insert(m_query->value(1).toString(),
++ m_query->value(0).toInt());
++ idsToInsert.removeAll(m_query->value(1).toString());
++ }
++
++ for (const QString &id : std::as_const(idsToInsert)) {
++ m_query->prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)"));
++ m_query->bindValue(0, id);
++ m_query->exec();
++ attributeMap.insert(id, m_query->lastInsertId().toInt());
++ }
++
++ int nameId = -1;
++ m_query->prepare(QLatin1String("SELECT Id FROM FilterNameTable WHERE Name=?"));
++ m_query->bindValue(0, filterName);
++ m_query->exec();
++ while (m_query->next()) {
++ nameId = m_query->value(0).toInt();
++ break;
++ }
++
++ if (nameId < 0) {
++ m_query->prepare(QLatin1String("INSERT INTO FilterNameTable VALUES(NULL, ?)"));
++ m_query->bindValue(0, filterName);
++ if (m_query->exec())
++ nameId = m_query->lastInsertId().toInt();
++ } else if (!forceUpdate) {
++ m_error = tr("The filter %1 is already registered.").arg(filterName);
++ return false;
++ }
++
++ if (nameId < 0) {
++ m_error = tr("Cannot register filter %1.").arg(filterName);
++ return false;
++ }
++
++ m_query->prepare(QLatin1String("DELETE FROM FilterTable WHERE NameId=?"));
++ m_query->bindValue(0, nameId);
++ m_query->exec();
++
++ for (const QString &att : filterAttribs) {
++ m_query->prepare(QLatin1String("INSERT INTO FilterTable VALUES(?, ?)"));
++ m_query->bindValue(0, nameId);
++ m_query->bindValue(1, attributeMap[att]);
++ if (!m_query->exec())
++ return false;
++ }
++ return true;
++ }
++
++ bool HelpGeneratorPrivate::insertKeywords(const QList<QHelpDataIndexItem> &keywords,
++ const QStringList &filterAttributes)
++ {
++ if (!m_query)
++ return false;
++
++ emit statusChanged(tr("Insert indices..."));
++ int indexId = 1;
++ m_query->exec(QLatin1String("SELECT MAX(Id) FROM IndexTable"));
++ if (m_query->next())
++ indexId = m_query->value(0).toInt() + 1;
++
++ QList<int> filterAtts;
++ for (const QString &filterAtt : filterAttributes) {
++ m_query->prepare(QLatin1String("SELECT Id FROM FilterAttributeTable WHERE Name=?"));
++ m_query->bindValue(0, filterAtt);
++ m_query->exec();
++ if (m_query->next())
++ filterAtts.append(m_query->value(0).toInt());
++ }
++
++ QList<int> indexFilterTable;
++
++ int i = 0;
++ m_query->exec(QLatin1String("BEGIN"));
++ QSet<QString> indices;
++ for (const QHelpDataIndexItem &itm : keywords) {
++ // Identical ids make no sense and just confuse the Assistant user,
++ // so we ignore all repetitions.
++ if (indices.contains(itm.identifier))
++ continue;
++
++ // Still empty ids should be ignored, as otherwise we will include only
++ // the first keyword with an empty id.
++ if (!itm.identifier.isEmpty())
++ indices.insert(itm.identifier);
++
++ const int pos = itm.reference.indexOf(QLatin1Char('#'));
++ const QString &fileName = itm.reference.left(pos);
++ const QString anchor = pos < 0 ? QString() : itm.reference.mid(pos + 1);
++
++ const QString &fName = QDir::cleanPath(fileName);
++
++ const auto &it = m_fileMap.constFind(fName);
++ const int fileId = it == m_fileMap.cend() ? 1 : it.value();
++
++ m_query->prepare(QLatin1String("INSERT INTO IndexTable (Name, Identifier, NamespaceId, FileId, Anchor) "
++ "VALUES(?, ?, ?, ?, ?)"));
++ m_query->bindValue(0, itm.name);
++ m_query->bindValue(1, itm.identifier);
++ m_query->bindValue(2, m_namespaceId);
++ m_query->bindValue(3, fileId);
++ m_query->bindValue(4, anchor);
++ m_query->exec();
++
++ indexFilterTable.append(indexId++);
++ if (++i % 100 == 0)
++ addProgress(m_indexStep * 100.0);
++ }
++ m_query->exec(QLatin1String("COMMIT"));
++
++ m_query->exec(QLatin1String("BEGIN"));
++ for (int idx : std::as_const(indexFilterTable)) {
++ for (int a : std::as_const(filterAtts)) {
++ m_query->prepare(QLatin1String("INSERT INTO IndexFilterTable (FilterAttributeId, IndexId) "
++ "VALUES(?, ?)"));
++ m_query->bindValue(0, a);
++ m_query->bindValue(1, idx);
++ m_query->exec();
++ }
++ }
++ m_query->exec(QLatin1String("COMMIT"));
++
++ m_query->exec(QLatin1String("SELECT COUNT(Id) FROM IndexTable"));
++ if (m_query->next() && m_query->value(0).toInt() >= indices.size())
++ return true;
++ return false;
++ }
++
++ bool HelpGeneratorPrivate::insertContents(const QByteArray &ba,
++ const QStringList &filterAttributes)
++ {
++ if (!m_query)
++ return false;
++
++ emit statusChanged(tr("Insert contents..."));
++ m_query->prepare(QLatin1String("INSERT INTO ContentsTable (NamespaceId, Data) "
++ "VALUES(?, ?)"));
++ m_query->bindValue(0, m_namespaceId);
++ m_query->bindValue(1, ba);
++ m_query->exec();
++ int contentId = m_query->lastInsertId().toInt();
++ if (contentId < 1) {
++ m_error = tr("Cannot insert contents.");
++ return false;
++ }
++
++ // associate the filter attributes
++ for (const QString &filterAtt : filterAttributes) {
++ m_query->prepare(QLatin1String("INSERT INTO ContentsFilterTable (FilterAttributeId, ContentsId) "
++ "SELECT Id, ? FROM FilterAttributeTable WHERE Name=?"));
++ m_query->bindValue(0, contentId);
++ m_query->bindValue(1, filterAtt);
++ m_query->exec();
++ if (!m_query->isActive()) {
++ m_error = tr("Cannot register contents.");
++ return false;
++ }
++ }
++ addProgress(m_contentStep);
++ return true;
++ }
++
++ bool HelpGeneratorPrivate::insertFilterAttributes(const QStringList &attributes)
++ {
++ if (!m_query)
++ return false;
++
++ m_query->exec(QLatin1String("SELECT Name FROM FilterAttributeTable"));
++ QSet<QString> atts;
++ while (m_query->next())
++ atts.insert(m_query->value(0).toString());
++
++ for (const QString &s : attributes) {
++ if (!atts.contains(s)) {
++ m_query->prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)"));
++ m_query->bindValue(0, s);
++ m_query->exec();
++ }
++ }
++ return true;
++ }
++
++ bool HelpGeneratorPrivate::insertMetaData(const QMap<QString, QVariant> &metaData)
++ {
++ if (!m_query)
++ return false;
++
++ for (auto it = metaData.cbegin(), end = metaData.cend(); it != end; ++it) {
++ m_query->prepare(QLatin1String("INSERT INTO MetaDataTable VALUES(?, ?)"));
++ m_query->bindValue(0, it.key());
++ m_query->bindValue(1, it.value());
++ m_query->exec();
++ }
++ return true;
++ }
++
++ bool HelpGeneratorPrivate::checkLinks(const QHelpProjectData &helpData)
++ {
++ /*
++ * Step 1: Gather the canoncal file paths of all files in the project.
++ * We use a set, because there will be a lot of look-ups.
++ */
++ QSet<QString> files;
++ for (const QHelpDataFilterSection &filterSection : helpData.filterSections()) {
++ for (const QString &file : filterSection.files()) {
++ const QFileInfo fileInfo(helpData.rootPath() + QDir::separator() + file);
++ const QString &canonicalFileName = fileInfo.canonicalFilePath();
++ if (!fileInfo.exists())
++ emit warning(tr("File \"%1\" does not exist.").arg(file));
++ else
++ files.insert(canonicalFileName);
++ }
++ }
++
++ /*
++ * Step 2: Check the hypertext and image references of all HTML files.
++ * Note that we don't parse the files, but simply grep for the
++ * respective HTML elements. Therefore. contents that are e.g.
++ * commented out can cause false warning.
++ */
++ bool allLinksOk = true;
++ for (const QString &fileName : std::as_const(files)) {
++ if (!fileName.endsWith(QLatin1String("html"))
++ && !fileName.endsWith(QLatin1String("htm")))
++ continue;
++ QFile htmlFile(fileName);
++ if (!htmlFile.open(QIODevice::ReadOnly)) {
++ emit warning(tr("File \"%1\" cannot be opened.").arg(fileName));
++ continue;
++ }
++ const QRegularExpression linkPattern(QLatin1String("<(?:a href|img src)=\"?([^#\">]+)[#\">]"));
++ QByteArray data = htmlFile.readAll();
++ auto encoding = QStringDecoder::encodingForHtml(data);
++ if (!encoding)
++ encoding = QStringDecoder::Utf8;
++ const QString &content = QStringDecoder(*encoding)(data);
++ QStringList invalidLinks;
++ QRegularExpressionMatch match;
++ int pos = 0;
++ while ((match = linkPattern.match(content, pos)).hasMatch()) {
++ pos = match.capturedEnd();
++ const QString &linkedFileName = match.captured(1);
++ if (linkedFileName.contains(QLatin1String("://")))
++ continue;
++ const QString &curDir = QFileInfo(fileName).dir().path();
++ const QString &canonicalLinkedFileName =
++ QFileInfo(curDir + QDir::separator() + linkedFileName).canonicalFilePath();
++ if (!files.contains(canonicalLinkedFileName)
++ && !invalidLinks.contains(canonicalLinkedFileName)) {
++ emit warning(tr("File \"%1\" contains an invalid link to file \"%2\"").
++ arg(fileName).arg(linkedFileName));
++ allLinksOk = false;
++ invalidLinks.append(canonicalLinkedFileName);
++ }
++ }
++ }
++
++ if (!allLinksOk)
++ m_error = tr("Invalid links in HTML files.");
++ return allLinksOk;
++ }
++
++ //////////////////////////////
++
++ HelpGenerator::HelpGenerator(bool silent)
++ {
++ m_private = new HelpGeneratorPrivate(this);
++ if (!silent) {
++ connect(m_private, &HelpGeneratorPrivate::statusChanged,
++ this, &HelpGenerator::printStatus);
++ }
++ connect(m_private, &HelpGeneratorPrivate::warning,
++ this, &HelpGenerator::printWarning);
++ }
++
++ bool HelpGenerator::generate(QHelpProjectData *helpData,
++ const QString &outputFileName)
++ {
++ return m_private->generate(helpData, outputFileName);
++ }
++
++ bool HelpGenerator::checkLinks(const QHelpProjectData &helpData)
++ {
++ return m_private->checkLinks(helpData);
++ }
++
++ QString HelpGenerator::error() const
++ {
++ return m_private->error();
++ }
++
++ void HelpGenerator::printStatus(const QString &msg)
++ {
++ puts(qPrintable(msg));
++ }
++
++ void HelpGenerator::printWarning(const QString &msg)
++ {
++ puts(qPrintable(tr("Warning: %1").arg(msg)));
++ }
++
++ QT_END_NAMESPACE
++
++ #include "helpgenerator.moc"