1 /*
2 * Copyright (C) 2009-2011 Nokia <ivan.frade@nokia.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library 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 GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authors: Jürg Billeter <juerg.billeter@codethink.co.uk>
18 * Martyn Russell <martyn@lanedo.com>
19 *
20 * Based on nautilus-search-engine-tracker.c
21 */
22
23 #include "config.h"
24
25 #include <string.h>
26
27 #include <gio/gio.h>
28 #include <gmodule.h>
29 #include <gdk/gdk.h>
30 #include <gtk/gtk.h>
31
32 #include "gtksearchenginetracker.h"
33
34 #define DBUS_SERVICE_RESOURCES "org.freedesktop.Tracker1"
35 #define DBUS_PATH_RESOURCES "/org/freedesktop/Tracker1/Resources"
36 #define DBUS_INTERFACE_RESOURCES "org.freedesktop.Tracker1.Resources"
37
38 #define DBUS_SERVICE_STATUS "org.freedesktop.Tracker1"
39 #define DBUS_PATH_STATUS "/org/freedesktop/Tracker1/Status"
40 #define DBUS_INTERFACE_STATUS "org.freedesktop.Tracker1.Status"
41
42 /* Time in second to wait for service before deciding it's not available */
43 #define WAIT_TIMEOUT_SECONDS 1
44
45 /* Time in second to wait for query results to come back */
46 #define QUERY_TIMEOUT_SECONDS 10
47
48 /* If defined, we use fts:match, this has to be enabled in Tracker to
49 * work which it usually is. The alternative is to undefine it and
50 * use filename matching instead. This doesn’t use the content of the
51 * file however.
52 */
53 #define FTS_MATCHING
54
55 struct _GtkSearchEngineTracker
56 {
57 GtkSearchEngine parent;
58 GDBusConnection *connection;
59 GCancellable *cancellable;
60 GtkQuery *query;
61 gboolean query_pending;
62 GPtrArray *indexed_locations;
63 };
64
65 struct _GtkSearchEngineTrackerClass
66 {
67 GtkSearchEngineClass parent_class;
68 };
69
G_DEFINE_TYPE(GtkSearchEngineTracker,_gtk_search_engine_tracker,GTK_TYPE_SEARCH_ENGINE)70 G_DEFINE_TYPE (GtkSearchEngineTracker, _gtk_search_engine_tracker, GTK_TYPE_SEARCH_ENGINE)
71
72 static void
73 finalize (GObject *object)
74 {
75 GtkSearchEngineTracker *tracker;
76
77 g_debug ("Finalizing GtkSearchEngineTracker");
78
79 tracker = GTK_SEARCH_ENGINE_TRACKER (object);
80
81 if (tracker->cancellable)
82 {
83 g_cancellable_cancel (tracker->cancellable);
84 g_object_unref (tracker->cancellable);
85 }
86
87 g_clear_object (&tracker->query);
88 g_clear_object (&tracker->connection);
89
90 g_ptr_array_unref (tracker->indexed_locations);
91
92 G_OBJECT_CLASS (_gtk_search_engine_tracker_parent_class)->finalize (object);
93 }
94
95 static GDBusConnection *
get_connection(void)96 get_connection (void)
97 {
98 GDBusConnection *connection;
99 GError *error = NULL;
100 GVariant *reply;
101
102 /* Normally I hate sync calls with UIs, but we need to return NULL
103 * or a GtkSearchEngine as a result of this function.
104 */
105 connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
106
107 if (error)
108 {
109 g_debug ("Couldn't connect to D-Bus session bus, %s", error->message);
110 g_error_free (error);
111 return NULL;
112 }
113
114 /* If connection is set, we know it worked. */
115 g_debug ("Finding out if Tracker is available via D-Bus...");
116
117 /* We only wait 1 second max, we expect it to be very fast. If we
118 * don't get a response by then, clearly we're replaying a journal
119 * or cleaning up the DB internally. Either way, services is not
120 * available.
121 *
122 * We use the sync call here because we don't expect to be waiting
123 * long enough to block UI painting.
124 */
125 reply = g_dbus_connection_call_sync (connection,
126 DBUS_SERVICE_STATUS,
127 DBUS_PATH_STATUS,
128 DBUS_INTERFACE_STATUS,
129 "Wait",
130 NULL,
131 NULL,
132 G_DBUS_CALL_FLAGS_NONE,
133 WAIT_TIMEOUT_SECONDS * 1000,
134 NULL,
135 &error);
136
137 if (error)
138 {
139 g_debug ("Tracker is not available, %s", error->message);
140 g_error_free (error);
141 g_object_unref (connection);
142 return NULL;
143 }
144
145 g_variant_unref (reply);
146
147 g_debug ("Tracker is ready");
148
149 return connection;
150 }
151
152 static void
get_query_results(GtkSearchEngineTracker * engine,const gchar * sparql,GAsyncReadyCallback callback,gpointer user_data)153 get_query_results (GtkSearchEngineTracker *engine,
154 const gchar *sparql,
155 GAsyncReadyCallback callback,
156 gpointer user_data)
157 {
158 g_dbus_connection_call (engine->connection,
159 DBUS_SERVICE_RESOURCES,
160 DBUS_PATH_RESOURCES,
161 DBUS_INTERFACE_RESOURCES,
162 "SparqlQuery",
163 g_variant_new ("(s)", sparql),
164 NULL,
165 G_DBUS_CALL_FLAGS_NONE,
166 QUERY_TIMEOUT_SECONDS * 1000,
167 engine->cancellable,
168 callback,
169 user_data);
170 }
171
172 /* Stolen from libtracker-sparql */
173 static gchar *
sparql_escape_string(const gchar * literal)174 sparql_escape_string (const gchar *literal)
175 {
176 GString *str;
177 const gchar *p;
178
179 g_return_val_if_fail (literal != NULL, NULL);
180
181 str = g_string_new ("");
182 p = literal;
183
184 while (TRUE)
185 {
186 gsize len;
187
188 if (!((*p) != '\0'))
189 break;
190
191 len = strcspn ((const gchar *) p, "\t\n\r\b\f\"\\");
192 g_string_append_len (str, (const gchar *) p, (gssize) ((glong) len));
193 p = p + len;
194
195 switch (*p)
196 {
197 case '\t':
198 g_string_append (str, "\\t");
199 break;
200 case '\n':
201 g_string_append (str, "\\n");
202 break;
203 case '\r':
204 g_string_append (str, "\\r");
205 break;
206 case '\b':
207 g_string_append (str, "\\b");
208 break;
209 case '\f':
210 g_string_append (str, "\\f");
211 break;
212 case '"':
213 g_string_append (str, "\\\"");
214 break;
215 case '\\':
216 g_string_append (str, "\\\\");
217 break;
218 default:
219 continue;
220 }
221
222 p++;
223 }
224 return g_string_free (str, FALSE);
225 }
226
227 static void
sparql_append_string_literal(GString * sparql,const gchar * str,gboolean glob,gboolean is_dir_uri,gboolean quoted)228 sparql_append_string_literal (GString *sparql,
229 const gchar *str,
230 gboolean glob,
231 gboolean is_dir_uri,
232 gboolean quoted)
233 {
234 gchar *s;
235
236 s = sparql_escape_string (str);
237
238 g_string_append_c (sparql, '"');
239 if (quoted)
240 g_string_append (sparql, "\\\"");
241 g_string_append (sparql, s);
242
243 if (is_dir_uri)
244 g_string_append_c (sparql, '/');
245 if (quoted)
246 g_string_append (sparql, "\\\"");
247 if (glob)
248 g_string_append_c (sparql, '*');
249 g_string_append_c (sparql, '"');
250
251 g_free (s);
252 }
253
254 static void
sparql_append_string_literal_lower_case(GString * sparql,const gchar * str)255 sparql_append_string_literal_lower_case (GString *sparql,
256 const gchar *str)
257 {
258 gchar *s;
259
260 s = g_utf8_strdown (str, -1);
261 sparql_append_string_literal (sparql, s, FALSE, FALSE, FALSE);
262 g_free (s);
263 }
264
265 static void
query_callback(GObject * object,GAsyncResult * res,gpointer user_data)266 query_callback (GObject *object,
267 GAsyncResult *res,
268 gpointer user_data)
269 {
270 GtkSearchEngineTracker *tracker;
271 GList *hits;
272 GVariant *reply;
273 GVariant *r;
274 GVariantIter iter;
275 GError *error = NULL;
276 gint i, n;
277 GtkSearchHit *hit;
278
279 tracker = GTK_SEARCH_ENGINE_TRACKER (user_data);
280
281 tracker->query_pending = FALSE;
282
283 reply = g_dbus_connection_call_finish (tracker->connection, res, &error);
284 if (error)
285 {
286 _gtk_search_engine_error (GTK_SEARCH_ENGINE (tracker), error->message);
287 g_error_free (error);
288 g_object_unref (tracker);
289 return;
290 }
291
292 if (!reply)
293 {
294 _gtk_search_engine_finished (GTK_SEARCH_ENGINE (tracker), FALSE);
295 g_object_unref (tracker);
296 return;
297 }
298
299 r = g_variant_get_child_value (reply, 0);
300 g_variant_iter_init (&iter, r);
301 n = g_variant_iter_n_children (&iter);
302 hit = g_new (GtkSearchHit, n);
303 hits = NULL;
304 for (i = 0; i < n; i++)
305 {
306 GVariant *v;
307 const gchar **strv;
308
309 v = g_variant_iter_next_value (&iter);
310 strv = g_variant_get_strv (v, NULL);
311 hit[i].file = g_file_new_for_uri (strv[0]);
312 hit[i].info = NULL;
313 g_free (strv);
314 hits = g_list_prepend (hits, &hit[i]);
315 }
316
317 _gtk_search_engine_hits_added (GTK_SEARCH_ENGINE (tracker), hits);
318 _gtk_search_engine_finished (GTK_SEARCH_ENGINE (tracker), i > 0);
319
320 g_list_free (hits);
321 for (i = 0; i < n; i++)
322 g_object_unref (hit[i].file);
323 g_free (hit);
324
325 g_variant_unref (reply);
326 g_variant_unref (r);
327
328 g_object_unref (tracker);
329 }
330
331 static void
gtk_search_engine_tracker_start(GtkSearchEngine * engine)332 gtk_search_engine_tracker_start (GtkSearchEngine *engine)
333 {
334 GtkSearchEngineTracker *tracker;
335 const gchar *search_text;
336 GFile *location;
337 GString *sparql;
338 gboolean recursive;
339
340 tracker = GTK_SEARCH_ENGINE_TRACKER (engine);
341
342 if (tracker->query_pending)
343 {
344 g_debug ("Attempt to start a new search while one is pending, doing nothing");
345 return;
346 }
347
348 if (tracker->query == NULL)
349 {
350 g_debug ("Attempt to start a new search with no GtkQuery, doing nothing");
351 return;
352 }
353
354 search_text = gtk_query_get_text (tracker->query);
355 location = gtk_query_get_location (tracker->query);
356 recursive = _gtk_search_engine_get_recursive (engine);
357
358 sparql = g_string_new ("SELECT nie:url(?urn) "
359 "WHERE {"
360 " ?urn a nfo:FileDataObject ;"
361 " tracker:available true ; "
362 " nfo:belongsToContainer ?parent; ");
363
364 #ifdef FTS_MATCHING
365 /* Using FTS: */
366 g_string_append (sparql, "fts:match ");
367 sparql_append_string_literal (sparql, search_text, TRUE, FALSE, TRUE);
368 #endif
369
370 g_string_append (sparql, ". FILTER (BOUND(nie:url(?urn)) && ");
371
372 g_string_append (sparql, "fn:contains(fn:lower-case(nfo:fileName(?urn)),");
373 sparql_append_string_literal_lower_case (sparql, search_text);
374 g_string_append (sparql, ")");
375
376 if (location)
377 {
378 gchar *location_uri = g_file_get_uri (location);
379 g_string_append (sparql, " && ");
380 if (recursive)
381 {
382 g_string_append (sparql, "fn:starts-with(nie:url(?urn),");
383 sparql_append_string_literal (sparql, location_uri, FALSE, TRUE, FALSE);
384 g_string_append (sparql, ")");
385 }
386 else
387 {
388 g_string_append (sparql, "nie:url(?parent) = ");
389 sparql_append_string_literal (sparql, location_uri, FALSE, FALSE, FALSE);
390 }
391 g_free (location_uri);
392 }
393
394 g_string_append (sparql, ")");
395
396 #ifdef FTS_MATCHING
397 g_string_append (sparql, " } ORDER BY DESC(fts:rank(?urn)) DESC(nie:url(?urn))");
398 #else /* FTS_MATCHING */
399 g_string_append (sparql, "} ORDER BY DESC(nie:url(?urn)) DESC(nfo:fileName(?urn))");
400 #endif /* FTS_MATCHING */
401
402 tracker->query_pending = TRUE;
403
404 g_debug ("SearchEngineTracker: query: %s", sparql->str);
405
406 get_query_results (tracker, sparql->str, query_callback, g_object_ref (tracker));
407
408 g_string_free (sparql, TRUE);
409 }
410
411 static void
gtk_search_engine_tracker_stop(GtkSearchEngine * engine)412 gtk_search_engine_tracker_stop (GtkSearchEngine *engine)
413 {
414 GtkSearchEngineTracker *tracker;
415
416 tracker = GTK_SEARCH_ENGINE_TRACKER (engine);
417
418 if (tracker->query && tracker->query_pending)
419 {
420 g_cancellable_cancel (tracker->cancellable);
421 tracker->query_pending = FALSE;
422 }
423 }
424
425 static void
gtk_search_engine_tracker_set_query(GtkSearchEngine * engine,GtkQuery * query)426 gtk_search_engine_tracker_set_query (GtkSearchEngine *engine,
427 GtkQuery *query)
428 {
429 GtkSearchEngineTracker *tracker;
430
431 tracker = GTK_SEARCH_ENGINE_TRACKER (engine);
432
433 if (query)
434 g_object_ref (query);
435
436 if (tracker->query)
437 g_object_unref (tracker->query);
438
439 tracker->query = query;
440 }
441
442 static void
_gtk_search_engine_tracker_class_init(GtkSearchEngineTrackerClass * class)443 _gtk_search_engine_tracker_class_init (GtkSearchEngineTrackerClass *class)
444 {
445 GObjectClass *gobject_class;
446 GtkSearchEngineClass *engine_class;
447
448 gobject_class = G_OBJECT_CLASS (class);
449 gobject_class->finalize = finalize;
450
451 engine_class = GTK_SEARCH_ENGINE_CLASS (class);
452 engine_class->set_query = gtk_search_engine_tracker_set_query;
453 engine_class->start = gtk_search_engine_tracker_start;
454 engine_class->stop = gtk_search_engine_tracker_stop;
455 }
456
457 static void get_indexed_locations (GtkSearchEngineTracker *engine);
458
459 static void
_gtk_search_engine_tracker_init(GtkSearchEngineTracker * engine)460 _gtk_search_engine_tracker_init (GtkSearchEngineTracker *engine)
461 {
462 engine->cancellable = g_cancellable_new ();
463 engine->query_pending = FALSE;
464 engine->indexed_locations = g_ptr_array_new_with_free_func (g_object_unref);
465
466 get_indexed_locations (engine);
467 }
468
469 GtkSearchEngine *
_gtk_search_engine_tracker_new(void)470 _gtk_search_engine_tracker_new (void)
471 {
472 GtkSearchEngineTracker *engine;
473 GDBusConnection *connection;
474
475 g_debug ("--");
476
477 connection = get_connection ();
478 if (!connection)
479 return NULL;
480
481 g_debug ("Creating GtkSearchEngineTracker...");
482
483 engine = g_object_new (GTK_TYPE_SEARCH_ENGINE_TRACKER, NULL);
484
485 engine->connection = connection;
486
487 return GTK_SEARCH_ENGINE (engine);
488 }
489
490 #define TRACKER_SCHEMA "org.freedesktop.Tracker.Miner.Files"
491 #define TRACKER_KEY_RECURSIVE_DIRECTORIES "index-recursive-directories"
492
493 static const gchar *
get_user_special_dir_if_not_home(GUserDirectory idx)494 get_user_special_dir_if_not_home (GUserDirectory idx)
495 {
496 const gchar *path;
497 path = g_get_user_special_dir (idx);
498 if (g_strcmp0 (path, g_get_home_dir ()) == 0)
499 return NULL;
500
501 return path;
502 }
503
504 static const gchar *
path_from_tracker_dir(const gchar * value)505 path_from_tracker_dir (const gchar *value)
506 {
507 const gchar *path;
508
509 if (g_strcmp0 (value, "&DESKTOP") == 0)
510 path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_DESKTOP);
511 else if (g_strcmp0 (value, "&DOCUMENTS") == 0)
512 path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_DOCUMENTS);
513 else if (g_strcmp0 (value, "&DOWNLOAD") == 0)
514 path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_DOWNLOAD);
515 else if (g_strcmp0 (value, "&MUSIC") == 0)
516 path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_MUSIC);
517 else if (g_strcmp0 (value, "&PICTURES") == 0)
518 path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_PICTURES);
519 else if (g_strcmp0 (value, "&PUBLIC_SHARE") == 0)
520 path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_PUBLIC_SHARE);
521 else if (g_strcmp0 (value, "&TEMPLATES") == 0)
522 path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_TEMPLATES);
523 else if (g_strcmp0 (value, "&VIDEOS") == 0)
524 path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_VIDEOS);
525 else if (g_strcmp0 (value, "$HOME") == 0)
526 path = g_get_home_dir ();
527 else
528 path = value;
529
530 return path;
531 }
532
533 static void
get_indexed_locations(GtkSearchEngineTracker * engine)534 get_indexed_locations (GtkSearchEngineTracker *engine)
535 {
536 GSettingsSchemaSource *source;
537 GSettingsSchema *schema;
538 GSettings *settings;
539 gchar **locations;
540 gint i;
541 GFile *location;
542 const gchar *path;
543
544 source = g_settings_schema_source_get_default ();
545 schema = g_settings_schema_source_lookup (source, TRACKER_SCHEMA, FALSE);
546 if (!schema)
547 return;
548
549 settings = g_settings_new_full (schema, NULL, NULL);
550 g_settings_schema_unref (schema);
551
552 locations = g_settings_get_strv (settings, TRACKER_KEY_RECURSIVE_DIRECTORIES);
553
554 for (i = 0; locations[i] != NULL; i++)
555 {
556 path = path_from_tracker_dir (locations[i]);
557 if (path == NULL)
558 continue;
559
560 location = g_file_new_for_path (path);
561 g_ptr_array_add (engine->indexed_locations, location);
562 }
563
564 g_strfreev (locations);
565 g_object_unref (settings);
566 }
567
568 gboolean
_gtk_search_engine_tracker_is_indexed(GFile * location,gpointer data)569 _gtk_search_engine_tracker_is_indexed (GFile *location,
570 gpointer data)
571 {
572 GtkSearchEngineTracker *engine = data;
573 gint i;
574 GFile *place;
575
576 for (i = 0; i < engine->indexed_locations->len; i++)
577 {
578 place = g_ptr_array_index (engine->indexed_locations, i);
579 if (g_file_equal (location, place) || g_file_has_prefix (location, place))
580 return TRUE;
581 }
582
583 return FALSE;
584 }
585