Danish Eniro search fixed.
[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://m.krak.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>|<td class=\"hTd2\">(.*)<span class=\"bold\"\\}>(.*)</td>";
49     static const QString SINGLE_REGEXP = "<div class=\"header\">(.*)</div>(.*)<div class=\"callRow\">(.*)(<div class=\"block\">|</p>(.*)<br/>|</p>(.*)<br />)";
50     static const QString NUMBER_REGEXP = "<div class=\"callRow\">(.*)</div>";
51     static const QString LOGIN_CHECK = "<input class=\"inpTxt\" id=\"loginformUsername\"";
52 }
53
54 Eniro::Eniro(QObject *parent): Source(parent), site_(Eniro::FI),
55 loggedIn_(false), username_(""), password_(""),
56 timerId_(0), pendingSearches_(), pendingNumberRequests_()
57 {
58 }
59
60 Eniro::~Eniro()
61 {
62 }
63
64 void Eniro::abort()
65 {
66     Source::abort();
67
68     for(searchMap::iterator sit = pendingSearches_.begin();
69     sit != pendingSearches_.end(); sit++)
70     {
71         if(sit.value() != 0)
72         {
73             delete sit.value();
74             sit.value() = 0;
75         }
76     }
77
78     pendingSearches_.clear();
79
80     for(numberMap::iterator nit = pendingNumberRequests_.begin();
81     nit != pendingNumberRequests_.end(); nit++)
82     {
83         if(nit.value() != 0)
84         {
85             delete nit.value();
86             nit.value() = 0;
87         }
88     }
89
90     pendingNumberRequests_.clear();
91     pendingLoginRequests_.clear();
92 }
93
94 void Eniro::setSite(Eniro::Site site)
95 {
96     site_ = site;
97 }
98
99 void Eniro::timerEvent(QTimerEvent* t)
100 {
101     Q_UNUSED(t);
102
103     int currentId = http_.currentId();
104
105     if(currentId)
106     {
107         searchMap::const_iterator it = pendingSearches_.find(currentId);
108
109         if(it != pendingSearches_.end())
110         {
111             QVector <Eniro::Result> results = it.value()->results;
112             SearchDetails details = it.value()->details;
113
114             abort();
115
116             setError(TIMEOUT, TIMEOUT_STRING);
117
118             emit requestFinished(results, details, true);
119         }
120     }
121 }
122
123 void Eniro::login(QString const& username,
124                   QString const& password)
125 {
126     username_ = username;
127     password_ = password;
128     loggedIn_ = true;
129 }
130
131 void Eniro::logout()
132 {
133     username_ = "";
134     password_ = "";
135     loggedIn_ = false;
136 }
137
138 void Eniro::search(SearchDetails const& details)
139 {
140     resetTimeout();
141
142     SearchType type = details.type;
143
144     // Only logged in users can use other than person search
145     if(!loggedIn_ && site_ == FI)
146     {
147         type = PERSONS;
148     }
149
150     QUrl url = createUrl(details.query, details.location);
151     QString what;
152
153     // We must use full search instead of wap page because wap search is currently not
154     // working for persons
155     if(loggedIn_ && type == PERSONS && site_ == FI && getMaxResults() > 1)
156     {
157         what = "wp";
158     }
159     else if(loggedIn_ || site_ != FI)
160     {
161         switch(type)
162         {
163         case YELLOW_PAGES:
164             what = "mobcs";
165             break;
166
167         case PERSONS:
168             what = "mobwp";
169             break;
170
171         default:
172             what = "moball";
173             break;
174         }
175
176     }
177     else
178     {
179         what = "moball";
180     }
181
182     url.addQueryItem("what", what);
183
184     http_.setHost(url.host(), url.port(80));
185     int id = http_.get(url.encodedPath() + '?' + url.encodedQuery());
186
187     //qDebug() << "Url: " << url.host() << url.encodedPath() << "?" << url.encodedQuery();
188
189     QVector <Source::Result> results;
190
191     // Store search data for later identification
192     SearchData* newData = new SearchData;
193     newData->details = details;
194     newData->results = results;
195     newData->foundNumbers = 0;
196     newData->numbersTotal = 0;
197
198     // Store request id so that it can be identified later
199     pendingSearches_[id] = newData;
200
201 }
202
203 void Eniro::handleHttpData(int id, QByteArray const& data)
204 {
205     searchMap::const_iterator searchIt;
206     numberMap::const_iterator numberIt;
207
208     // Check if request is pending search request
209     if((searchIt = pendingSearches_.find(id)) !=
210         pendingSearches_.end())
211     {
212         if(data.isEmpty())
213         {
214             setError(CONNECTION_FAILURE, "Server returned empty data");
215             emitRequestFinished(id, searchIt.value(), true);
216             return;
217         }
218
219         // Load results from html data
220         loadResults(id, data);
221     }
222
223     // Check if request is pending number requests
224     else if((numberIt = pendingNumberRequests_.find(id)) !=
225         pendingNumberRequests_.end())
226     {
227         if(data.isEmpty())
228         {
229             setError(CONNECTION_FAILURE, "Server returned empty data");
230             emitRequestFinished(id, searchIt.value(), true);
231             return;
232         }
233
234         // Load number from html data
235         loadNumber(id, data);
236     }
237
238     // Check for login request
239     else if(pendingLoginRequests_.find(id) !=
240         pendingLoginRequests_.end())
241     {
242         bool success = true;
243
244         // If html source contains LOGIN_CHECK, login failed
245         if(data.indexOf(LOGIN_CHECK) != -1)
246         {
247             success = false;
248         }
249
250         emit loginStatus(success);
251     }
252
253 }
254
255 void Eniro::handleHttpError(int id)
256 {
257     searchMap::const_iterator searchIt;
258     numberMap::const_iterator numberIt;
259
260     // Check if request is pending search request
261     if((searchIt = pendingSearches_.find(id)) !=
262         pendingSearches_.end())
263     {
264         setError(CONNECTION_FAILURE, http_.errorString());
265         emitRequestFinished(id, searchIt.value(), true);
266     }
267
268     // Check if request is pending number requests
269     else if((numberIt = pendingNumberRequests_.find(id)) !=
270         pendingNumberRequests_.end())
271     {
272         setError(CONNECTION_FAILURE, http_.errorString());
273         delete pendingNumberRequests_[id];
274         pendingNumberRequests_.remove(id);
275     }
276
277     // Check for login request
278     else if(pendingLoginRequests_.find(id) !=
279         pendingLoginRequests_.end())
280     {
281         emit loginStatus(false);
282     }
283 }
284
285 // Loads results from html source code
286 void Eniro::loadResults(int id, QString const& httpData)
287 {
288     searchMap::iterator it = pendingSearches_.find(id);
289
290     // Finnish person search is not working in wap mode so we have to use different type of loading
291     if(getMaxResults() > 1 && loggedIn_ && site_ == FI && it.value()->details.type == PERSONS)
292     {
293         loadFinnishPersonResults(id, httpData);
294         return;
295     }
296
297     QRegExp rx("((" + YELLOW_REGEXP + ")|(" + PERSON_REGEXP + ")|(" + SINGLE_REGEXP + "))");
298     rx.setMinimal(true);
299
300     bool requestsPending = false;
301     int pos = 0;
302     QString data;
303
304     // Find all matches
305     while((pos = rx.indexIn(httpData, pos)) != -1)
306     {
307         pos += rx.matchedLength();
308
309         data = rx.cap(1);
310
311         data = stripTags(data);
312
313         QStringList rows = data.split('\n');
314
315         for(int i = 0; i < rows.size(); i++)
316         {
317             // Remove white spaces
318             QString trimmed = rows.at(i).trimmed().toLower();
319
320             // Remove empty strings
321             if(trimmed.isEmpty())
322             {
323                 rows.removeAt(i);
324                 i--;
325             }
326             else
327             {
328                 // Convert words to uppercase
329                 rows[i] = ucFirst(trimmed);
330             }
331         }
332
333         Result result;
334
335         switch(site_)
336         {
337         case FI:
338             result.country = "Finland";
339             break;
340         case SE:
341             result.country = "Sweden";
342             break;
343         case DK:
344             result.country = "Denmark";
345             break;
346         }
347
348         int size = rows.size();
349
350         switch(size)
351         {
352         case 1:
353             result.name = rows[0];
354             break;
355
356         case 2:
357             result.name = rows[0];
358             result.city = rows[1];
359             break;
360
361         case 3:
362             if(isPhoneNumber(rows[1]))
363             {
364                 result.name = rows[0];
365                 result.number = cleanUpNumber(rows[1]);
366                 result.city = rows[2];
367             }
368             else
369             {
370                 result.name = rows[0];
371                 result.street = rows[1];
372                 result.city = rows[2];
373             }
374             break;
375
376         case 4:
377             result.name = rows[0];
378             // Remove slashes and spaces from number
379             result.number = cleanUpNumber(rows[1]);
380             result.street = rows[2];
381             result.city = rows[3];
382             break;
383
384         default:
385             bool ok = false;
386
387             for(int a = 0; a < size && a < 8; a++)
388             {
389                 if(isPhoneNumber(rows[a]))
390                 {
391                     result.name = rows[0];
392                     result.number = cleanUpNumber(rows[a]);
393
394                     for(int i = a + 1; i < size && i < 8; i++)
395                     {
396                         if(!isPhoneNumber(rows[i]) && size > i + 1 && isStreet(rows[i]))
397                         {
398                             result.street = rows[i];
399                             result.city = rows[i+1];
400                             ok = true;
401                             break;
402                         }
403                     }
404
405                 }
406
407             }
408
409             if(ok)
410             {
411                 break;
412             }
413
414             continue;
415
416         }
417
418         it.value()->results.push_back(result);
419
420         unsigned int foundResults = ++(it.value()->numbersTotal);
421
422         // If phone number search is enabled, we have to make another
423         // request to find it out
424         if(getFindNumber() && size < 4 && (loggedIn_ || site_ != FI) &&
425                 it.value()->details.type != YELLOW_PAGES)
426         {
427             requestsPending = true;
428             getNumberForResult(id, it.value()->results.size() - 1, it.value()->details);
429         }
430         // Otherwise result is ready
431         else
432         {
433             it.value()->foundNumbers++;
434             emit resultAvailable(result, it.value()->details);
435         }
436
437         unsigned int maxResults = getMaxResults();
438
439         // Stop searching if max results is reached
440         if(maxResults && (foundResults >= maxResults))
441         {
442             break;
443         }
444     }
445
446     // If there were no results or no phone numbers needed to
447     // be fetched, the whole request is ready
448     if(it.value()->numbersTotal == 0 || !requestsPending)
449     {
450         bool error = false;
451
452         if(httpData.indexOf(LOGIN_CHECK) != -1)
453         {
454             setError(INVALID_LOGIN, INVALID_LOGIN_STRING),
455             error = true;
456         }
457
458         emitRequestFinished(it.key(), it.value(), error);
459     }
460 }
461
462 void Eniro::loadFinnishPersonResults(int id, QString const& httpData)
463 {
464     searchMap::iterator it = pendingSearches_.find(id);
465
466     static QRegExp rx("<div id=\"hit_(.*)<p class=\"adLinks\">");
467     static QRegExp name("<a class=\"fn expand\" href=\"#\">(.*)</a>");
468     static QRegExp number("<!-- sphoneid(.*)-->(.*)<!--");
469     static QRegExp street("<span class=\"street-address\">(.*)</span>");
470     static QRegExp zipCode("<span class=\"postal-code\">(.*)</span>");
471     static QRegExp city("<span class=\"locality\">(.*)</span>");
472     rx.setMinimal(true);
473     name.setMinimal(true);
474     number.setMinimal(true);
475     street.setMinimal(true);
476     zipCode.setMinimal(true);
477     city.setMinimal(true);
478
479     int pos = 0;
480
481     unsigned int maxResults = getMaxResults();
482     unsigned int foundResults = 0;
483
484     while((pos = rx.indexIn(httpData, pos)) != -1)
485     {
486         pos += rx.matchedLength();
487
488         QString data = rx.cap(0);
489
490         Result result;
491
492         if(name.indexIn(data) != -1)
493         {
494             result.name = name.cap(1);
495         }
496         else
497         {
498             continue;
499         }
500
501         if(number.indexIn(data) != -1)
502         {
503             result.number = number.cap(2);
504         }
505
506         if(street.indexIn(data) != -1)
507         {
508             result.street = street.cap(1);
509         }
510
511         QString cityStr;
512
513         if(zipCode.indexIn(data) != -1)
514         {
515             cityStr = zipCode.cap(1) + " ";
516         }
517
518         if(city.indexIn(data) != -1)
519         {
520             cityStr += city.cap(1);
521         }
522
523         result.city = cityStr;
524
525         result.name = cleanUpString(result.name);
526         result.street = cleanUpString(result.street);
527         result.number = cleanUpNumber(result.number);
528         result.city = cleanUpString(result.city);
529         result.country = "Finland";
530
531         it.value()->results.push_back(result);
532         emit resultAvailable(result, it.value()->details);
533
534         foundResults++;
535
536         if(foundResults >= maxResults)
537         {
538             break;
539         }
540
541     }
542
543     emitRequestFinished(it.key(), it.value(), false);
544
545 }
546
547 QString& Eniro::cleanUpString(QString& str)
548 {
549     str = htmlEntityDecode(str);
550     str = str.toLower();
551     str = str.trimmed();
552     static QRegExp cleaner("(\r\n|\n|\t| )+");
553     str = str.replace(cleaner, " ");
554     str = ucFirst(str);
555     return str;
556 }
557
558 // Loads phone number from html source
559 void Eniro::loadNumber(int id, QString const& result)
560 {
561     numberMap::iterator numberIt = pendingNumberRequests_.find(id);
562
563     // Make sure that id exists in pending number requests
564     if(numberIt == pendingNumberRequests_.end() || numberIt.value() == 0)
565     {
566         return;
567     }
568
569     searchMap::iterator searchIt = pendingSearches_.find(numberIt.value()->searchId);
570
571     if(searchIt == pendingSearches_.end() || searchIt.value() == 0)
572     {
573         return;
574     }
575
576     QRegExp rx(NUMBER_REGEXP);
577     rx.setMinimal(true);
578
579     int pos = 0;
580     bool error = true;
581
582     if((pos = rx.indexIn(result, pos)) != -1)
583     {
584         QString data = rx.cap(1);
585         data = stripTags(data);
586
587         QString trimmed = data.trimmed();
588
589         if(!trimmed.isEmpty())
590         {
591             // Remove whitespaces from number
592             searchIt.value()->results[numberIt.value()->index].number = cleanUpNumber(trimmed);
593
594             emit resultAvailable(searchIt.value()->results[numberIt.value()->index], searchIt.value()->details);
595
596             unsigned int found = ++searchIt.value()->foundNumbers;
597
598             // Check if all numbers have been found
599             if(found >= searchIt.value()->numbersTotal)
600             {
601                 emitRequestFinished(searchIt.key(), searchIt.value(), false);
602             }
603
604             // If number was found, there was no error
605             error = false;
606         }
607     }
608
609     if(error)
610     {
611         setError(INVALID_LOGIN, INVALID_LOGIN_STRING);
612         emitRequestFinished(searchIt.key(), searchIt.value(), true);
613     }
614
615     // Remove number request
616     int key = numberIt.key();
617
618     delete pendingNumberRequests_[key];
619     pendingNumberRequests_[key] = 0;
620     pendingNumberRequests_.remove(key);
621
622 }
623
624 QUrl Eniro::createUrl(QString const& query, QString const& location)
625 {
626     QUrl url(SITE_URLS[site_] + "query");
627
628     if(!query.isEmpty())
629     {
630         url.addQueryItem("search_word", query);
631     }
632
633     if(!location.isEmpty())
634     {
635         url.addQueryItem("geo_area", location);
636     }
637
638     unsigned int maxResults = getMaxResults();
639
640     if(maxResults)
641     {
642         url.addQueryItem("hpp", QString::number(maxResults));
643     }
644     if(loggedIn_ && site_ == FI)
645     {
646         url.addQueryItem("login_name", username_);
647         url.addQueryItem("login_password", password_);
648     }
649
650     fixUrl(url);
651
652     return url;
653 }
654
655 // Creates a new request for phone number retrieval
656 void Eniro::getNumberForResult(int id, int index, SearchDetails const& details)
657 {
658     QUrl url = createUrl(details.query, details.location);
659     url.addQueryItem("what", "mobwpinfo");
660     url.addQueryItem("search_number", QString::number(index + 1));
661
662     http_.setHost(url.host(), url.port(80));
663     int requestId = http_.get(url.encodedPath() + '?' + url.encodedQuery());
664     NumberData* number = new NumberData;
665     number->searchId = id;
666     number->index = index;
667     pendingNumberRequests_[requestId] = number;
668
669 }
670
671 void Eniro::emitRequestFinished(int key, SearchData* data, bool error)
672 {
673     emit requestFinished(data->results, data->details, error);
674     delete pendingSearches_[key];
675     pendingSearches_[key] = 0;
676     pendingSearches_.remove(key);
677 }
678
679
680 QMap <Eniro::Site, Eniro::SiteDetails> Eniro::getSites()
681 {
682     QMap <Site, SiteDetails> sites;
683
684     for(int i = 0; i < SITE_COUNT; i++)
685     {
686         SiteDetails details;
687         details.name = SITE_NAMES[i];
688         details.id = SITE_IDS[i];
689         sites[static_cast<Site>(i)] = details;
690     }
691
692     return sites;
693 }
694
695 Eniro::Site Eniro::stringToSite(QString const& str)
696 {
697     Site site = FI;
698     QString lower = str.toLower();
699
700     for(int i = 0; i < SITE_COUNT; i++)
701     {
702         if(lower == SITE_NAMES[i] || lower == SITE_IDS[i])
703         {
704             site = static_cast <Site> (i);
705             break;
706         }
707     }
708
709     return site;
710 }
711
712 bool Eniro::isStreet(QString const& str)
713 {
714     static QRegExp number("([0-9]+)");
715     int a = number.indexIn(str);
716     int b = str.indexOf(" ");
717
718     if((a == -1 && b == -1) || (a != -1 && b != -1))
719     {
720         return true;
721     }
722
723     return false;
724 }