PageRenderTime 124ms CodeModel.GetById 14ms app.highlight 90ms RepoModel.GetById 2ms app.codeStats 0ms

/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> &params) {
 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> &params) {
 85    out << "</div>";
 86    out << "</body></html>";
 87}
 88void
 89StrigiHtmlGui::printHelp(ostream& out, const string& path,
 90        const map<string, string> &params) {
 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> &params) {
 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> &params) {
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> &params) {
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> &params) {
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 << "&nbsp;(" << 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> &params) {
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> &params) {
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> &params) {
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}