Better cache handling. Added clear cache button to settings.
[jenirok] / src / common / eniro.cpp
1 /*
2  * This file is part of Jenirok.
3  *
4  * Jenirok 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 3 of the License, or
7  * (at your option) any later version.
8  *
9  * Jenirok 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 Jenirok.  If not, see <http://www.gnu.org/licenses/>.
16  *
17  */
18
19 #include <QtCore/QDebug>
20 #include "eniro.h"
21
22 namespace
23 {
24     static const QString SITE_URLS[Eniro::SITE_COUNT] =
25     {
26             "http://wap.eniro.fi/",
27             "http://wap.eniro.se/",
28             "http://wap.eniro.dk/"
29     };
30
31     static const QString SITE_NAMES[Eniro::SITE_COUNT] =
32     {
33          "finnish",
34          "swedish",
35          "danish"
36     };
37
38     static const QString SITE_IDS[Eniro::SITE_COUNT] =
39     {
40          "fi",
41          "se",
42          "dk"
43     };
44
45     static const QString INVALID_LOGIN_STRING = "Invalid login details";
46     static const QString TIMEOUT_STRING = "Request timed out";
47     static const QString PERSON_REGEXP = "<td class=\"hTd2\">(.*)<b>(.*)</td>";
48     static const QString YELLOW_REGEXP = "<td class=\"hTd2\">(.*)<span class=\"gray\"\\}>(.*)</td>";
49     static const QString NUMBER_REGEXP = "<div class=\"callRow\">(.*)</div>";
50     static const QString LOGIN_CHECK = "<input class=\"inpTxt\" id=\"loginformUsername\"";
51 }
52
53 // Regexp used to remove numbers from string
54 QRegExp Eniro::numberCleaner_ = QRegExp("([^0-9]+)");
55
56 // Removes html tags from string
57 QRegExp Eniro::tagStripper_ = QRegExp("<([^>]+)>");
58
59 Eniro::Eniro(Site site, QObject *parent): QObject(parent), site_(site),
60 username_(""), password_(""), loggedIn_(false), error_(NO_ERROR),
61 errorString_(""), maxResults_(10), timeout_(0), timerId_(0), findNumber_(true),
62 pendingSearches_(), pendingNumberRequests_()
63 {
64     connect(&http_, SIGNAL(requestFinished(int, bool)), this, SLOT(httpReady(int, bool)));
65 }
66
67 Eniro::~Eniro()
68 {
69     abort();
70 }
71
72 void Eniro::abort()
73 {
74     http_.abort();
75
76     for(searchMap::iterator sit = pendingSearches_.begin();
77     sit != pendingSearches_.end(); sit++)
78     {
79         if(sit.value() != 0)
80         {
81             delete sit.value();
82             sit.value() = 0;
83         }
84     }
85
86     pendingSearches_.clear();
87
88     for(numberMap::iterator nit = pendingNumberRequests_.begin();
89     nit != pendingNumberRequests_.end(); nit++)
90     {
91         if(nit.value() != 0)
92         {
93             delete nit.value();
94             nit.value() = 0;
95         }
96     }
97
98     pendingNumberRequests_.clear();
99     pendingLoginRequests_.clear();
100 }
101
102 void Eniro::setMaxResults(unsigned int value)
103 {
104     maxResults_ = value;
105 }
106
107 void Eniro::setFindNumber(bool value)
108 {
109     findNumber_ = value;
110 }
111
112 void Eniro::setSite(Eniro::Site site)
113 {
114     site_ = site;
115 }
116
117 void Eniro::setTimeout(unsigned int ms)
118 {
119     timeout_ = ms;
120     resetTimeout();
121 }
122
123 void Eniro::resetTimeout()
124 {
125     if(timerId_)
126     {
127         killTimer(timerId_);
128     }
129     if(timeout_)
130     {
131         timerId_ = startTimer(timeout_);
132     }
133 }
134
135 void Eniro::timerEvent(QTimerEvent* t)
136 {
137     if(t->timerId() == timerId_)
138     {
139         int currentId = http_.currentId();
140
141         if(currentId)
142         {
143             searchMap::const_iterator it = pendingSearches_.find(currentId);
144
145             if(it != pendingSearches_.end())
146             {
147                 QVector <Eniro::Result> results = it.value()->results;
148                 SearchDetails details = it.value()->details;
149
150                 abort();
151
152                 error_ = TIMEOUT;
153                 errorString_ = TIMEOUT_STRING;
154
155                 emit requestFinished(results, details, true);
156             }
157         }
158
159     }
160 }
161
162 void Eniro::login(QString const& username,
163                   QString const& password)
164 {
165     username_ = username;
166     password_ = password;
167     loggedIn_ = true;
168 }
169
170 void Eniro::logout()
171 {
172     username_ = "";
173     password_ = "";
174     loggedIn_ = false;
175 }
176
177 void Eniro::testLogin()
178 {
179     QUrl url = createUrl("", "");
180
181     url.addQueryItem("what", "mobwp");
182     http_.setHost(url.host(), url.port(80));
183     int id = http_.get(url.encodedPath() + '?' + url.encodedQuery());
184
185     pendingLoginRequests_.insert(id);
186 }
187
188 bool Eniro::search(SearchDetails const& details)
189 {
190     resetTimeout();
191
192     SearchType type = details.type;
193
194     // Only logged in users can use other than person search
195     if(!loggedIn_)
196     {
197         type = PERSONS;
198     }
199
200     QUrl url = createUrl(details.query, details.location);
201     QString what;
202
203     if(loggedIn_)
204     {
205         switch(type)
206         {
207         case YELLOW_PAGES:
208             what = "mobcs";
209             break;
210
211         case PERSONS:
212             what = "mobwp";
213             break;
214
215         default:
216             what = "moball";
217         }
218
219     }
220     else
221     {
222         what = "moball";
223     }
224
225     url.addQueryItem("what", what);
226
227     http_.setHost(url.host(), url.port(80));
228     int id = http_.get(url.encodedPath() + '?' + url.encodedQuery());
229
230     QVector <Result> results;
231
232     // Store search data for later identification
233     SearchData* newData = new SearchData;
234     newData->details = details;
235     newData->results = results;
236     newData->foundNumbers = 0;
237     newData->numbersTotal = 0;
238
239     // Store request id so that it can be identified later
240     pendingSearches_[id] = newData;
241
242     return true;
243 }
244
245 Eniro::Error Eniro::error() const
246 {
247     return error_;
248 }
249
250 const QString& Eniro::errorString() const
251 {
252     return errorString_;
253 }
254
255 void Eniro::httpReady(int id, bool error)
256 {
257     if(error)
258     {
259         qDebug() << "Error: " << http_.errorString();
260     }
261
262     searchMap::const_iterator searchIt;
263     numberMap::const_iterator numberIt;
264
265     // Check if request is pending search request
266     if((searchIt = pendingSearches_.find(id)) !=
267         pendingSearches_.end())
268     {
269         if(error)
270         {
271             error_ = CONNECTION_FAILURE;
272             errorString_ = http_.errorString();
273             emitRequestFinished(id, searchIt.value(), true);
274             return;
275         }
276
277         QString result(http_.readAll());
278
279         // Load results from html data
280         loadResults(id, result);
281     }
282
283     // Check if request is pending number requests
284     else if((numberIt = pendingNumberRequests_.find(id)) !=
285         pendingNumberRequests_.end())
286     {
287         if(error)
288         {
289             error_ = CONNECTION_FAILURE;
290             errorString_ = http_.errorString();
291             delete pendingNumberRequests_[id];
292             pendingNumberRequests_.remove(id);
293             return;
294         }
295
296         QString result(http_.readAll());
297
298         // Load number from html data
299         loadNumber(id, result);
300     }
301
302     // Check for login request
303     else if(pendingLoginRequests_.find(id) !=
304         pendingLoginRequests_.end())
305     {
306         bool success = true;
307
308         if(!error)
309         {
310             QString result(http_.readAll());
311
312             // If html source contains LOGIN_CHECK, login failed
313             if(result.indexOf(LOGIN_CHECK) != -1)
314             {
315                 success = false;
316             }
317         }
318         else
319         {
320             success = false;
321         }
322
323         emit loginStatus(success);
324     }
325
326 }
327
328 // Loads results from html source code
329 void Eniro::loadResults(int id, QString const& httpData)
330 {
331     searchMap::iterator it = pendingSearches_.find(id);
332     QString expr;
333
334     switch(it.value()->details.type)
335     {
336     case YELLOW_PAGES:
337         expr = YELLOW_REGEXP;
338         break;
339     case PERSONS:
340         expr = PERSON_REGEXP;
341         break;
342     default:
343         return;
344     }
345
346     QRegExp rx(expr);
347     rx.setMinimal(true);
348
349     bool requestsPending = false;
350     int pos = 0;
351     QString data;
352
353     // Find all matches
354     while((pos = rx.indexIn(httpData, pos)) != -1)
355     {
356         pos += rx.matchedLength();
357
358         data = rx.cap(2);
359         data = stripTags(data);
360         QStringList rows = data.split('\n');
361
362         for(int i = 0; i < rows.size(); i++)
363         {
364             // Remove white spaces
365             QString trimmed = rows.at(i).trimmed().toLower();
366
367             // Remove empty strings
368             if(trimmed.isEmpty())
369             {
370                 rows.removeAt(i);
371                 i--;
372             }
373             else
374             {
375                 // Convert words to uppercase
376                 rows[i] = ucFirst(trimmed);
377             }
378         }
379
380         Result result;
381
382         int size = rows.size();
383
384         switch(size)
385         {
386         case 1:
387             result.name = rows[0];
388             break;
389
390         case 2:
391             result.name = rows[0];
392             result.city = rows[1];
393             break;
394
395         case 3:
396             result.name = rows[0];
397             result.street = rows[1];
398             result.city = rows[2];
399             break;
400
401         case 4:
402             result.name = rows[0];
403             // Remove slashes and spaces from number
404             result.number = cleanUpNumber(rows[1]);
405             result.street = rows[2];
406             result.city = rows[3];
407             break;
408
409         default:
410             continue;
411
412         }
413
414         it.value()->results.push_back(result);
415
416         unsigned int foundResults = ++(it.value()->numbersTotal);
417
418         // If phone number searh is enabled, we have to make another
419         // request to find it out
420         if(findNumber_ && size < 4 && loggedIn_ &&
421                 it.value()->details.type != YELLOW_PAGES)
422         {
423             requestsPending = true;
424             getNumberForResult(id, it.value()->results.size() - 1, it.value()->details);
425         }
426         // Otherwise result is ready
427         else
428         {
429             emit resultAvailable(result, it.value()->details);
430         }
431
432         // Stop searching if max results is reached
433         if(maxResults_ && (foundResults >= maxResults_))
434         {
435             break;
436         }
437     }
438
439     // If number there were no results or no phone numbers needed to
440     // be fetched, the whole request is ready
441     if(it.value()->numbersTotal == 0 || !requestsPending)
442     {
443         bool error = false;
444
445         if(httpData.indexOf(LOGIN_CHECK) != -1)
446         {
447             error_ = INVALID_LOGIN;
448             errorString_ = INVALID_LOGIN_STRING;
449             error = true;
450         }
451
452         emitRequestFinished(it.key(), it.value(), error);
453     }
454 }
455
456 // Loads phone number from html source
457 void Eniro::loadNumber(int id, QString const& result)
458 {
459     numberMap::iterator numberIt = pendingNumberRequests_.find(id);
460
461     // Make sure that id exists in pending number requests
462     if(numberIt == pendingNumberRequests_.end() || numberIt.value() == 0)
463     {
464         return;
465     }
466
467     searchMap::iterator searchIt = pendingSearches_.find(numberIt.value()->searchId);
468
469     if(searchIt == pendingSearches_.end() || searchIt.value() == 0)
470     {
471         return;
472     }
473
474     QRegExp rx(NUMBER_REGEXP);
475     rx.setMinimal(true);
476
477     int pos = 0;
478     bool error = true;
479
480     if((pos = rx.indexIn(result, pos)) != -1)
481     {
482         QString data = rx.cap(1);
483         data = stripTags(data);
484
485         QString trimmed = data.trimmed();
486
487         if(!trimmed.isEmpty())
488         {
489             // Remove whitespaces from number
490             searchIt.value()->results[numberIt.value()->index].number = cleanUpNumber(trimmed);
491
492             emit resultAvailable(searchIt.value()->results[numberIt.value()->index], searchIt.value()->details);
493
494             unsigned int found = ++searchIt.value()->foundNumbers;
495
496             // Check if all numbers have been found
497             if(found >= searchIt.value()->numbersTotal)
498             {
499                 emitRequestFinished(searchIt.key(), searchIt.value(), false);
500             }
501
502             // If number was found, there was no error
503             error = false;
504         }
505     }
506
507     if(error)
508     {
509         error_ = INVALID_LOGIN;
510         errorString_ = INVALID_LOGIN;
511         emitRequestFinished(searchIt.key(), searchIt.value(), true);
512     }
513
514     // Remove number request
515     int key = numberIt.key();
516
517     delete pendingNumberRequests_[key];
518     pendingNumberRequests_[key] = 0;
519     pendingNumberRequests_.remove(key);
520
521 }
522
523 QUrl Eniro::createUrl(QString const& query, QString const& location)
524 {
525     QUrl url(SITE_URLS[site_] + "query");
526
527     if(!query.isEmpty())
528     {
529         url.addQueryItem("search_word", query);
530     }
531
532     if(!location.isEmpty())
533     {
534         url.addQueryItem("geo_area", location);
535     }
536
537     if(maxResults_)
538     {
539         url.addQueryItem("hpp", QString::number(maxResults_));
540     }
541     if(loggedIn_)
542     {
543         url.addQueryItem("login_name", username_);
544         url.addQueryItem("login_password", password_);
545     }
546
547     QByteArray path = url.encodedQuery().replace('+', "%2B");
548     url.setEncodedQuery(path);
549
550     return url;
551 }
552
553 // Creates a new request for phone number retrieval
554 void Eniro::getNumberForResult(int id, int index, SearchDetails const& details)
555 {
556     QUrl url = createUrl(details.query, details.location);
557     url.addQueryItem("what", "mobwpinfo");
558     url.addQueryItem("search_number", QString::number(index + 1));
559
560     http_.setHost(url.host(), url.port(80));
561     int requestId = http_.get(url.encodedPath() + '?' + url.encodedQuery());
562     NumberData* number = new NumberData;
563     number->searchId = id;
564     number->index = index;
565     pendingNumberRequests_[requestId] = number;
566
567 }
568
569 void Eniro::emitRequestFinished(int key, SearchData* data, bool error)
570 {
571
572     // Do not emit "Request aborted" error
573     if(!(error && (http_.error() == QHttp::Aborted)))
574     {
575         emit requestFinished(data->results, data->details, error);
576     }
577
578     delete pendingSearches_[key];
579     pendingSearches_[key] = 0;
580     pendingSearches_.remove(key);
581 }
582
583 QString Eniro::ucFirst(QString& str)
584 {
585     if (str.size() < 1) {
586         return "";
587     }
588
589     QStringList tokens = str.split(" ");
590     QList<QString>::iterator tokItr;
591
592     for (tokItr = tokens.begin(); tokItr != tokens.end(); ++tokItr)
593     {
594         (*tokItr) = (*tokItr).at(0).toUpper() + (*tokItr).mid(1);
595     }
596
597     return tokens.join(" ");
598 }
599
600 QString& Eniro::cleanUpNumber(QString& number)
601 {
602     return number.replace(numberCleaner_, "");
603 }
604
605 QString& Eniro::stripTags(QString& string)
606 {
607     return string.replace(tagStripper_, "");
608 }
609
610 QMap <Eniro::Site, Eniro::SiteDetails> Eniro::getSites()
611 {
612     QMap <Site, SiteDetails> sites;
613     SiteDetails details;
614
615     for(int i = 0; i < SITE_COUNT; i++)
616     {
617         SiteDetails details;
618         details.name = SITE_NAMES[i];
619         details.id = SITE_IDS[i];
620         sites[static_cast<Site>(i)] = details;
621     }
622
623     return sites;
624 }
625
626 Eniro::Site Eniro::stringToSite(QString const& str)
627 {
628     Site site = FI;
629     QString lower = str.toLower();
630
631     for(int i = 0; i < SITE_COUNT; i++)
632     {
633         if(lower == SITE_NAMES[i] || lower == SITE_IDS[i])
634         {
635             site = static_cast <Site> (i);
636         }
637     }
638
639     return site;
640 }
641
642 Eniro::SearchDetails::SearchDetails(QString const& q,
643                                     QString const& loc,
644                                     SearchType t)
645 {
646     query = q;
647     location = loc;
648     type = t;
649 }