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