/strigi-0.7.7/strigiclient/lib/htmlgui/strigihtmlgui.cpp
C++ | 537 lines | 479 code | 13 blank | 45 comment | 125 complexity | 1d2678b550d93a0ffc57254d1c8bab84 MD5 | raw file
Possible License(s): LGPL-2.0
1/* This file is part of Strigi Desktop Search
2 *
3 * Copyright (C) 2006 Jos van den Oever <jos@vandenoever.info>
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Library General Public
7 * License as published by the Free Software Foundation; either
8 * version 2 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Library General Public License for more details.
14 *
15 * You should have received a copy of the GNU Library General Public License
16 * along with this library; see the file COPYING.LIB. If not, write to
17 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 * Boston, MA 02110-1301, USA.
19 */
20#include "strigihtmlgui.h"
21#include <strigi/socketclient.h>
22#include <strigi/indexreader.h>
23#include <strigi/query.h>
24#include <strigi/queryparser.h>
25#include <unistd.h>
26#include <sys/types.h>
27#include <dirent.h>
28#include <sstream>
29#include <fstream>
30#include <iostream>
31#include <sys/stat.h>
32#include <stdlib.h>
33#include <string.h>
34
35using namespace std;
36using namespace Strigi;
37
38class StrigiHtmlGui::Private {
39private:
40 HtmlHelper* h;
41 string highlightTerms(const string& t, const Query& q) const;
42 void printSearchResult(ostream& out,
43 const Strigi::IndexedDocument& doc, const Query& q) const;
44public:
45 SocketClient strigi;
46
47 Private(HtmlHelper* h);
48 void printSearchResults(ostream& out, const ClientInterface::Hits&,
49 const string& query) const;
50};
51
52StrigiHtmlGui::StrigiHtmlGui(HtmlHelper* h) :helper(h), p(new Private(helper)) {
53}
54StrigiHtmlGui::~StrigiHtmlGui() {
55 delete p;
56}
57void
58StrigiHtmlGui::printHtmlHeader(ostream& out) {
59 // FIXME: extra spaces added at the beginning as a wordaround because
60 // KIO discards several first chars for unknown reason
61 out << " <?xml version='1.0' encoding='utf-8'?>\n"
62 "<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN' "
63 "'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>\n"
64 "<html xmlns='http://www.w3.org/1999/xhtml'>"
65 "<head><meta http-equiv='Content-Type' "
66 "content='text/html; charset=utf-8' />"
67 "<link rel='stylesheet' type='text/css' href='"
68 << helper->getCssUrl()
69 << "'/>"
70 "<title>Strigi Desktop Search</title>"
71 "</head><body>";
72}
73void
74StrigiHtmlGui::printHeader(ostream& out, const string& path,
75 const map<string, string> ¶ms) {
76 printHtmlHeader(out);
77 out << "<div class='header'>";
78 printMenu(out, path, params);
79 out << "<div class='title'>Strigi Desktop Search</div>";
80 out << "</div><div class='box'>";
81}
82void
83StrigiHtmlGui::printFooter(ostream& out, const string& path,
84 const map<string, string> ¶ms) {
85 out << "</div>";
86 out << "</body></html>";
87}
88void
89StrigiHtmlGui::printHelp(ostream& out, const string& path,
90 const map<string, string> ¶ms) {
91 out << "For help see the <a href='http://strigi.sf.net'>Strigi Wiki</a>";
92}
93void
94StrigiHtmlGui::printAbout(ostream& out, const string& path,
95 const map<string, string> ¶ms) {
96 out << "Strigi v"STRIGI_VERSION_STRING"<br/>";
97 out << "For more details see <a href='http://strigi.sf.net/'>"
98 "the Strigi Website</a>.";
99}
100void
101StrigiHtmlGui::printConfig(ostream& out, const string& path,
102 const map<string, string> ¶ms) {
103 printIndexedDirs(out, path, params);
104}
105bool
106exists(const char* file) {
107 struct stat s;
108 if (stat(file, &s)) {
109 return false;
110 }
111 return S_ISREG(s.st_mode);
112}
113void
114startDaemon() {
115 char daemon[13];
116 strcpy(daemon, "strigidaemon");
117 // find the executable
118 // get the PATH environment
119 const char* path = getenv("PATH");
120 const char* end = strchr(path, ':');
121 string p;
122 while (end) {
123 p.assign(path, end);
124 p.append("/");
125 p.append(daemon);
126 path = end+1;
127 end = strchr(path, ':');
128 if (exists(p.c_str())) {
129 if (fork()) {
130 const char * const args[] = {daemon, "clucene", 0};
131 execvp(p.c_str(), (char **)args);
132 }
133 break;
134 }
135 }
136
137}
138void
139StrigiHtmlGui::printStatus(ostream& out, const string& path,
140 const map<string, string> ¶ms) {
141 map<string, string> status;
142 if (path == "status/start") {
143 status = p->strigi.getStatus();
144 if (status.size() == 0) {
145 startDaemon();
146 int n = 0;
147 while (n < 5 && status.size() == 0) {
148 sleep(1);
149 status = p->strigi.getStatus();
150 n++;
151 }
152 }
153 } else if (path == "status/stop") {
154 p->strigi.stopDaemon();
155 } else if (path == "status/stopindexing") {
156 p->strigi.stopIndexing();
157 status = p->strigi.getStatus();
158 } else if (path == "status/startindexing") {
159 p->strigi.startIndexing();
160 status = p->strigi.getStatus();
161 } else {
162 status = p->strigi.getStatus();
163 }
164 if (status.size() == 0) {
165 out << "<p><a href='/status/start'>Start daemon</a></p>";
166 } else {
167 map<string, string>::const_iterator i;
168 out << "<table>";
169 for (i = status.begin(); i != status.end(); ++i) {
170 out << "<tr><td>" << i->first << "</td><td>" << i->second
171 << "</td><tr>";
172 }
173 out << "</table>";
174 out << "<p><a href='/status/stop'>Stop daemon</a></p>";
175 if (status["Status"] == "indexing") {
176 out << "<p><a href='/status/stopindexing'>Stop indexing</a></p>";
177 } else {
178 out << "<p><a href='/status/startindexing'>Start indexing</a></p>";
179 }
180 }
181 // automatically reload the status page
182 out << "<script type='text/javascript'>\n"
183 "setTimeout('window.location.replace(\"/status\")', 2000);\n"
184 "</script>\n";
185}
186void
187StrigiHtmlGui::printSearch(ostream& out, const string& path,
188 const map<string, string> ¶ms) {
189 string query;
190 map<string, string>::const_iterator i = params.find("q");
191 if (i != params.end()) query = i->second;
192 int max = 10;
193 i = params.find("m");
194 if (i != params.end()) {
195 max = atoi(i->second.c_str());
196 }
197 if (max <= 0 || max > 1000) max = 10;
198 int off = 0;
199 i = params.find("o");
200 if (i != params.end()) {
201 off = atoi(i->second.c_str());
202 }
203 if (off < 0) {
204 off = 0;
205 }
206 string selectedtab;
207 i = params.find("t");
208 if (i != params.end()) {
209 selectedtab = i->second;
210 }
211
212 // print tabs
213 int count = 0;
214 if (query.length()) {
215 count = p->strigi.countHits(query);
216 }
217 string activetab;
218 string activequery = query;
219 map<string, int> hitcounts;
220 if (count) {
221 map<string, string> tabs;
222 bool doother = true;
223 tabs = readTabQueries();
224 if (tabs.size() == 0) {
225 tabs["Images"] = "mimeType:image*";
226 tabs["Mail"] = "mimeType:message/*";
227 tabs["Web"] = "mimeType:text/html";
228 tabs["Text"] = "mimeType:text/*";
229 }
230 map<string, string>::const_iterator j;
231 string otherq = query;
232 for (j = tabs.begin(); j != tabs.end(); ++j) {
233 string q = query;
234 if (q != j->second) {
235 q += ' ' + j->second;
236 }
237 int c = p->strigi.countHits(q);
238 if (c > 0) {
239 hitcounts[j->first] = c;
240 doother &= c < count;
241 otherq += " -" + j->second;
242 if (j->first == selectedtab || activetab.size() == 0) {
243 activetab = j->first;
244 activequery = q;
245 }
246 }
247 }
248 doother &= hitcounts.size() > 0;
249 int othercount = 0;
250 if (doother) {
251 othercount = p->strigi.countHits(otherq);
252 if (othercount > 0) {
253 hitcounts["Other"] = othercount;
254 if (selectedtab == "Other") {
255 activetab = selectedtab;
256 activequery = otherq;
257 }
258 }
259 }
260 }
261
262 // print gui
263 out << "<div class='control' style='text-align:right;' padding='5px'>";
264 out << "<form method='get'>";
265 out << "<input type='text' name='q' value='" << query << "'/>";
266 out << "<input type='hidden' name='m' value='" << max << "'/>";
267 out << "<input type='hidden' name='t' value='" << activetab << "'/>";
268 out << "<input type='submit' value='search'/>";
269 int activecount = count;
270 if (hitcounts.size() == 0 && count > 0) {
271 out << " Found " << count << " results.";
272 } else {
273 map<string, int>::const_iterator l;
274 for (l = hitcounts.begin(); l != hitcounts.end(); ++l) {
275 if (l->first == activetab) {
276 out << " <a class='activetab' ";
277 activecount = l->second;
278 } else {
279 out << " <a class='tab' ";
280 }
281 out << "href='?q=" << query << "&t=" << l->first << "'>"
282 << l->first << " (" << l->second << ")" << "</a>";
283 }
284 }
285 out << "</form></div>\n";
286
287 if (activequery.length()) {
288 // print results
289 const ClientInterface::Hits hits = p->strigi.getHits(activequery,
290 max, off);
291 if (hits.hits.size()) {
292 out << "<div class='hits'>";
293 p->printSearchResults(out, hits, activequery);
294 out << "</div>";
295 }
296 }
297 if (activecount > max) {
298 ostringstream oss;
299 oss << ".?q=" << query << "&m=" << max << "&t=" << activetab << "&o=";
300 out << "<div class='pager'>";
301 int o = 0;
302 int n = 1;
303 while (o < activecount && n <= 20) {
304 out << "<a href='" << oss.str() << o << "'>" << n << "</a> ";
305 o += max;
306 n++;
307 }
308 out << "</div>";
309 }
310}
311void
312getFields(set<string>& fields, const Query& query) {
313 copy(query.fields().begin(), query.fields().end(),
314 inserter(fields, fields.begin()));
315 for (vector<Query>::const_iterator i = query.subQueries().begin();
316 i != query.subQueries().end(); ++i) {
317 getFields(fields, *i);
318 }
319}
320void
321getTerms(set<string>& terms, const Query& q) {
322 if (q.term().string().size() && !q.negate()) {
323 terms.insert(q.term().string());
324 }
325 for (vector<Query>::const_iterator i = q.subQueries().begin();
326 i != q.subQueries().end(); ++i) {
327 getTerms(terms, *i);
328 }
329}
330string
331StrigiHtmlGui::Private::highlightTerms(const string& t, const Query& q) const {
332 vector<string> terms;
333 set<string> termset;
334 getTerms(termset, q);
335 copy(termset.begin(), termset.end(), back_inserter(terms));
336 string out = h->highlight(t, terms);
337 return out;
338}
339void
340StrigiHtmlGui::printMenu(ostream& out, const string& path,
341 const map<string, string> ¶ms) {
342 out << "<div class='menu'>" << endl;
343 out << "<a href='/'>search</a> " << endl;
344 out << "<a href='/status'>status</a> " << endl;
345 out << "<a href='/config'>preferences</a> " << endl;
346 out << "<a href='/help'>help</a> " << endl;
347 out << "<a href='/about'>about</a> " << endl;
348 out << "</div>" << endl;
349}
350void
351StrigiHtmlGui::printIndexedDirs(ostream& out, const string& path,
352 const map<string, string> ¶ms) {
353 set<string> dirs = p->strigi.getIndexedDirectories();
354 map<string,string>::const_iterator i = params.find("adddir");
355 if (i != params.end()) {
356 DIR* dir = opendir(i->second.c_str());
357 if (dir) {
358 dirs.insert(i->second);
359 closedir(dir);
360 p->strigi.setIndexedDirectories(dirs);
361 out << "<p>Directory added. Don't forget to start indexing.</p>";
362 }
363 }
364 i = params.find("deldir");
365 if (i != params.end()) {
366 size_t oldsize = dirs.size();
367 dirs.erase(i->second);
368 if (dirs.size() != oldsize) {
369 p->strigi.setIndexedDirectories(dirs);
370 }
371 }
372
373 out << "<table>";
374 set<string>::const_iterator j;
375 for (j = dirs.begin(); j != dirs.end(); ++j) {
376 out << "<tr><td><form method='get'>"
377 "<input type='hidden' name='deldir' value='" << *j << "'/>"
378 "<input type='submit' value='delete directory'/></form></td><td>"
379 << *j << "</td></tr>";
380 }
381 out << "<form><tr><td><input type='submit' value='add directory'/></td>"
382 "<td><input name='adddir' type='file'/></td></tr></form>";
383 out << "</table>";
384}
385void
386StrigiHtmlGui::printPage(ostream& out, const string& path,
387 const map<string, string> ¶ms) {
388 printHeader(out, path, params);
389
390 bool running = p->strigi.getStatus().size() > 0;
391 if (strncmp(path.c_str(), "help", 4) == 0) {
392 printHelp(out, path, params);
393 } else if (strncmp(path.c_str(), "about", 5) == 0) {
394 printAbout(out, path, params);
395 } else if (running && strncmp(path.c_str(), "config", 6) == 0) {
396 printConfig(out, path, params);
397 } else if (strncmp(path.c_str(), "status", 6) == 0) {
398 printStatus(out, path, params);
399 } else if (running) {
400 printSearch(out, path, params);
401 } else {
402 printStatus(out, path, params);
403 }
404
405 printFooter(out, path, params);
406}
407StrigiHtmlGui::Private::Private(HtmlHelper* helper) :h(helper) {
408 string homedir = getenv("HOME");
409 strigi.setSocketName(homedir+"/.strigi/socket");
410}
411string
412toSizeString(int s) {
413 ostringstream o;
414 if (s > 1024) {
415 o << (s+512)/1024 << "k";
416 } else {
417 o << s << " bytes";
418 }
419 return o.str();
420}
421void
422StrigiHtmlGui::Private::printSearchResult(ostream& out,
423 const Strigi::IndexedDocument& doc, const Query& query) const {
424 multimap<string, string>::const_iterator t;
425 string link, icon, name, folder;
426 int depth = 0;
427 t = doc.properties.find("depth");
428 if (t != doc.properties.end()) {
429 depth = atoi(t->second.c_str());
430 }
431 link = h->mapLinkUrl(doc.uri, depth);
432 icon = h->mapMimetypeIcon(doc.uri, doc.mimetype);
433 if (icon.length()) {
434 icon = "<div class='iconbox'><img class='icon' src='"+icon;
435 icon += "'/></div>\n";
436 }
437 t = doc.properties.find("title");
438 if (t == doc.properties.end()) {
439 t = doc.properties.find("subject");
440 }
441 size_t l = doc.uri.rfind('/');
442 if (t != doc.properties.end()) {
443 name = t->second.c_str();
444 } else if (l != string::npos) {
445 name = doc.uri.substr(l+1);
446 } else {
447 name = doc.uri;
448 }
449 name = h->escapeString(name);
450 if (l != string::npos) {
451 folder = doc.uri.substr(0, l);
452 }
453 out << "<div class='hit'>" << icon << "<h2><a href='" << link << "'>";
454 out << name << "</a></h2>";
455/* out << "<br/>score: ";
456 out << doc.score << ", mime-type: " << doc.mimetype.c_str() << ", size: ";
457 out << doc.size << ", last modified: " << h->formatDate(doc.mtime);*/
458 string fragment = h->escapeString(doc.fragment);
459 fragment = highlightTerms(fragment, query);
460 out << "<div class='fragment'>" << fragment << "</div>";
461 string path = h->escapeString(doc.uri);
462
463 out << "<div class='path'>";
464 string::size_type p = path.find('/');
465 string::size_type pp = 0;
466 string subpath;
467 while (p != string::npos) {
468 subpath = path.substr(pp, p-pp+1);
469 link = h->mapLinkUrl(path.substr(0, p));
470 out << "<a href='" << link << "'>" << subpath << "</a>"
471 // the images are meant to allow line break between characters
472 //"<img class='softbreak' src=''/><span style=''>/</span>"
473 << h->getPathCharacterSeparator();
474 //"<img class='softbreak' src=''/>";
475 pp = p+1;
476 p = path.find('/', pp);
477 }
478 subpath = path.substr(pp, path.length()-pp+1);
479 link = h->mapLinkUrl(doc.uri, depth);
480 out << "<a href='" << link << "'>" << subpath << "</a>";
481
482 out << " - "
483 << toSizeString((int)doc.size) << " - "
484 << h->mimetypeDescription(doc.mimetype) << "</div>";
485 /*out << "<table>";
486 map<string, string>::const_iterator j;
487 for (j = doc.properties.begin(); j != doc.properties.end(); ++j) {
488 out << "<tr><td>" << j->first << ":</td><td>" << j->second.c_str()
489 << "</td></tr>";
490 }
491 out << "</table>";*/
492 out << "</div>";
493}
494void
495StrigiHtmlGui::Private::printSearchResults(ostream& out,
496 const ClientInterface::Hits& hits, const string& q) const {
497 QueryParser parser;
498 Query query = parser.buildQuery(q);
499 vector<Strigi::IndexedDocument>::const_iterator i;
500 for (i = hits.hits.begin(); i != hits.hits.end(); ++i) {
501 printSearchResult(out, *i, query);
502 }
503}
504/**
505 * Read the tab queries from a configuration file.
506 * This file is, in the current implementation, located at ~/.strigi/tabqueries.
507 * The format is a tab separated table with two columns: the name and the query.
508 **/
509map<string, string>
510StrigiHtmlGui::readTabQueries() const {
511 map<string, string> tabs;
512 string path = getenv("HOME");
513 path += "/.strigi/tabqueries";
514 ifstream in;
515 in.open(path.c_str());
516 string s;
517 do {
518 getline(in, s);
519 if (s.size()) {
520 size_t p = s.find('\t');
521 if (p != string::npos) {
522 string name = s.substr(0, p);
523 string value = s.substr(p);
524 tabs[name] = value;
525 }
526 }
527 } while (!in.eof() && in.good());
528 in.close();
529 return tabs;
530}
531void
532StrigiHtmlGui::printSearchResults(std::ostream& out,
533 const ClientInterface::Hits& hits, const std::string& query) {
534 printHtmlHeader(out);
535 p->printSearchResults(out, hits, query);
536 out << "</body></html>";
537}