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