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