1 /*
2  *  Copyright 2005-2021 Fabrice Colin
3  *
4  *  This program is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation; either version 2 of the License, or
7  *  (at your option) any later version.
8  *
9  *  This program is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program; if not, write to the Free Software
16  *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17  */
18 
19 #include <stdlib.h>
20 #include <signal.h>
21 #include <unistd.h>
22 #include <libintl.h>
23 #include <getopt.h>
24 #include <strings.h>
25 #include <iostream>
26 #include <fstream>
27 #include <giomm/init.h>
28 #include <glibmm.h>
29 #include <glibmm/thread.h>
30 #include <glibmm/ustring.h>
31 #include <glibmm/miscutils.h>
32 #include <glibmm/convert.h>
33 #include "config.h"
34 #include <gtkmm/main.h>
35 
36 #include "NLS.h"
37 #include "FilterFactory.h"
38 #include "Languages.h"
39 #include "MIMEScanner.h"
40 #include "ModuleFactory.h"
41 #include "ActionQueue.h"
42 #include "QueryHistory.h"
43 #include "ViewHistory.h"
44 #include "DownloaderInterface.h"
45 #ifdef HAVE_DBUS
46 #include "DBusIndex.h"
47 #endif
48 #include "PinotSettings.h"
49 #include "MainWindow.h"
50 #include "PrefsWindow.h"
51 
52 #define EXPIRY_PERIOD	(3600 * 24 * 30 * 6)
53 
54 using namespace std;
55 
56 static ofstream g_outputFile;
57 static streambuf *g_coutBuf = NULL;
58 static streambuf *g_cerrBuf = NULL;
59 static streambuf *g_clogBuf = NULL;
60 static struct option g_longOptions[] = {
61 	{"help", 0, 0, 'h'},
62 	{"preferences", 0, 0, 'p'},
63 	{"query", 0, 0, 'q'},
64 	{"version", 0, 0, 'v'},
65 	{0, 0, 0, 0}
66 };
67 
closeAll(void)68 static void closeAll(void)
69 {
70 	clog << "Exiting..." << endl;
71 
72 	// Close everything
73 	ModuleFactory::unloadModules();
74 
75 	// Restore the stream buffers
76 	if (g_coutBuf != NULL)
77 	{
78 		clog.rdbuf(g_coutBuf);
79 	}
80 	if (g_cerrBuf != NULL)
81 	{
82 		clog.rdbuf(g_cerrBuf);
83 	}
84 	if (g_clogBuf != NULL)
85 	{
86 		clog.rdbuf(g_clogBuf);
87 	}
88 	g_outputFile.close();
89 
90 	DownloaderInterface::shutdown();
91 	MIMEScanner::shutdown();
92 }
93 
quitAll(int sigNum)94 static void quitAll(int sigNum)
95 {
96 	clog << "Quitting..." << endl;
97 
98 	Gtk::Main::quit();
99 }
100 
main(int argc,char ** argv)101 int main(int argc, char **argv)
102 {
103 	string prefixDir(PREFIX), queryTerms;
104 	Glib::ustring statusText;
105 	int longOptionIndex = 0;
106 	bool warnAboutVersion = false, prefsMode = false;
107 
108 	// Look at the options
109 	int optionChar = getopt_long(argc, argv, "hpq:v", g_longOptions, &longOptionIndex);
110 	while (optionChar != -1)
111 	{
112 		switch (optionChar)
113 		{
114 			case 'h':
115 				// Help
116 				clog << "pinot - Desktop search and metasearch\n\n"
117 					<< "Usage: pinot [OPTIONS]\n\n"
118 					<< "Options:\n"
119 					<< "  -h, --help		display this help and exit\n"
120 					<< "  -p, --preferences	show preferences\n"
121 					<< "  -q, --query		open with query terms\n"
122 					<< "  -v, --version		output version information and exit\n"
123 					<< "\nReport bugs to " << PACKAGE_BUGREPORT << endl;
124 				return EXIT_SUCCESS;
125 			case 'p':
126 				prefsMode = true;
127 				break;
128 			case 'q':
129 				if (optarg != NULL)
130 				{
131 					queryTerms = optarg;
132 				}
133 				break;
134 			case 'v':
135 				clog << "pinot - " << PACKAGE_STRING << "\n\n"
136 					<< "This is free software.  You may redistribute copies of it under the terms of\n"
137 					<< "the GNU General Public License <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>.\n"
138 					<< "There is NO WARRANTY, to the extent permitted by law." << endl;
139 				return EXIT_SUCCESS;
140 			default:
141 				return EXIT_FAILURE;
142 		}
143 
144 		// Next option
145 		optionChar = getopt_long(argc, argv, "hpq:v", g_longOptions, &longOptionIndex);
146 	}
147 
148 	string programName(argv[0]);
149 	if ((programName.length() >= 11) &&
150 		(programName.substr(programName.length() - 11) == "pinot-prefs"))
151 	{
152 		prefsMode = true;
153 	}
154 
155 #if defined(ENABLE_NLS)
156 	bindtextdomain(GETTEXT_PACKAGE, PACKAGE_LOCALE_DIR);
157 	bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
158 	textdomain(GETTEXT_PACKAGE);
159 #endif //ENABLE_NLS
160 
161 	Glib::init();
162 	Gio::init();
163 	// Initialize threads support before doing anything else
164 	if (Glib::thread_supported() == false)
165 	{
166 		Glib::thread_init();
167 	}
168 	// Initialize GType
169 #if !GLIB_CHECK_VERSION(2,35,0)
170 	g_type_init();
171 #endif
172 
173 	Gtk::Main m(&argc, &argv);
174 	if (prefsMode == false)
175 	{
176 		Glib::set_application_name("Pinot");
177 	}
178 	else
179 	{
180 		Glib::set_application_name("Pinot Preferences");
181 	}
182 
183 	// This will be useful for indexes served by xapian-progsrv+ssh
184 	Glib::setenv("SSH_ASKPASS", prefixDir + "/libexec/openssh/ssh-askpass");
185 
186 	// This is a hack to force the locale to UTF-8
187 	char *pLocale = setlocale(LC_ALL, NULL);
188 	if (pLocale != NULL)
189 	{
190 		string locale(pLocale);
191 
192 		if (locale != "C")
193 		{
194 			bool appendUTF8 = false;
195 
196 			string::size_type pos = locale.find_last_of(".");
197 			if ((pos != string::npos) &&
198 				((strcasecmp(locale.substr(pos).c_str(), ".utf8") != 0) &&
199 				(strcasecmp(locale.substr(pos).c_str(), ".utf-8") != 0)))
200 			{
201 				locale.resize(pos);
202 				appendUTF8 = true;
203 			}
204 
205 			if (appendUTF8 == true)
206 			{
207 				locale += ".UTF-8";
208 
209 				pLocale = setlocale(LC_ALL, locale.c_str());
210 				if (pLocale != NULL)
211 				{
212 #ifdef DEBUG
213 					clog << "Changed locale to " << pLocale << endl;
214 #endif
215 				}
216 			}
217 		}
218 	}
219 
220 	// This will create the necessary directories on the first run
221 	PinotSettings &settings = PinotSettings::getInstance();
222 #ifdef HAVE_DBUS
223 	// Talk to the daemon through DBus
224 	settings.setClientMode(true);
225 #endif
226 
227 	string confDirectory(PinotSettings::getConfigurationDirectory());
228 	if (chdir(confDirectory.c_str()) == 0)
229 	{
230 		// Redirect cout, cerr and clog to a file
231 		string logFileName = confDirectory;
232 		if (prefsMode == false)
233 		{
234 			logFileName += "/pinot.log";
235 		}
236 		else
237 		{
238 			logFileName += "/pinot-prefs.log";
239 		}
240 		g_outputFile.open(logFileName.c_str());
241 		g_coutBuf = clog.rdbuf();
242 		g_cerrBuf = clog.rdbuf();
243 		g_clogBuf = clog.rdbuf();
244 		clog.rdbuf(g_outputFile.rdbuf());
245 		clog.rdbuf(g_outputFile.rdbuf());
246 		clog.rdbuf(g_outputFile.rdbuf());
247 	}
248 
249 	// Initialize utility classes
250 	if (MIMEScanner::initialize(PinotSettings::getHomeDirectory() + "/.local",
251 		string(SHARED_MIME_INFO_PREFIX)) == false)
252 	{
253 		clog << "Couldn't load MIME settings" << endl;
254 	}
255 	DownloaderInterface::initialize();
256 	// Load filter libraries, if any
257 	Dijon::FilterFactory::loadFilters(string(LIBDIR) + "/pinot/filters");
258 	Dijon::FilterFactory::loadFilters(confDirectory + "/filters");
259 	// Load backends, if any
260 	ModuleFactory::loadModules(string(LIBDIR) + "/pinot/backends");
261 	ModuleFactory::loadModules(confDirectory + "/backends");
262 
263 	// Localize language names
264 	Languages::setIntlName(0, _("Unknown"));
265 	Languages::setIntlName(1, _("Danish"));
266 	Languages::setIntlName(2, _("Dutch"));
267 	Languages::setIntlName(3, _("English"));
268 	Languages::setIntlName(4, _("Finnish"));
269 	Languages::setIntlName(5, _("French"));
270 	Languages::setIntlName(6, _("German"));
271 	Languages::setIntlName(7, _("Hungarian"));
272 	Languages::setIntlName(8, _("Italian"));
273 	Languages::setIntlName(9, _("Norwegian"));
274 	Languages::setIntlName(10, _("Portuguese"));
275 	Languages::setIntlName(11, _("Romanian"));
276 	Languages::setIntlName(12, _("Russian"));
277 	Languages::setIntlName(13, _("Spanish"));
278 	Languages::setIntlName(14, _("Swedish"));
279 	Languages::setIntlName(15, _("Turkish"));
280 
281 	// Load the settings
282 	settings.load(PinotSettings::LOAD_ALL);
283 
284 	// Catch interrupts
285 #ifdef HAVE_SIGACTION
286 	struct sigaction newAction;
287 	sigemptyset(&newAction.sa_mask);
288 	newAction.sa_flags = 0;
289 	newAction.sa_handler = quitAll;
290 	sigaction(SIGINT, &newAction, NULL);
291 	sigaction(SIGQUIT, &newAction, NULL);
292 	sigaction(SIGTERM, &newAction, NULL);
293 #else
294 	signal(SIGINT, quitAll);
295 #ifdef SIGQUIT
296 	signal(SIGQUIT, quitAll);
297 #endif
298 	signal(SIGTERM, quitAll);
299 #endif
300 
301 	// Open this index read-write, unless we are in preferences mode
302 	bool wasObsoleteFormat = false;
303 	if (ModuleFactory::openOrCreateIndex(settings.m_defaultBackend, settings.m_docsIndexLocation, wasObsoleteFormat, prefsMode) == false)
304 	{
305 		statusText = _("Couldn't open index");
306 		statusText += " ";
307 		statusText += settings.m_docsIndexLocation;
308 	}
309 	else
310 	{
311 		warnAboutVersion = wasObsoleteFormat;
312 	}
313 	// ...and the daemon index in read-only mode
314 	// If it can't be open, it just means the daemon has not yet created it
315 	ModuleFactory::openOrCreateIndex(settings.m_defaultBackend, settings.m_daemonIndexLocation, wasObsoleteFormat, true);
316 	// Merge these two, this will be useful later
317 	ModuleFactory::mergeIndexes(settings.m_defaultBackend, "MERGED", settings.m_docsIndexLocation, settings.m_daemonIndexLocation);
318 
319 	// Do the same for the history database
320 	string historyDatabase(settings.getHistoryDatabaseName());
321 	if ((historyDatabase.empty() == true) ||
322 		(ActionQueue::create(historyDatabase) == false) ||
323 		(QueryHistory::create(historyDatabase) == false) ||
324 		(ViewHistory::create(historyDatabase) == false))
325 	{
326 		statusText = _("Couldn't create history database");
327 		statusText += " ";
328 		statusText += historyDatabase;
329 	}
330 	else
331 	{
332 		ActionQueue actionQueue(historyDatabase, Glib::get_prgname());
333 		QueryHistory queryHistory(historyDatabase);
334 		ViewHistory viewHistory(historyDatabase);
335 		time_t timeNow = time(NULL);
336 
337 		// Expire all actions left from last time
338 		actionQueue.expireItems(timeNow);
339 		// Expire items
340 		queryHistory.expireItems(timeNow - EXPIRY_PERIOD);
341 		viewHistory.expireItems(timeNow - EXPIRY_PERIOD);
342 	}
343 
344 	atexit(closeAll);
345 
346 	if (prefsMode == false)
347 	{
348 		IndexInterface *pIndex = settings.getIndex(settings.m_docsIndexLocation);
349 		if (pIndex != NULL)
350 		{
351 			string indexVersion(pIndex->getMetadata("version"));
352 
353 			// What version is the index at ?
354 			if (indexVersion.empty() == true)
355 			{
356 				indexVersion = "0.0";
357 			}
358 			// Is an upgrade necessary ?
359 			if ((indexVersion < PINOT_INDEX_MIN_VERSION) &&
360 				(pIndex->getDocumentsCount() > 0))
361 			{
362 				warnAboutVersion = true;
363 			}
364 #ifdef DEBUG
365 			clog << "My Web Pages was set to version " << indexVersion << endl;
366 #endif
367 			pIndex->setMetadata("version", VERSION);
368 
369 			delete pIndex;
370 		}
371 		if (warnAboutVersion == true)
372 		{
373 			settings.m_warnAboutVersion = warnAboutVersion;
374 		}
375 	}
376 
377 	try
378 	{
379 		// Set an icon for all windows
380 		Gtk::Window::set_default_icon_from_file(prefixDir + "/share/icons/hicolor/48x48/apps/pinot.png");
381 		// Get a builder
382 		Glib::RefPtr<Gtk::Builder> refBuilder = Gtk::Builder::create_from_file(prefixDir + "/share/pinot/metase-gtk3.gtkbuilder");
383 
384 		refBuilder->set_translation_domain(GETTEXT_PACKAGE);
385 
386 		// Create and open the window
387 		if (prefsMode == false)
388 		{
389 			MainWindow *pMainBox = NULL;
390 			refBuilder->get_widget_derived<MainWindow>("mainWindow", pMainBox, statusText);
391 
392 			if (pMainBox != NULL)
393 			{
394 				pMainBox->setQueryTerms(queryTerms);
395 
396 				m.run(*pMainBox);
397 			}
398 		}
399 		else
400 		{
401 			PrefsWindow *pPrefsBox = NULL;
402 			refBuilder->get_widget_derived<PrefsWindow>("prefsWindow", pPrefsBox);
403 
404 			if (pPrefsBox != NULL)
405 			{
406 				m.run(*pPrefsBox);
407 			}
408 		}
409 	}
410 	catch (const Glib::Exception &e)
411 	{
412 		clog << "Caught exception: " << e.what() << endl;
413 		return EXIT_FAILURE;
414 	}
415 	catch (const char *pMsg)
416 	{
417 		clog << "Caught error: " << pMsg << endl;
418 		cout << "Caught error: " << pMsg << endl;
419 		cerr << "Caught error: " << pMsg << endl;
420 		return EXIT_FAILURE;
421 	}
422 	catch (...)
423 	{
424 		clog << "Unknown exception" << endl;
425 		return EXIT_FAILURE;
426 	}
427 
428 	return EXIT_SUCCESS;
429 }
430