Fixed problems with special html characters and entities in Norwegian version.
[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, QByteArray const& data)
194 {
195     searchMap::const_iterator searchIt;
196     numberMap::const_iterator numberIt;
197
198     // Check if request is pending search request
199     if((searchIt = pendingSearches_.find(id)) !=
200         pendingSearches_.end())
201     {
202         // Load results from html data
203         loadResults(id, data);
204     }
205
206     // Check if request is pending number requests
207     else if((numberIt = pendingNumberRequests_.find(id)) !=
208         pendingNumberRequests_.end())
209     {
210         // Load number from html data
211         loadNumber(id, data);
212     }
213
214     // Check for login request
215     else if(pendingLoginRequests_.find(id) !=
216         pendingLoginRequests_.end())
217     {
218         bool success = true;
219
220         // If html source contains LOGIN_CHECK, login failed
221         if(data.indexOf(LOGIN_CHECK) != -1)
222         {
223             success = false;
224         }
225
226         emit loginStatus(success);
227     }
228
229 }
230
231 void Eniro::handleHttpError(int id)
232 {
233     searchMap::const_iterator searchIt;
234     numberMap::const_iterator numberIt;
235
236     // Check if request is pending search request
237     if((searchIt = pendingSearches_.find(id)) !=
238         pendingSearches_.end())
239     {
240         setError(CONNECTION_FAILURE, http_.errorString());
241         emitRequestFinished(id, searchIt.value(), true);
242     }
243
244     // Check if request is pending number requests
245     else if((numberIt = pendingNumberRequests_.find(id)) !=
246         pendingNumberRequests_.end())
247     {
248         setError(CONNECTION_FAILURE, http_.errorString());
249         delete pendingNumberRequests_[id];
250         pendingNumberRequests_.remove(id);
251     }
252
253     // Check for login request
254     else if(pendingLoginRequests_.find(id) !=
255         pendingLoginRequests_.end())
256     {
257         emit loginStatus(false);
258
259     }
260 }
261
262 // Loads results from html source code
263 void Eniro::loadResults(int id, QString const& httpData)
264 {
265     searchMap::iterator it = pendingSearches_.find(id);
266     QString expr;
267
268     switch(it.value()->details.type)
269     {
270     case YELLOW_PAGES:
271         expr = YELLOW_REGEXP;
272         break;
273     case PERSONS:
274         expr = PERSON_REGEXP;
275         break;
276     default:
277         return;
278     }
279
280     QRegExp rx(expr);
281     rx.setMinimal(true);
282
283     bool requestsPending = false;
284     int pos = 0;
285     QString data;
286
287     // Find all matches
288     while((pos = rx.indexIn(httpData, pos)) != -1)
289     {
290         pos += rx.matchedLength();
291
292         data = rx.cap(2);
293         data = stripTags(data);
294         QStringList rows = data.split('\n');
295
296         for(int i = 0; i < rows.size(); i++)
297         {
298             // Remove white spaces
299             QString trimmed = rows.at(i).trimmed().toLower();
300
301             // Remove empty strings
302             if(trimmed.isEmpty())
303             {
304                 rows.removeAt(i);
305                 i--;
306             }
307             else
308             {
309                 // Convert words to uppercase
310                 rows[i] = ucFirst(trimmed);
311             }
312         }
313
314         Result result;
315
316         int size = rows.size();
317
318         switch(size)
319         {
320         case 1:
321             result.name = rows[0];
322             break;
323
324         case 2:
325             result.name = rows[0];
326             result.city = rows[1];
327             break;
328
329         case 3:
330             result.name = rows[0];
331             result.street = rows[1];
332             result.city = rows[2];
333             break;
334
335         case 4:
336             result.name = rows[0];
337             // Remove slashes and spaces from number
338             result.number = cleanUpNumber(rows[1]);
339             result.street = rows[2];
340             result.city = rows[3];
341             break;
342
343         default:
344             continue;
345
346         }
347
348         it.value()->results.push_back(result);
349
350         unsigned int foundResults = ++(it.value()->numbersTotal);
351
352         // If phone number searh is enabled, we have to make another
353         // request to find it out
354         if(getFindNumber() && size < 4 && loggedIn_ &&
355                 it.value()->details.type != YELLOW_PAGES)
356         {
357             requestsPending = true;
358             getNumberForResult(id, it.value()->results.size() - 1, it.value()->details);
359         }
360         // Otherwise result is ready
361         else
362         {
363             emit resultAvailable(result, it.value()->details);
364         }
365
366         unsigned int maxResults = getMaxResults();
367
368         // Stop searching if max results is reached
369         if(maxResults && (foundResults >= maxResults))
370         {
371             break;
372         }
373     }
374
375     // If there were no results or no phone numbers needed to
376     // be fetched, the whole request is ready
377     if(it.value()->numbersTotal == 0 || !requestsPending)
378     {
379         bool error = false;
380
381         if(httpData.indexOf(LOGIN_CHECK) != -1)
382         {
383             setError(INVALID_LOGIN, INVALID_LOGIN_STRING),
384             error = true;
385         }
386
387         emitRequestFinished(it.key(), it.value(), error);
388     }
389 }
390
391 // Loads phone number from html source
392 void Eniro::loadNumber(int id, QString const& result)
393 {
394     numberMap::iterator numberIt = pendingNumberRequests_.find(id);
395
396     // Make sure that id exists in pending number requests
397     if(numberIt == pendingNumberRequests_.end() || numberIt.value() == 0)
398     {
399         return;
400     }
401
402     searchMap::iterator searchIt = pendingSearches_.find(numberIt.value()->searchId);
403
404     if(searchIt == pendingSearches_.end() || searchIt.value() == 0)
405     {
406         return;
407     }
408
409     QRegExp rx(NUMBER_REGEXP);
410     rx.setMinimal(true);
411
412     int pos = 0;
413     bool error = true;
414
415     if((pos = rx.indexIn(result, pos)) != -1)
416     {
417         QString data = rx.cap(1);
418         data = stripTags(data);
419
420         QString trimmed = data.trimmed();
421
422         if(!trimmed.isEmpty())
423         {
424             // Remove whitespaces from number
425             searchIt.value()->results[numberIt.value()->index].number = cleanUpNumber(trimmed);
426
427             emit resultAvailable(searchIt.value()->results[numberIt.value()->index], searchIt.value()->details);
428
429             unsigned int found = ++searchIt.value()->foundNumbers;
430
431             // Check if all numbers have been found
432             if(found >= searchIt.value()->numbersTotal)
433             {
434                 emitRequestFinished(searchIt.key(), searchIt.value(), false);
435             }
436
437             // If number was found, there was no error
438             error = false;
439         }
440     }
441
442     if(error)
443     {
444         setError(INVALID_LOGIN, INVALID_LOGIN_STRING);
445         emitRequestFinished(searchIt.key(), searchIt.value(), true);
446     }
447
448     // Remove number request
449     int key = numberIt.key();
450
451     delete pendingNumberRequests_[key];
452     pendingNumberRequests_[key] = 0;
453     pendingNumberRequests_.remove(key);
454
455 }
456
457 QUrl Eniro::createUrl(QString const& query, QString const& location)
458 {
459     QUrl url(SITE_URLS[site_] + "query");
460
461     if(!query.isEmpty())
462     {
463         url.addQueryItem("search_word", query);
464     }
465
466     if(!location.isEmpty())
467     {
468         url.addQueryItem("geo_area", location);
469     }
470
471     unsigned int maxResults = getMaxResults();
472
473     if(maxResults)
474     {
475         url.addQueryItem("hpp", QString::number(maxResults));
476     }
477     if(loggedIn_)
478     {
479         url.addQueryItem("login_name", username_);
480         url.addQueryItem("login_password", password_);
481     }
482
483     fixUrl(url);
484
485     return url;
486 }
487
488 // Creates a new request for phone number retrieval
489 void Eniro::getNumberForResult(int id, int index, SearchDetails const& details)
490 {
491     QUrl url = createUrl(details.query, details.location);
492     url.addQueryItem("what", "mobwpinfo");
493     url.addQueryItem("search_number", QString::number(index + 1));
494
495     http_.setHost(url.host(), url.port(80));
496     int requestId = http_.get(url.encodedPath() + '?' + url.encodedQuery());
497     NumberData* number = new NumberData;
498     number->searchId = id;
499     number->index = index;
500     pendingNumberRequests_[requestId] = number;
501
502 }
503
504 void Eniro::emitRequestFinished(int key, SearchData* data, bool error)
505 {
506     emit requestFinished(data->results, data->details, error);
507     delete pendingSearches_[key];
508     pendingSearches_[key] = 0;
509     pendingSearches_.remove(key);
510 }
511
512
513 QMap <Eniro::Site, Eniro::SiteDetails> Eniro::getSites()
514 {
515     QMap <Site, SiteDetails> sites;
516     SiteDetails details;
517
518     for(int i = 0; i < SITE_COUNT; i++)
519     {
520         SiteDetails details;
521         details.name = SITE_NAMES[i];
522         details.id = SITE_IDS[i];
523         sites[static_cast<Site>(i)] = details;
524     }
525
526     return sites;
527 }
528
529 Eniro::Site Eniro::stringToSite(QString const& str)
530 {
531     Site site = FI;
532     QString lower = str.toLower();
533
534     for(int i = 0; i < SITE_COUNT; i++)
535     {
536         if(lower == SITE_NAMES[i] || lower == SITE_IDS[i])
537         {
538             site = static_cast <Site> (i);
539             break;
540         }
541     }
542
543     return site;
544 }