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