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:GPL-EXCEPT$
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 General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21 ** included in the packaging of this file. Please review the following
22 ** information to ensure the GNU General Public License requirements will
23 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24 **
25 ** $QT_END_LICENSE$
26 **
27 ****************************************************************************/
28 #include "tracer.h"
29 
30 #include <QtCore/QDir>
31 #include <QtCore/QFileInfo>
32 #include <QtCore/QLibraryInfo>
33 #include <QtCore/QLocale>
34 #include <QtCore/QScopedPointer>
35 #include <QtCore/QStringList>
36 #include <QtCore/QTranslator>
37 #include <QtCore/QUrl>
38 
39 #include <QtWidgets/QApplication>
40 #include <QtGui/QDesktopServices>
41 
42 #include <QtHelp/QHelpEngine>
43 #include <QtHelp/QHelpSearchEngine>
44 
45 #include <QtSql/QSqlDatabase>
46 
47 #if defined(BROWSER_QTWEBKIT)
48 #include <QtGui/QFont>
49 #include <QWebSettings>
50 #endif
51 
52 #include "../shared/collectionconfiguration.h"
53 #include "helpenginewrapper.h"
54 #include "mainwindow.h"
55 #include "cmdlineparser.h"
56 
57 // #define TRACING_REQUESTED
58 // #define DEBUG_TRANSLATIONS
59 
60 QT_USE_NAMESPACE
61 
62 namespace {
63 
64 void
updateLastPagesOnUnregister(QHelpEngineCore & helpEngine,const QString & nsName)65 updateLastPagesOnUnregister(QHelpEngineCore& helpEngine, const QString& nsName)
66 {
67     TRACE_OBJ
68     int lastPage = CollectionConfiguration::lastTabPage(helpEngine);
69     QStringList currentPages = CollectionConfiguration::lastShownPages(helpEngine);
70     if (!currentPages.isEmpty()) {
71         QStringList zoomList = CollectionConfiguration::lastZoomFactors(helpEngine);
72         while (zoomList.count() < currentPages.count())
73             zoomList.append(CollectionConfiguration::DefaultZoomFactor);
74 
75         for (int i = currentPages.count(); --i >= 0;) {
76             if (QUrl(currentPages.at(i)).host() == nsName) {
77                 zoomList.removeAt(i);
78                 currentPages.removeAt(i);
79                 lastPage = (lastPage == (i + 1)) ? 1 : lastPage;
80             }
81         }
82 
83         CollectionConfiguration::setLastShownPages(helpEngine, currentPages);
84         CollectionConfiguration::setLastTabPage(helpEngine, lastPage);
85         CollectionConfiguration::setLastZoomFactors(helpEngine, zoomList);
86     }
87 }
88 
stripNonexistingDocs(QHelpEngineCore & collection)89 void stripNonexistingDocs(QHelpEngineCore& collection)
90 {
91     TRACE_OBJ
92     const QStringList &namespaces = collection.registeredDocumentations();
93     for (const QString &ns : namespaces) {
94         QFileInfo fi(collection.documentationFileName(ns));
95         if (!fi.exists() || !fi.isFile())
96             collection.unregisterDocumentation(ns);
97     }
98 }
99 
indexFilesFolder(const QString & collectionFile)100 QString indexFilesFolder(const QString &collectionFile)
101 {
102     TRACE_OBJ
103     QString indexFilesFolder = QLatin1String(".fulltextsearch");
104     if (!collectionFile.isEmpty()) {
105         QFileInfo fi(collectionFile);
106         indexFilesFolder = QLatin1Char('.') +
107             fi.fileName().left(fi.fileName().lastIndexOf(QLatin1String(".qhc")));
108     }
109     return indexFilesFolder;
110 }
111 
112 /*
113  * Returns the expected absolute file path of the cached collection file
114  * correspondinging to the given collection's file.
115  * It may or may not exist yet.
116  */
constructCachedCollectionFilePath(const QHelpEngineCore & collection)117 QString constructCachedCollectionFilePath(const QHelpEngineCore &collection)
118 {
119     TRACE_OBJ
120     const QString &filePath = collection.collectionFile();
121     const QString &fileName = QFileInfo(filePath).fileName();
122     const QString &cacheDir = CollectionConfiguration::cacheDir(collection);
123     const QString &dir = !cacheDir.isEmpty()
124         && CollectionConfiguration::cacheDirIsRelativeToCollection(collection)
125             ? QFileInfo(filePath).dir().absolutePath()
126                 + QDir::separator() + cacheDir
127             : MainWindow::collectionFileDirectory(false, cacheDir);
128     return dir + QDir::separator() + fileName;
129 }
130 
synchronizeDocs(QHelpEngineCore & collection,QHelpEngineCore & cachedCollection,CmdLineParser & cmd)131 bool synchronizeDocs(QHelpEngineCore &collection,
132                      QHelpEngineCore &cachedCollection,
133                      CmdLineParser &cmd)
134 {
135     TRACE_OBJ
136     const QDateTime &lastCollectionRegisterTime =
137         CollectionConfiguration::lastRegisterTime(collection);
138     if (!lastCollectionRegisterTime.isValid() || lastCollectionRegisterTime
139         < CollectionConfiguration::lastRegisterTime(cachedCollection))
140         return true;
141 
142     const QStringList &docs = collection.registeredDocumentations();
143     const QStringList &cachedDocs = cachedCollection.registeredDocumentations();
144 
145     /*
146      * Ensure that the cached collection contains all docs that
147      * the collection contains.
148      */
149     for (const QString &doc : docs) {
150         if (!cachedDocs.contains(doc)) {
151             const QString &docFile = collection.documentationFileName(doc);
152             if (!cachedCollection.registerDocumentation(docFile)) {
153                 cmd.showMessage(QCoreApplication::translate("Assistant",
154                                     "Error registering documentation file '%1': %2").
155                                 arg(docFile).arg(cachedCollection.error()), true);
156                 return false;
157             }
158         }
159     }
160 
161     CollectionConfiguration::updateLastRegisterTime(cachedCollection);
162 
163     return true;
164 }
165 
removeSearchIndex(const QString & collectionFile)166 bool removeSearchIndex(const QString &collectionFile)
167 {
168     TRACE_OBJ
169     QString path = QFileInfo(collectionFile).path();
170     path += QLatin1Char('/') + indexFilesFolder(collectionFile);
171 
172     QDir dir(path);
173     if (!dir.exists())
174         return false;
175 
176     const QStringList &list = dir.entryList(QDir::Files | QDir::Hidden);
177     for (const QString &item : list)
178         dir.remove(item);
179     return true;
180 }
181 
createApplication(int & argc,char * argv[])182 QCoreApplication* createApplication(int &argc, char *argv[])
183 {
184     TRACE_OBJ
185 #ifndef Q_OS_WIN
186     // Look for arguments that imply command-line mode.
187     const char * cmdModeArgs[] = {
188         "-help", "-register", "-unregister", "-remove-search-index",
189         "-rebuild-search-index"
190     };
191     for (int i = 1; i < argc; ++i) {
192         for (size_t j = 0; j < sizeof cmdModeArgs/sizeof *cmdModeArgs; ++j) {
193             if (strcmp(argv[i], cmdModeArgs[j]) == 0)
194                 return new QCoreApplication(argc, argv);
195         }
196     }
197 #endif
198     QApplication *app = new QApplication(argc, argv);
199     app->connect(app, &QGuiApplication::lastWindowClosed,
200                  &QCoreApplication::quit);
201     return app;
202 }
203 
registerDocumentation(QHelpEngineCore & collection,CmdLineParser & cmd,bool printSuccess)204 bool registerDocumentation(QHelpEngineCore &collection, CmdLineParser &cmd,
205                            bool printSuccess)
206 {
207     TRACE_OBJ
208     if (!collection.registerDocumentation(cmd.helpFile())) {
209         cmd.showMessage(QCoreApplication::translate("Assistant",
210                      "Could not register documentation file\n%1\n\nReason:\n%2")
211                      .arg(cmd.helpFile()).arg(collection.error()), true);
212         return false;
213     }
214     if (printSuccess)
215         cmd.showMessage(QCoreApplication::translate("Assistant",
216                             "Documentation successfully registered."),
217                         false);
218     CollectionConfiguration::updateLastRegisterTime(collection);
219     return true;
220 }
221 
unregisterDocumentation(QHelpEngineCore & collection,const QString & namespaceName,CmdLineParser & cmd,bool printSuccess)222 bool unregisterDocumentation(QHelpEngineCore &collection,
223     const QString &namespaceName, CmdLineParser &cmd, bool printSuccess)
224 {
225     TRACE_OBJ
226     if (!collection.unregisterDocumentation(namespaceName)) {
227         cmd.showMessage(QCoreApplication::translate("Assistant",
228                              "Could not unregister documentation"
229                              " file\n%1\n\nReason:\n%2").
230                         arg(cmd.helpFile()).arg(collection.error()), true);
231         return false;
232     }
233     updateLastPagesOnUnregister(collection, namespaceName);
234     if (printSuccess)
235         cmd.showMessage(QCoreApplication::translate("Assistant",
236                             "Documentation successfully unregistered."),
237                         false);
238     return true;
239 }
240 
setupTranslation(const QString & fileName,const QString & dir)241 void setupTranslation(const QString &fileName, const QString &dir)
242 {
243     QTranslator *translator = new QTranslator(QCoreApplication::instance());
244     if (translator->load(fileName, dir))
245         QCoreApplication::installTranslator(translator);
246 #ifdef DEBUG_TRANSLATIONS
247     else if (!fileName.endsWith(QLatin1String("en_US"))
248              && !fileName.endsWith(QLatin1String("_C"))) {
249         qDebug("Could not load translation file %s in directory %s.",
250                qPrintable(fileName), qPrintable(dir));
251     }
252 #endif
253 }
254 
setupTranslations()255 void setupTranslations()
256 {
257     TRACE_OBJ
258     const QString& locale = QLocale::system().name();
259     const QString &resourceDir
260         = QLibraryInfo::location(QLibraryInfo::TranslationsPath);
261     setupTranslation(QLatin1String("assistant_") + locale, resourceDir);
262     setupTranslation(QLatin1String("qt_") + locale, resourceDir);
263     setupTranslation(QLatin1String("qt_help_") + locale, resourceDir);
264 }
265 
266 } // Anonymous namespace.
267 
268 enum ExitStatus {
269     ExitSuccess = 0,
270     ExitFailure,
271     NoExit
272 };
273 
preliminarySetup(CmdLineParser * cmd)274 static ExitStatus preliminarySetup(CmdLineParser *cmd)
275 {
276     /*
277      * Create the collection objects that we need. We always have the
278      * cached collection file. Depending on whether the user specified
279      * one, we also may have an input collection file.
280      */
281     const QString collectionFile = cmd->collectionFile();
282     const bool collectionFileGiven = !collectionFile.isEmpty();
283     QScopedPointer<QHelpEngineCore> collection;
284     if (collectionFileGiven) {
285         collection.reset(new QHelpEngineCore(collectionFile));
286         collection->setProperty("_q_readonly", QVariant::fromValue<bool>(true));
287         if (!collection->setupData()) {
288             cmd->showMessage(QCoreApplication::translate("Assistant",
289                              "Error reading collection file '%1': %2.")
290                             .arg(collectionFile).arg(collection->error()), true);
291             return ExitFailure;
292         }
293     }
294     const QString &cachedCollectionFile = collectionFileGiven
295             ? constructCachedCollectionFilePath(*collection)
296             : MainWindow::defaultHelpCollectionFileName();
297     if (collectionFileGiven && !QFileInfo(cachedCollectionFile).exists()
298             && !collection->copyCollectionFile(cachedCollectionFile)) {
299         cmd->showMessage(QCoreApplication::translate("Assistant",
300                          "Error creating collection file '%1': %2.")
301                         .arg(cachedCollectionFile).arg(collection->error()), true);
302         return ExitFailure;
303     }
304     QHelpEngineCore cachedCollection(cachedCollectionFile);
305     if (!cachedCollection.setupData()) {
306         cmd->showMessage(QCoreApplication::translate("Assistant",
307                          "Error reading collection file '%1': %2.")
308                         .arg(cachedCollectionFile)
309                         .arg(cachedCollection.error()), true);
310         return ExitFailure;
311     }
312 
313     stripNonexistingDocs(cachedCollection);
314     if (collectionFileGiven) {
315         if (CollectionConfiguration::isNewer(*collection, cachedCollection))
316             CollectionConfiguration::copyConfiguration(*collection,
317                                                        cachedCollection);
318         if (!synchronizeDocs(*collection, cachedCollection, *cmd))
319             return ExitFailure;
320     }
321 
322     if (cmd->registerRequest() != CmdLineParser::None) {
323         const QStringList &cachedDocs =
324                 cachedCollection.registeredDocumentations();
325         const QString &namespaceName =
326                 QHelpEngineCore::namespaceName(cmd->helpFile());
327         if (cmd->registerRequest() == CmdLineParser::Register) {
328             if (collectionFileGiven
329                     && !registerDocumentation(*collection, *cmd, true))
330                 return ExitFailure;
331             if (!cachedDocs.contains(namespaceName)
332                     && !registerDocumentation(cachedCollection, *cmd, !collectionFileGiven))
333                 return ExitFailure;
334             return ExitSuccess;
335         }
336         if (cmd->registerRequest() == CmdLineParser::Unregister) {
337             if (collectionFileGiven
338                     && !unregisterDocumentation(*collection, namespaceName, *cmd, true))
339                 return ExitFailure;
340             if (cachedDocs.contains(namespaceName)
341                     && !unregisterDocumentation(cachedCollection, namespaceName,
342                                                 *cmd, !collectionFileGiven))
343                 return ExitFailure;
344             return ExitSuccess;
345         }
346     }
347 
348     if (cmd->removeSearchIndex()) {
349         return removeSearchIndex(cachedCollectionFile)
350                 ? ExitSuccess : ExitFailure;
351     }
352 
353     if (!QSqlDatabase::isDriverAvailable(QLatin1String("QSQLITE"))) {
354         cmd->showMessage(QCoreApplication::translate("Assistant",
355                          "Cannot load sqlite database driver!"),
356                          true);
357         return ExitFailure;
358     }
359 
360     if (!cmd->currentFilter().isEmpty()) {
361         if (collectionFileGiven)
362             collection->setCurrentFilter(cmd->currentFilter());
363         cachedCollection.setCurrentFilter(cmd->currentFilter());
364     }
365 
366     if (collectionFileGiven)
367         cmd->setCollectionFile(cachedCollectionFile);
368 
369     return NoExit;
370 }
371 
main(int argc,char * argv[])372 int main(int argc, char *argv[])
373 {
374     QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
375     QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
376     QCoreApplication::setAttribute(Qt::AA_DisableWindowContextHelpButton);
377     TRACE_OBJ
378     QScopedPointer<QCoreApplication> a(createApplication(argc, argv));
379 #if QT_CONFIG(library)
380     a->addLibraryPath(a->applicationDirPath() + QLatin1String("/plugins"));
381 #endif
382     setupTranslations();
383 
384 #if defined(BROWSER_QTWEBKIT)
385     if (qobject_cast<QApplication *>(a.data())) {
386         QFont f;
387         f.setStyleHint(QFont::SansSerif);
388         QWebSettings::globalSettings()->setFontFamily(QWebSettings::StandardFont, f.defaultFamily());
389     }
390 #endif // BROWSER_QTWEBKIT
391 
392     // Parse arguments.
393     CmdLineParser cmd(a->arguments());
394     CmdLineParser::Result res = cmd.parse();
395     if (res == CmdLineParser::Help)
396         return 0;
397     else if (res == CmdLineParser::Error)
398         return -1;
399 
400     const ExitStatus status = preliminarySetup(&cmd);
401     switch (status) {
402         case ExitFailure: return EXIT_FAILURE;
403         case ExitSuccess: return EXIT_SUCCESS;
404         default: break;
405     }
406 
407     MainWindow *w = new MainWindow(&cmd);
408     w->show();
409 
410     /*
411      * We need to be careful here: The main window has to be deleted before
412      * the help engine wrapper, which has to be deleted before the
413      * QApplication.
414      */
415     const int retval = a->exec();
416     delete w;
417     HelpEngineWrapper::removeInstance();
418     return retval;
419 }
420