1 /* Copyright (C) 2005 J.F.Dockes
2  *   This program is free software; you can redistribute it and/or modify
3  *   it under the terms of the GNU General Public License as published by
4  *   the Free Software Foundation; either version 2 of the License, or
5  *   (at your option) any later version.
6  *
7  *   This program is distributed in the hope that it will be useful,
8  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
9  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  *   GNU General Public License for more details.
11  *
12  *   You should have received a copy of the GNU General Public License
13  *   along with this program; if not, write to the
14  *   Free Software Foundation, Inc.,
15  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16  */
17 #include "autoconfig.h"
18 
19 #include "safeunistd.h"
20 
21 #include <list>
22 
23 #include <QMessageBox>
24 
25 #include "qxtconfirmationmessage.h"
26 
27 #include "log.h"
28 #include "fileudi.h"
29 #include "execmd.h"
30 #include "transcode.h"
31 #include "docseqhist.h"
32 #include "docseqdb.h"
33 #include "internfile.h"
34 #include "rclmain_w.h"
35 #include "rclzg.h"
36 #include "pathut.h"
37 
38 using namespace std;
39 
40 // Browser list used if xdg-open fails for opening the help doc
41 static const vector<string> browser_list{
42     "opera", "google-chrome", "chromium-browser",
43     "palemoon", "iceweasel", "firefox", "konqueror", "epiphany"};
44 
45 
46 // Start native viewer or preview for input Doc. This is used to allow
47 // using recoll from another app (e.g. Unity Scope) to view embedded
48 // result docs (docs with an ipath). . We act as a proxy to extract
49 // the data and start a viewer.  The Url are encoded as
50 // file://path#ipath
viewUrl()51 void RclMain::viewUrl()
52 {
53     if (m_urltoview.isEmpty() || !rcldb)
54         return;
55 
56     QUrl qurl(m_urltoview);
57     LOGDEB("RclMain::viewUrl: Path [" << qs2path(qurl.path()) <<
58            "] fragment [" << qs2path(qurl.fragment()) << "]\n");
59 
60     /* In theory, the url might not be for a file managed by the fs
61        indexer so that the make_udi() call here would be
62        wrong(). When/if this happens we'll have to hide this part
63        inside internfile and have some url magic to indicate the
64        appropriate indexer/identification scheme */
65     string udi;
66     make_udi(qs2path(qurl.path()), qs2path(qurl.fragment()), udi);
67 
68     Rcl::Doc doc;
69     Rcl::Doc idxdoc; // idxdoc.idxi == 0 -> works with base index only
70     if (!rcldb->getDoc(udi, idxdoc, doc) || doc.pc == -1)
71         return;
72 
73     // StartNativeViewer needs a db source to call getEnclosing() on.
74     Rcl::Query *query = new Rcl::Query(rcldb.get());
75     DocSequenceDb *src = new DocSequenceDb(
76         rcldb, std::shared_ptr<Rcl::Query>(query), "",
77         std::shared_ptr<Rcl::SearchData>(new Rcl::SearchData));
78     m_source = std::shared_ptr<DocSequence>(src);
79 
80 
81     // Start a native viewer if the mimetype has one defined, else a
82     // preview.
83     string apptag;
84     doc.getmeta(Rcl::Doc::keyapptg, &apptag);
85     string viewer = theconfig->getMimeViewerDef(doc.mimetype, apptag,
86                                                 prefs.useDesktopOpen);
87     if (viewer.empty()) {
88         startPreview(doc);
89     } else {
90         hide();
91         startNativeViewer(doc);
92         // We have a problem here because xdg-open will exit
93         // immediately after starting the command instead of waiting
94         // for it, so we can't wait either and we don't know when we
95         // can exit (deleting the temp file). As a bad workaround we
96         // sleep some time then exit. The alternative would be to just
97         // prevent the temp file deletion completely, leaving it
98         // around forever. Better to let the user save a copy if he
99         // wants I think.
100         sleep(30);
101         fileExit();
102     }
103 }
104 
105 /* Look for html browser. We make a special effort for html because it's
106  * used for reading help. This is only used if the normal approach
107  * (xdg-open etc.) failed */
lookForHtmlBrowser(string & exefile)108 static bool lookForHtmlBrowser(string &exefile)
109 {
110     const char *path = getenv("PATH");
111     if (path == 0) {
112         path = "/usr/local/bin:/usr/bin:/bin";
113     }
114     // Look for each browser
115     for (const auto& entry : browser_list) {
116         if (ExecCmd::which(entry, exefile, path))
117             return true;
118     }
119     exefile.clear();
120     return false;
121 }
122 
openWith(Rcl::Doc doc,string cmdspec)123 void RclMain::openWith(Rcl::Doc doc, string cmdspec)
124 {
125     LOGDEB("RclMain::openWith: " << cmdspec << "\n");
126 
127     // Split the command line
128     vector<string> lcmd;
129     if (!stringToStrings(cmdspec, lcmd)) {
130         QMessageBox::warning(
131             0, "Recoll", tr("Bad desktop app spec for %1: [%2]\n"
132                             "Please check the desktop file")
133             .arg(u8s2qs(doc.mimetype)).arg(path2qs(cmdspec)));
134         return;
135     }
136 
137     // Look for the command to execute in the exec path and the filters
138     // directory
139     string execname = lcmd.front();
140     lcmd.erase(lcmd.begin());
141     string url = doc.url;
142     string fn = fileurltolocalpath(doc.url);
143 
144     // Try to keep the letters used more or less consistent with the reslist
145     // paragraph format.
146     map<string, string> subs;
147 #ifdef _WIN32
148     path_backslashize(fn);
149 #endif
150     subs["F"] = fn;
151     subs["f"] = fn;
152     subs["U"] = url_encode(url);
153     subs["u"] = url;
154 
155     execViewer(subs, false, execname, lcmd, cmdspec, doc);
156 }
157 
startNativeViewer(Rcl::Doc doc,int pagenum,QString term)158 void RclMain::startNativeViewer(Rcl::Doc doc, int pagenum, QString term)
159 {
160     string apptag;
161     doc.getmeta(Rcl::Doc::keyapptg, &apptag);
162     LOGDEB("RclMain::startNativeViewer: mtype [" << doc.mimetype <<
163            "] apptag ["  << apptag << "] page "  << pagenum << " term ["  <<
164            qs2utf8s(term) << "] url ["  << doc.url << "] ipath [" <<
165            doc.ipath << "]\n");
166 
167     // Look for appropriate viewer
168     string cmdplusattr = theconfig->getMimeViewerDef(doc.mimetype, apptag,
169                                                      prefs.useDesktopOpen);
170     if (cmdplusattr.empty()) {
171         QMessageBox::warning(0, "Recoll",
172                              tr("No external viewer configured for mime type [")
173                              + doc.mimetype.c_str() + "]");
174         return;
175     }
176     LOGDEB("StartNativeViewer: viewerdef from config: " << cmdplusattr << endl);
177 
178     // Separate command string and viewer attributes (if any)
179     ConfSimple viewerattrs;
180     string cmd;
181     theconfig->valueSplitAttributes(cmdplusattr, cmd, viewerattrs);
182     bool ignoreipath = false;
183     int execwflags = 0;
184     if (viewerattrs.get("ignoreipath", cmdplusattr))
185         ignoreipath = stringToBool(cmdplusattr);
186     if (viewerattrs.get("maximize", cmdplusattr)) {
187         if (stringToBool(cmdplusattr)) {
188             execwflags |= ExecCmd::EXF_MAXIMIZED;
189         }
190     }
191 
192     // Split the command line
193     vector<string> lcmd;
194     if (!stringToStrings(cmd, lcmd)) {
195         QMessageBox::warning(
196             0, "Recoll", tr("Bad viewer command line for %1: [%2]\n"
197                             "Please check the mimeview file")
198             .arg(u8s2qs(doc.mimetype)).arg(path2qs(cmd)));
199         return;
200     }
201 
202     // Look for the command to execute in the exec path and the filters
203     // directory
204     string execpath;
205     if (!ExecCmd::which(lcmd.front(), execpath)) {
206         execpath = theconfig->findFilter(lcmd.front());
207         // findFilter returns its input param if the filter is not in
208         // the normal places. As we already looked in the path, we
209         // have no use for a simple command name here (as opposed to
210         // mimehandler which will just let execvp do its thing). Erase
211         // execpath so that the user dialog will be started further
212         // down.
213         if (!execpath.compare(lcmd.front()))
214             execpath.erase();
215 
216         // Specialcase text/html because of the help browser need
217         if (execpath.empty() && !doc.mimetype.compare("text/html") &&
218             apptag.empty()) {
219             if (lookForHtmlBrowser(execpath)) {
220                 lcmd.clear();
221                 lcmd.push_back(execpath);
222                 lcmd.push_back("%u");
223             }
224         }
225     }
226 
227     // Command not found: start the user dialog to help find another one:
228     if (execpath.empty()) {
229         QString mt = QString::fromUtf8(doc.mimetype.c_str());
230         QString message = tr("The viewer specified in mimeview for %1: %2"
231                              " is not found.\nDo you want to start the "
232                              " preferences dialog ?")
233             .arg(mt).arg(path2qs(lcmd.front()));
234 
235         switch(QMessageBox::warning(0, "Recoll", message,
236                                     "Yes", "No", 0, 0, 1)) {
237         case 0:
238             showUIPrefs();
239             if (uiprefs)
240                 uiprefs->showViewAction(mt);
241             break;
242         case 1:
243             break;
244         }
245         // The user will have to click on the link again to try the
246         // new command.
247         return;
248     }
249     // Get rid of the command name. lcmd is now argv[1...n]
250     lcmd.erase(lcmd.begin());
251 
252     // Process the command arguments to determine if we need to create
253     // a temporary file.
254 
255     // If the command has a %i parameter it will manage the
256     // un-embedding. Else if ipath is not empty, we need a temp file.
257     // This can be overridden with the "ignoreipath" attribute
258     bool groksipath = (cmd.find("%i") != string::npos) || ignoreipath;
259 
260     // We used to try being clever here, but actually, the only case
261     // where we don't need a local file copy of the document (or
262     // parent document) is the case of an HTML page with a non-file
263     // URL (http or https). Trying to guess based on %u or %f is
264     // doomed because we pass %u to xdg-open.
265     bool wantsfile = false;
266     bool wantsparentfile = cmd.find("%F") != string::npos;
267     if (!wantsparentfile &&
268         (cmd.find("%f") != string::npos || urlisfileurl(doc.url) ||
269          doc.mimetype.compare("text/html"))) {
270         wantsfile = true;
271     }
272 
273     if (wantsparentfile && !urlisfileurl(doc.url)) {
274         QMessageBox::warning(0, "Recoll",
275                              tr("Viewer command line for %1 specifies "
276                                 "parent file but URL is http[s]: unsupported")
277                              .arg(QString::fromUtf8(doc.mimetype.c_str())));
278         return;
279     }
280     if (wantsfile && wantsparentfile) {
281         QMessageBox::warning(0, "Recoll",
282                              tr("Viewer command line for %1 specifies both "
283                                 "file and parent file value: unsupported")
284                              .arg(QString::fromUtf8(doc.mimetype.c_str())));
285         return;
286     }
287 
288     string url = doc.url;
289     string fn = fileurltolocalpath(doc.url);
290     Rcl::Doc pdoc;
291     if (wantsparentfile) {
292         // We want the path for the parent document. For example to
293         // open the chm file, not the internal page. Note that we just
294         // override the other file name in this case.
295         if (!m_source || !m_source->getEnclosing(doc, pdoc)) {
296             QMessageBox::warning(0, "Recoll",
297                                  tr("Cannot find parent document"));
298             return;
299         }
300         // Override fn with the parent's :
301         fn = fileurltolocalpath(pdoc.url);
302 
303         // If the parent document has an ipath too, we need to create
304         // a temp file even if the command takes an ipath
305         // parameter. We have no viewer which could handle a double
306         // embedding. Will have to change if such a one appears.
307         if (!pdoc.ipath.empty()) {
308             groksipath = false;
309         }
310     }
311 
312     // Can't remember what enterHistory was actually for. Set it to
313     // true always for now
314     bool enterHistory = true;
315     bool istempfile = false;
316 
317     LOGDEB("StartNativeViewer: groksipath " << groksipath << " wantsf " <<
318            wantsfile << " wantsparentf " << wantsparentfile << "\n");
319 
320     // If the command wants a file but this is not a file url, or
321     // there is an ipath that it won't understand, we need a temp file:
322     theconfig->setKeyDir(fn.empty() ? "" : path_getfather(fn));
323     if (((wantsfile || wantsparentfile) && fn.empty()) ||
324         (!groksipath && !doc.ipath.empty()) ) {
325         TempFile temp;
326         Rcl::Doc& thedoc = wantsparentfile ? pdoc : doc;
327         if (!FileInterner::idocToFile(temp, string(), theconfig, thedoc)) {
328             QMessageBox::warning(0, "Recoll",
329                                  tr("Cannot extract document or create "
330                                     "temporary file"));
331             return;
332         }
333         enterHistory = true;
334         istempfile = true;
335         rememberTempFile(temp);
336         fn = temp.filename();
337         url = path_pathtofileurl(fn);
338     }
339 
340     // If using an actual file, check that it exists, and if it is
341     // compressed, we may need an uncompressed version
342     if (!fn.empty() && theconfig->mimeViewerNeedsUncomp(doc.mimetype)) {
343         if (!path_readable(fn)) {
344             QMessageBox::warning(0, "Recoll",
345                                  tr("Can't access file: ") + u8s2qs(fn));
346             return;
347         }
348         TempFile temp;
349         if (FileInterner::isCompressed(fn, theconfig)) {
350             if (!FileInterner::maybeUncompressToTemp(temp, fn, theconfig,doc)) {
351                 QMessageBox::warning(
352                     0, "Recoll", tr("Can't uncompress file: ") + path2qs(fn));
353                 return;
354             }
355         }
356         if (temp.ok()) {
357             istempfile = true;
358             rememberTempFile(temp);
359             fn = temp.filename();
360             url = path_pathtofileurl(fn);
361         }
362     }
363 
364     if (istempfile) {
365         QxtConfirmationMessage confirm(
366             QMessageBox::Warning, "Recoll",
367             tr("Opening a temporary copy. Edits will be lost if you don't save"
368                "<br/>them to a permanent location."),
369             tr("Do not show this warning next time (use GUI preferences "
370                "to restore)."));
371         confirm.setOverrideSettingsKey("Recoll/prefs/showTempFileWarning");
372         confirm.exec();
373         QSettings settings;
374         prefs.showTempFileWarning =
375             settings.value("Recoll/prefs/showTempFileWarning").toInt();
376     }
377 
378     // If we are not called with a page number (which would happen for a call
379     // from the snippets window), see if we can compute a page number anyway.
380     if (pagenum == -1) {
381         pagenum = 1;
382         string lterm;
383         if (m_source)
384             pagenum = m_source->getFirstMatchPage(doc, lterm);
385         if (pagenum == -1)
386             pagenum = 1;
387         else // We get the match term used to compute the page
388             term = QString::fromUtf8(lterm.c_str());
389     }
390     char cpagenum[20];
391     sprintf(cpagenum, "%d", pagenum);
392 
393 
394     // Substitute %xx inside arguments
395     string efftime;
396     if (!doc.dmtime.empty() || !doc.fmtime.empty()) {
397         efftime = doc.dmtime.empty() ? doc.fmtime : doc.dmtime;
398     } else {
399         efftime = "0";
400     }
401     // Try to keep the letters used more or less consistent with the reslist
402     // paragraph format.
403     map<string, string> subs;
404     subs["D"] = efftime;
405 #ifdef _WIN32
406     path_backslashize(fn);
407 #endif
408     subs["f"] = fn;
409     subs["F"] = fn;
410     subs["i"] = FileInterner::getLastIpathElt(doc.ipath);
411     subs["M"] = doc.mimetype;
412     subs["p"] = cpagenum;
413     subs["s"] = (const char*)term.toLocal8Bit();
414     subs["U"] = url_encode(url);
415     subs["u"] = url;
416     // Let %(xx) access all metadata.
417     for (const auto& ent :doc.meta) {
418         subs[ent.first] = ent.second;
419     }
420     execViewer(subs, enterHistory, execpath, lcmd, cmd, doc, execwflags);
421 }
422 
execViewer(const map<string,string> & subs,bool enterHistory,const string & execpath,const vector<string> & _lcmd,const string & cmd,Rcl::Doc doc,int flags)423 void RclMain::execViewer(const map<string, string>& subs, bool enterHistory,
424                          const string& execpath,
425                          const vector<string>& _lcmd, const string& cmd,
426                          Rcl::Doc doc, int flags)
427 {
428     string ncmd;
429     vector<string> lcmd;
430     for (vector<string>::const_iterator it = _lcmd.begin();
431          it != _lcmd.end(); it++) {
432         pcSubst(*it, ncmd, subs);
433         LOGDEB(""  << *it << "->"  << (ncmd) << "\n" );
434         lcmd.push_back(ncmd);
435     }
436 
437     // Also substitute inside the unsplit command line and display
438     // in status bar
439     pcSubst(cmd, ncmd, subs);
440 #ifndef _WIN32
441     ncmd += " &";
442 #endif
443     QStatusBar *stb = statusBar();
444     if (stb) {
445         string prcmd;
446 #ifdef _WIN32
447         prcmd = ncmd;
448 #else
449         string fcharset = theconfig->getDefCharset(true);
450         transcode(ncmd, prcmd, fcharset, "UTF-8");
451 #endif
452         QString msg = tr("Executing: [") +
453             QString::fromUtf8(prcmd.c_str()) + "]";
454         stb->showMessage(msg, 10000);
455     }
456 
457     if (enterHistory)
458         historyEnterDoc(rcldb.get(), g_dynconf, doc);
459 
460     // Do the zeitgeist thing
461     zg_send_event(ZGSEND_OPEN, doc);
462 
463     // We keep pushing back and never deleting. This can't be good...
464     ExecCmd *ecmd = new ExecCmd(ExecCmd::EXF_SHOWWINDOW | flags);
465     m_viewers.push_back(ecmd);
466     ecmd->startExec(execpath, lcmd, false, false);
467 }
468 
startManual()469 void RclMain::startManual()
470 {
471     startManual(string());
472 }
473 
startManual(const string & index)474 void RclMain::startManual(const string& index)
475 {
476     string docdir = path_cat(theconfig->getDatadir(), "doc");
477 
478     // The single page user manual is nicer if we have an index. Else
479     // the webhelp one is nicer if it is present
480     string usermanual = path_cat(docdir, "usermanual.html");
481     string webhelp = path_cat(docdir, "webhelp");
482     webhelp = path_cat(webhelp, "index.html");
483     bool has_wh = path_exists(webhelp);
484 
485     LOGDEB("RclMain::startManual: help index is " <<
486            (index.empty() ? "(null)" : index) << "\n");
487     bool indexempty = index.empty();
488 
489 #ifdef _WIN32
490     // On Windows I could not find any way to pass the fragment through
491     // rclstartw (tried to set text/html as exception with rclstartw %u).
492     // So always start the webhelp
493     indexempty = true;
494 #endif
495 
496     if (!indexempty) {
497         usermanual += "#";
498         usermanual += index;
499     }
500     Rcl::Doc doc;
501     if (has_wh && indexempty) {
502         doc.url = path_pathtofileurl(webhelp);
503     } else {
504         doc.url = path_pathtofileurl(usermanual);
505     }
506     doc.mimetype = "text/html";
507     doc.addmeta(Rcl::Doc::keyapptg, "rclman");
508     startNativeViewer(doc);
509 }
510