1 /* ============================================================
2 * QuiteRSS is a open-source cross-platform RSS/Atom news feeds reader
3 * Copyright (C) 2011-2020 QuiteRSS Team <quiterssteam@gmail.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 * ============================================================ */
18 #include "parseobject.h"
19 
20 #include "mainapplication.h"
21 #include "database.h"
22 #include "VersionNo.h"
23 #include "common.h"
24 
25 #include <QDebug>
26 #include <QDesktopServices>
27 #include <QTextDocumentFragment>
28 #if defined(Q_OS_WIN)
29 #include <windows.h>
30 #endif
31 #include <qzregexp.h>
32 
ParseObject(QObject * parent)33 ParseObject::ParseObject(QObject *parent)
34   : QObject(parent)
35 {
36   setObjectName("parseObject_");
37 
38   db_ = Database::connection("secondConnection");
39 
40   parseTimer_ = new QTimer(this);
41   parseTimer_->setSingleShot(true);
42   parseTimer_->setInterval(10);
43   connect(parseTimer_, SIGNAL(timeout()), this, SLOT(getQueuedXml()),
44           Qt::QueuedConnection);
45 
46   connect(this, SIGNAL(signalReadyParse(QByteArray,int,QDateTime,QString)),
47           SLOT(slotParse(QByteArray,int,QDateTime,QString)));
48 }
49 
~ParseObject()50 ParseObject::~ParseObject()
51 {
52 
53 }
54 
disconnectObjects()55 void ParseObject::disconnectObjects()
56 {
57   disconnect(this);
58 }
59 
60 /** @brief Queueing xml-data
61  *----------------------------------------------------------------------------*/
parseXml(QByteArray data,int feedId,QDateTime dtReply,QString codecName)62 void ParseObject::parseXml(QByteArray data, int feedId,
63                            QDateTime dtReply, QString codecName)
64 {
65   idsQueue_.enqueue(feedId);
66   xmlsQueue_.enqueue(data);
67   dtReadyQueue_.enqueue(dtReply);
68   codecNameQueue_.enqueue(codecName);
69   qDebug() << "xmlsQueue_ <<" << feedId << "count=" << xmlsQueue_.count();
70 
71   if (!parseTimer_->isActive())
72     parseTimer_->start();
73 }
74 
75 /** @brief Process xml-data queue
76  *----------------------------------------------------------------------------*/
getQueuedXml()77 void ParseObject::getQueuedXml()
78 {
79   if (!mutex_.tryLock())
80     return;
81 
82   if (idsQueue_.count()) {
83     int feedId = idsQueue_.dequeue();
84     QByteArray currentXml_ = xmlsQueue_.dequeue();
85     QDateTime currentDtReady_ = dtReadyQueue_.dequeue();
86     QString currentCodecName_ = codecNameQueue_.dequeue();
87     qDebug() << "xmlsQueue_ >>" << feedId << "count=" << xmlsQueue_.count();
88 
89     emit signalReadyParse(currentXml_, feedId, currentDtReady_, currentCodecName_);
90   }
91 
92   mutex_.unlock();
93 
94   if (idsQueue_.count())
95     parseTimer_->start();
96 }
97 
98 /** @brief Parse xml-data
99  *----------------------------------------------------------------------------*/
slotParse(const QByteArray & xmlData,const int & feedId,const QDateTime & dtReply,const QString & codecName)100 void ParseObject::slotParse(const QByteArray &xmlData, const int &feedId,
101                             const QDateTime &dtReply, const QString &codecName)
102 {
103   if (mainApp->isSaveDataLastFeed()) {
104     QFile file(mainApp->dataDir()  + "/lastfeed.dat");
105     file.open(QIODevice::WriteOnly);
106     file.write(xmlData);
107     file.close();
108   }
109 
110   qDebug() << "=================== parseXml:start ============================";
111 
112   db_.transaction();
113 
114   // extract feed id, duplicate news mode and date to avoid from feed table
115   parseFeedId_ = feedId;
116   QString feedUrl;
117   duplicateNewsMode_ = false;
118   addSingleNewsAnyDate_ = false;
119   avoidedOldSingleNews_ = false;
120   avoidedOldSingleNewsDate_ = QDate::currentDate();
121   QSqlQuery q(db_);
122   q.setForwardOnly(true);
123   q.exec(QString("SELECT duplicateNewsMode, xmlUrl, addSingleNewsAnyDateOn, avoidedOldSingleNewsDateOn, avoidedOldSingleNewsDate"
124                  " FROM feeds WHERE id=='%1'").arg(parseFeedId_));
125   if (q.first()) {
126     duplicateNewsMode_ = q.value(0).toBool();
127     feedUrl = q.value(1).toString();
128     addSingleNewsAnyDate_ = q.value(2).toBool();
129     avoidedOldSingleNews_ = q.value(3).toBool();
130     avoidedOldSingleNewsDate_ = q.value(4).toDate();
131   }
132 
133   // id not found (ex. feed deleted while updating)
134   if (feedUrl.isEmpty()) {
135     qWarning() << QString("Feed with id = '%1' not found").arg(parseFeedId_);
136     emit signalFinishUpdate(parseFeedId_, false, 0, "0");
137     db_.commit();
138     return;
139   }
140 
141   qDebug() << QString("Feed '%1' found with id = %2").arg(feedUrl).arg(parseFeedId_);
142 
143   // actually parsing
144   feedChanged_ = false;
145   lastBuildDate_ = dtReply;
146 
147   bool codecOk = false;
148   QString convertData(xmlData);
149   QString feedType;
150   QDomDocument doc;
151   QString errorStr;
152   int errorLine;
153   int errorColumn;
154 
155   QzRegExp rx("encoding=\"([^\"]+)", Qt::CaseInsensitive);
156   int pos = rx.indexIn(xmlData);
157   if (pos == -1) {
158     rx.setPattern("encoding='([^']+)");
159     pos = rx.indexIn(xmlData);
160   }
161   if (pos > -1) {
162     QString codecNameT = rx.cap(1);
163     qDebug() << "Codec name (1):" << codecNameT;
164     QTextCodec *codec = QTextCodec::codecForName(codecNameT.toUtf8());
165     if (codec) {
166       convertData = codec->toUnicode(xmlData);
167     } else {
168       qWarning() << "Codec not found (1): " << codecNameT << feedUrl;
169       if (codecNameT.contains("us-ascii", Qt::CaseInsensitive)) {
170         QString str(xmlData);
171         convertData = str.remove(rx.cap(0)+"\"");
172       }
173     }
174   } else {
175     if (!codecName.isEmpty()) {
176       qDebug() << "Codec name (2):" << codecName;
177       QTextCodec *codec = QTextCodec::codecForName(codecName.toUtf8());
178       if (codec) {
179         convertData = codec->toUnicode(xmlData);
180         codecOk = true;
181       } else {
182         qWarning() << "Codec not found (2): " << codecName << feedUrl;
183       }
184     }
185     if (!codecOk) {
186       codecOk = false;
187       QStringList codecNameList;
188       codecNameList << "UTF-8" << "Windows-1251" << "KOI8-R" << "KOI8-U"
189                     << "ISO 8859-5" << "IBM 866";
190       foreach (QString codecNameT, codecNameList) {
191         QTextCodec *codec = QTextCodec::codecForName(codecNameT.toUtf8());
192         if (codec && codec->canEncode(xmlData)) {
193           qDebug() << "Codec name (3):" << codecNameT;
194           convertData = codec->toUnicode(xmlData);
195           codecOk = true;
196           break;
197         }
198       }
199       if (!codecOk) {
200         convertData = QString::fromLocal8Bit(xmlData);
201       }
202     }
203   }
204 
205   if (!doc.setContent(convertData, false, &errorStr, &errorLine, &errorColumn)) {
206     qWarning() << QString("Parse data error (2): url %1, id %2, line %3, column %4: %5").
207                   arg(feedUrl).arg(parseFeedId_).
208                   arg(errorLine).arg(errorColumn).arg(errorStr);
209   } else {
210     QDomElement rootElem = doc.documentElement();
211     feedType = rootElem.tagName();
212     qDebug() << "Feed type: " << feedType;
213 
214     q.exec(QString("SELECT id, guid, title, published, link_href FROM news WHERE feedId='%1'").
215            arg(parseFeedId_));
216     if (q.lastError().isValid()) {
217       qWarning() << __PRETTY_FUNCTION__ << __LINE__
218                  << "q.lastError(): " << q.lastError().text();
219     }
220     else {
221       while (q.next()) {
222         QString str = q.value(2).toString();
223         titleList_.append(str);
224 
225         str = q.value(1).toString();
226         guidList_.append(str);
227 
228         str = q.value(3).toString();
229         publishedList_.append(str);
230         str = q.value(4).toString();
231         linkList_.append(str);
232       }
233     }
234     q.finish();
235 
236     if (feedType == "feed") {
237       parseAtom(feedUrl, doc);
238     } else if ((feedType == "rss") || (feedType == "rdf:RDF")) {
239       parseRss(feedUrl, doc);
240     }
241 
242     guidList_.clear();
243     titleList_.clear();
244     publishedList_.clear();
245     linkList_.clear();
246   }
247 
248   // Set feed update time and receive data from server time
249   QString updated = QLocale::c().toString(QDateTime::currentDateTimeUtc(),
250                                           "yyyy-MM-ddTHH:mm:ss");
251   QString lastBuildDate = lastBuildDate_.toString(Qt::ISODate);
252   q.prepare("UPDATE feeds SET updated=?, lastBuildDate=?, status=0 WHERE id=?");
253   q.addBindValue(updated);
254   q.addBindValue(lastBuildDate);
255   q.addBindValue(parseFeedId_);
256   q.exec();
257 
258   int newCount = 0;
259   if (feedChanged_) {
260     runUserFilter(parseFeedId_);
261     newCount = recountFeedCounts(parseFeedId_, feedUrl, updated, lastBuildDate);
262   }
263 
264   q.finish();
265   db_.commit();
266 
267   emit signalFinishUpdate(parseFeedId_, feedChanged_, newCount, "0");
268   qDebug() << "=================== parseXml:finish ===========================";
269 }
270 
parseAtom(const QString & feedUrl,const QDomDocument & doc)271 void ParseObject::parseAtom(const QString &feedUrl, const QDomDocument &doc)
272 {
273   QDomElement rootElem = doc.documentElement();
274   FeedItemStruct feedItem;
275 
276   feedItem.linkBase = rootElem.attribute("xml:base");
277   feedItem.title = toPlainText(rootElem.namedItem("title").toElement().text());
278   feedItem.description = rootElem.namedItem("subtitle").toElement().text();
279   feedItem.updated = rootElem.namedItem("updated").toElement().text();
280   feedItem.updated = parseDate(feedItem.updated, feedUrl);
281   QDomElement authorElem = rootElem.namedItem("author").toElement();
282   if (!authorElem.isNull()) {
283     feedItem.author = toPlainText(authorElem.namedItem("name").toElement().text());
284     if (feedItem.author.isEmpty()) feedItem.author = toPlainText(authorElem.text());
285     feedItem.authorUri = authorElem.namedItem("uri").toElement().text();
286     feedItem.authorEmail = authorElem.namedItem("email").toElement().text();
287   }
288   feedItem.language = rootElem.namedItem("language").toElement().text();
289   QDomNodeList linksList = rootElem.elementsByTagName("link");
290   for (int j = 0; j < linksList.size(); j++) {
291     if (linksList.at(j).toElement().attribute("rel") == "alternate") {
292       feedItem.link = linksList.at(j).toElement().attribute("href");
293       break;
294     }
295   }
296   if (feedItem.link.isEmpty()) {
297     for (int j = 0; j < linksList.size(); j++) {
298         if (!(linksList.at(j).toElement().attribute("rel") == "self")) {
299           feedItem.link = linksList.at(j).toElement().attribute("href");
300           break;
301         }
302     }
303   }
304 
305   if (QUrl(feedItem.link).host().isEmpty() || (QUrl(feedItem.link).host().indexOf('.')) == -1) {
306     if (!feedItem.linkBase.isEmpty() && !QUrl(feedItem.linkBase).host().isEmpty())
307       feedItem.link = QUrl(feedItem.linkBase).scheme() %  "://" % QUrl(feedItem.linkBase).host();
308     else
309       feedItem.link = QUrl(feedUrl).scheme() %  "://" % QUrl(feedUrl).host();
310   }
311   if (feedItem.linkBase.isEmpty() && !QUrl(feedItem.link).host().isEmpty())
312     feedItem.linkBase = QUrl(feedItem.link).scheme() %  "://" % QUrl(feedItem.link).host();
313   if (QUrl(feedItem.link).host().isEmpty())
314     feedItem.link = feedItem.linkBase + feedItem.link;
315   feedItem.link = toPlainText(feedItem.link);
316   QUrl url = QUrl(feedItem.link);
317   if (url.scheme().isEmpty())
318     url.setScheme(QUrl(feedUrl).scheme());
319   feedItem.link = url.toString();
320 
321   QSqlQuery q(db_);
322   q.setForwardOnly(true);
323   QString qStr ("UPDATE feeds "
324                 "SET title=?, description=?, htmlUrl=?, "
325                 "author_name=?, author_email=?, "
326                 "author_uri=?, pubdate=?, language=? "
327                 "WHERE id==?");
328   q.prepare(qStr);
329   q.addBindValue(feedItem.title);
330   q.addBindValue(feedItem.description);
331   q.addBindValue(feedItem.link);
332   q.addBindValue(feedItem.author);
333   q.addBindValue(feedItem.authorEmail);
334   q.addBindValue(feedItem.authorUri);
335   q.addBindValue(feedItem.updated);
336   q.addBindValue(feedItem.language);
337   q.addBindValue(parseFeedId_);
338   q.exec();
339 
340   QDomNodeList newsList = doc.elementsByTagName("entry");
341   for (int i = 0; i < newsList.size(); i++) {
342     NewsItemStruct newsItem;
343     newsItem.id = newsList.item(i).namedItem("id").toElement().text();
344     newsItem.title = toPlainText(newsList.item(i).namedItem("title").toElement().text());
345     newsItem.updated = newsList.item(i).namedItem("published").toElement().text();
346     if (newsItem.updated.isEmpty())
347       newsItem.updated = newsList.item(i).namedItem("updated").toElement().text();
348     newsItem.updated = parseDate(newsItem.updated, feedUrl);
349     QDomElement authorElem = newsList.item(i).namedItem("author").toElement();
350     if (!authorElem.isNull()) {
351       newsItem.author = toPlainText(authorElem.namedItem("name").toElement().text());
352       if (newsItem.author.isEmpty()) newsItem.author = toPlainText(authorElem.text());
353       newsItem.authorUri = authorElem.namedItem("uri").toElement().text();
354       newsItem.authorEmail = authorElem.namedItem("email").toElement().text();
355     }
356 
357     newsItem.description = newsList.item(i).namedItem("summary").toElement().text();
358     QDomNode nodeSummary = newsList.item(i).namedItem("summary");
359     if (!nodeSummary.isNull() && newsItem.description.isEmpty()) {
360       QTextStream in(&newsItem.description);
361       nodeSummary.save(in, 0);
362     }
363     QDomNode nodeContent = newsList.item(i).namedItem("content");
364     if (nodeContent.toElement().attribute("type") == "xhtml") {
365       QTextStream in(&newsItem.content);
366       nodeContent.save(in, 0);
367     } else {
368       newsItem.content = nodeContent.toElement().text();
369     }
370     QString imgUrl = newsList.item(i).namedItem("media:thumbnail").toElement().attribute("url");
371     QString community = getCommunity(newsList.item(i).namedItem("media:community"));
372     nodeContent = newsList.item(i).namedItem("media:group");
373     if (!nodeContent.isNull()) {
374       QString description = nodeContent.namedItem("media:description").toElement().text();
375       if (description.length() > newsItem.content.length())
376         newsItem.content = description;
377       newsItem.content = fromPlainText(newsItem.content);
378       if (imgUrl.isEmpty())
379         imgUrl = nodeContent.namedItem("media:thumbnail").toElement().attribute("url");
380       if (community.isEmpty())
381         community = getCommunity(nodeContent.namedItem("media:community"));
382     }
383     if (!(newsItem.content.isEmpty() ||
384           (newsItem.description.length() > newsItem.content.length()))) {
385       newsItem.description = newsItem.content;
386     }
387     newsItem.content.clear();
388     if (!imgUrl.isEmpty()) {
389       newsItem.description = "<p class=\"description\">" + newsItem.description + "</p>";
390       newsItem.description += "<img src=\"" + imgUrl + "\" alt=\"image\"/>";
391     }
392     if (!community.isEmpty())
393       newsItem.description += community;
394 
395     QDomNodeList categoryElem = newsList.item(i).toElement().elementsByTagName("category");
396     for (int j = 0; j < categoryElem.size(); j++) {
397       if (!newsItem.category.isEmpty()) newsItem.category.append(", ");
398       QString category = categoryElem.at(j).toElement().attribute("label");
399       if (category.isEmpty())
400         category = categoryElem.at(j).toElement().attribute("term");
401       newsItem.category.append(toPlainText(category));
402     }
403     QDomElement enclosureElem = newsList.item(i).namedItem("enclosure").toElement();
404     newsItem.eUrl = enclosureElem.attribute("url");
405     newsItem.eType = enclosureElem.attribute("type");
406     newsItem.eLength = enclosureElem.attribute("length");
407     QDomNodeList linksList = newsList.item(i).toElement().elementsByTagName("link");
408     for (int j = 0; j < linksList.size(); j++) {
409       if (linksList.at(j).toElement().attribute("type") == "text/html") {
410         if (linksList.at(j).toElement().attribute("rel") == "self")
411           newsItem.link = linksList.at(j).toElement().attribute("href");
412         if (linksList.at(j).toElement().attribute("rel") == "alternate")
413           newsItem.linkAlternate = linksList.at(j).toElement().attribute("href");
414         if (linksList.at(j).toElement().attribute("rel") == "replies")
415           newsItem.comments = linksList.at(j).toElement().attribute("href");
416       } else if (newsItem.linkAlternate.isEmpty()) {
417         if (linksList.at(j).toElement().attribute("rel") == "alternate")
418           newsItem.linkAlternate = linksList.at(j).toElement().attribute("href");
419       }
420     }
421     for (int j = 0; j < linksList.size(); j++) {
422       if (newsItem.linkAlternate.isEmpty()) {
423         if (!(linksList.at(j).toElement().attribute("rel") == "self")) {
424           newsItem.linkAlternate = linksList.at(j).toElement().attribute("href");
425           break;
426         }
427       }
428     }
429 
430     if (!newsItem.link.isEmpty() && QUrl(newsItem.link).host().isEmpty())
431       newsItem.link = feedItem.linkBase + newsItem.link;
432     newsItem.link = toPlainText(newsItem.link);
433     if (!newsItem.linkAlternate.isEmpty() && QUrl(newsItem.linkAlternate).host().isEmpty())
434       newsItem.linkAlternate = feedItem.linkBase + newsItem.linkAlternate;
435     newsItem.linkAlternate = toPlainText(newsItem.linkAlternate);
436     if (newsItem.link.isEmpty()) {
437       newsItem.link = newsItem.linkAlternate;
438       newsItem.linkAlternate.clear();
439     }
440     url = QUrl(newsItem.link);
441     if (url.scheme().isEmpty())
442       url.setScheme(QUrl(feedUrl).scheme());
443     newsItem.link = url.toString();
444 
445     addAtomNewsIntoBase(&newsItem);
446   }
447 }
448 
addAtomNewsIntoBase(NewsItemStruct * newsItem)449 void ParseObject::addAtomNewsIntoBase(NewsItemStruct *newsItem)
450 {
451   Common::sleep(5);
452 
453   // search news duplicates in base
454   QSqlQuery q(db_);
455   q.setForwardOnly(true);
456   QString qStr;
457   qDebug() << "atomId:" << newsItem->id;
458   qDebug() << "title:" << newsItem->title;
459   qDebug() << "published:" << newsItem->updated;
460 
461   bool isDuplicate = false;
462   for (int i = 0; i < guidList_.count(); ++i) {
463     if (!newsItem->id.isEmpty()) {         // search by guid if present
464       if (guidList_.at(i) == newsItem->id) {
465         if (duplicateNewsMode_) {       // autodelete duplicate news enabled
466           isDuplicate = true;
467         } else {                        // autodelete dupl. news disabled
468           if (!newsItem->updated.isEmpty()) {  // search by pubDate if present
469             if (publishedList_.at(i) == newsItem->updated)
470               isDuplicate = true;
471           } else {                      // ... or by title
472             if (!newsItem->title.isEmpty() && (titleList_.at(i) == newsItem->title))
473               isDuplicate = true;
474           }
475         }
476       }
477     } else {                                // guid is absent
478       if (!newsItem->updated.isEmpty()) {    // search by pubDate if present
479         if (publishedList_.at(i) == newsItem->updated)
480           isDuplicate = true;
481       } else {                              // ... or by title
482         if (!newsItem->title.isEmpty() && (titleList_.at(i) == newsItem->title))
483           isDuplicate = true;
484       }
485     }
486     if (isDuplicate) break;
487   }
488 
489   // Verify old news before a date to avoid adding them to base
490   bool isOld = false;
491   QDateTime pubDate_ = QDateTime::fromString(newsItem->updated, "yyyy-MM-ddTHH:mm:ss");
492   QDateTime avoidedDate_ = QDateTime(mainApp->mainWindow()->avoidedOldNewsDate_);
493   if (!addSingleNewsAnyDate_) {      //
494     if (avoidedOldSingleNews_ ) {     // avoid adding old single news
495       if (QDateTime(avoidedOldSingleNewsDate_) > pubDate_)
496         isOld = true;
497       } else if (mainApp->mainWindow()->avoidOldNews_ && avoidedDate_ > pubDate_) {   // avoid adding old news
498         isOld = true;
499       }
500    }
501 
502   // if duplicates not found and is old news, add them into base
503   if (!isDuplicate && !isOld) {
504     bool read = false;
505     if (mainApp->mainWindow()->markIdenticalNewsRead_) {
506       q.prepare("SELECT id FROM news WHERE title LIKE :title AND feedId!=:id");
507       q.bindValue(":id", parseFeedId_);
508       q.bindValue(":title", newsItem->title);
509       q.exec();
510       if (q.first()) read = true;
511     }
512 
513     qStr = QString("INSERT INTO news("
514                    "feedId, description, content, guid, title, author_name, "
515                    "author_uri, author_email, published, received, "
516                    "link_href, link_alternate, category, comments, "
517                    "enclosure_url, enclosure_type, enclosure_length, new, read) "
518                    "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
519     q.prepare(qStr);
520     q.addBindValue(parseFeedId_);
521     q.addBindValue(newsItem->description);
522     q.addBindValue(newsItem->content);
523     q.addBindValue(newsItem->id);
524     q.addBindValue(newsItem->title);
525     q.addBindValue(newsItem->author);
526     q.addBindValue(newsItem->authorUri);
527     q.addBindValue(newsItem->authorEmail);
528     QString updated = newsItem->updated;
529     if (updated.isEmpty())
530       updated = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
531     q.addBindValue(updated);
532     q.addBindValue(QDateTime::currentDateTime().toString(Qt::ISODate));
533     q.addBindValue(newsItem->link);
534     q.addBindValue(newsItem->linkAlternate);
535     q.addBindValue(newsItem->category);
536     q.addBindValue(newsItem->comments);
537     q.addBindValue(newsItem->eUrl);
538     q.addBindValue(newsItem->eType);
539     q.addBindValue(newsItem->eLength);
540     q.addBindValue(read ? 0 : 1);
541     q.addBindValue(read ? 2 : 0);
542     if (!q.exec()) {
543       qWarning() << __PRETTY_FUNCTION__ << __LINE__
544                  << "q.lastError(): " << q.lastError().text();
545     }
546     q.finish();
547     qDebug() << "q.exec(" << q.lastQuery() << ")";
548     qDebug() << "       " << parseFeedId_;
549     qDebug() << "       " << newsItem->description;
550     qDebug() << "       " << newsItem->content;
551     qDebug() << "       " << newsItem->id;
552     qDebug() << "       " << newsItem->title;
553     qDebug() << "       " << newsItem->author;
554     qDebug() << "       " << newsItem->authorUri;
555     qDebug() << "       " << newsItem->authorEmail;
556     qDebug() << "       " << newsItem->updated;
557     qDebug() << "       " << QDateTime::currentDateTime().toString();
558     qDebug() << "       " << newsItem->link;
559     qDebug() << "       " << newsItem->linkAlternate;
560     qDebug() << "       " << newsItem->category;
561     qDebug() << "       " << newsItem->comments;
562     qDebug() << "       " << newsItem->eUrl;
563     qDebug() << "       " << newsItem->eType;
564     qDebug() << "       " << newsItem->eLength;
565 
566     if (lastBuildDate_ < QDateTime::fromString(newsItem->updated, Qt::ISODate))
567       lastBuildDate_ = QDateTime::fromString(newsItem->updated, Qt::ISODate);
568     feedChanged_ = true;
569   }
570 }
571 
parseRss(const QString & feedUrl,const QDomDocument & doc)572 void ParseObject::parseRss(const QString &feedUrl, const QDomDocument &doc)
573 {
574   QDomNode channel = doc.documentElement().namedItem("channel");
575   if (channel.isNull())
576     channel = doc.documentElement().namedItem("rss:channel");
577   FeedItemStruct feedItem;
578 
579   feedItem.title = toPlainText(channel.namedItem("title").toElement().text());
580   if (feedItem.title.isEmpty())
581     feedItem.title = toPlainText(channel.namedItem("rss:title").toElement().text());
582   feedItem.description = channel.namedItem("description").toElement().text();
583   if (feedItem.description.isEmpty())
584     feedItem.description = toPlainText(channel.namedItem("rss:description").toElement().text());
585   feedItem.link = toPlainText(channel.namedItem("link").toElement().text());
586   if (feedItem.link.isEmpty())
587     feedItem.link = toPlainText(channel.namedItem("rss:link").toElement().text());
588   QUrl url = QUrl(feedItem.link);
589   if (url.host().isEmpty())
590     url.setHost(QUrl(feedUrl).host());
591   if (url.scheme().isEmpty())
592     url.setScheme(QUrl(feedUrl).scheme());
593   feedItem.link = url.toString();
594 
595   feedItem.updated = channel.namedItem("pubDate").toElement().text();
596   if (feedItem.updated.isEmpty())
597     feedItem.updated = channel.namedItem("pubdate").toElement().text();
598   feedItem.updated = parseDate(feedItem.updated, feedUrl);
599   feedItem.author = toPlainText(channel.namedItem("author").toElement().text());
600   feedItem.language = channel.namedItem("language").toElement().text();
601   if (feedItem.language.isEmpty())
602     feedItem.language = channel.namedItem("dc:language").toElement().text();
603 
604   QSqlQuery q(db_);
605   q.setForwardOnly(true);
606   QString qStr("UPDATE feeds "
607                "SET title=?, description=?, htmlUrl=?, "
608                "author_name=?, pubdate=?, language=? "
609                "WHERE id==?");
610   q.prepare(qStr);
611   q.addBindValue(feedItem.title);
612   q.addBindValue(feedItem.description);
613   q.addBindValue(feedItem.link);
614   q.addBindValue(feedItem.author);
615   q.addBindValue(feedItem.updated);
616   q.addBindValue(feedItem.language);
617   q.addBindValue(parseFeedId_);
618   q.exec();
619 
620   QDomNodeList newsList = doc.elementsByTagName("item");
621   if (newsList.isEmpty())
622     newsList = doc.elementsByTagName("rss:item");
623   for (int i = 0; i < newsList.size(); i++) {
624     NewsItemStruct newsItem;
625     newsItem.id = newsList.item(i).namedItem("guid").toElement().text();
626     newsItem.title = toPlainText(newsList.item(i).namedItem("title").toElement().text());
627     if (newsItem.title.isEmpty())
628       newsItem.title = toPlainText(newsList.item(i).namedItem("rss:title").toElement().text());
629     newsItem.updated = newsList.item(i).namedItem("pubDate").toElement().text();
630     if (newsItem.updated.isEmpty())
631       newsItem.updated = newsList.item(i).namedItem("pubdate").toElement().text();
632     if (newsItem.updated.isEmpty())
633       newsItem.updated = newsList.item(i).namedItem("dc:date").toElement().text();
634     newsItem.updated = parseDate(newsItem.updated, feedUrl);
635     newsItem.author = toPlainText(newsList.item(i).namedItem("author").toElement().text());
636     if (newsItem.author.isEmpty())
637       newsItem.author = toPlainText(newsList.item(i).namedItem("dc:creator").toElement().text());
638     newsItem.link = toPlainText(newsList.item(i).namedItem("link").toElement().text());
639     if (newsItem.link.isEmpty()) {
640         newsItem.link = toPlainText(newsList.item(i).namedItem("rss:link").toElement().text());
641         if (newsItem.link.isEmpty()) {
642             if (newsList.item(i).namedItem("guid").toElement().attribute("isPermaLink") == "true")
643                 newsItem.link = newsItem.id;
644         }
645     }
646     url = QUrl(newsItem.link);
647     if (url.host().isEmpty())
648       url.setHost(QUrl(feedUrl).host());
649     if (url.scheme().isEmpty())
650       url.setScheme(QUrl(feedUrl).scheme());
651     newsItem.link = url.toString();
652 
653     newsItem.description = newsList.item(i).namedItem("description").toElement().text();
654     QDomNode nodeSummary = newsList.item(i).namedItem("description");
655     if (!nodeSummary.isNull() && newsItem.description.isEmpty()) {
656       QTextStream in(&newsItem.description);
657       nodeSummary.save(in, 0);
658     }
659     newsItem.content = newsList.item(i).namedItem("content:encoded").toElement().text();
660     QDomNode nodeContent = newsList.item(i).namedItem("content:encoded");
661     if (!nodeContent.isNull() && newsItem.content.isEmpty()) {
662       QTextStream in(&newsItem.content);
663       nodeContent.save(in, 0);
664     }
665     QString imgUrl = newsList.item(i).namedItem("media:thumbnail").toElement().attribute("url");
666     QString community = getCommunity(newsList.item(i).namedItem("media:community"));
667     nodeContent = newsList.item(i).namedItem("media:group");
668     if (!nodeContent.isNull()) {
669       QString description = nodeContent.namedItem("media:description").toElement().text();
670       if (description.length() > newsItem.content.length())
671         newsItem.content = description;
672       newsItem.content = fromPlainText(newsItem.content);
673       if (imgUrl.isEmpty())
674         imgUrl = nodeContent.namedItem("media:thumbnail").toElement().attribute("url");
675       if (community.isEmpty())
676         community = getCommunity(nodeContent.namedItem("media:community"));
677     }
678     if (!(newsItem.content.isEmpty() ||
679           (newsItem.description.length() > newsItem.content.length()))) {
680       newsItem.description = newsItem.content;
681     }
682     newsItem.content.clear();
683     if (!imgUrl.isEmpty()) {
684       newsItem.description = "<p class=\"description\">" + newsItem.description + "</p>";
685       newsItem.description += "<img src=\"" + imgUrl + "\" alt=\"image\"/>";
686     }
687     if (!community.isEmpty())
688       newsItem.description += community;
689 
690     QDomNodeList categoryElem = newsList.item(i).toElement().elementsByTagName("category");
691     for (int j = 0; j < categoryElem.size(); j++) {
692       if (!newsItem.category.isEmpty()) newsItem.category.append(", ");
693       newsItem.category.append(toPlainText(categoryElem.at(j).toElement().text()));
694     }
695     newsItem.comments = newsList.item(i).namedItem("comments").toElement().text();
696     QDomElement enclosureElem = newsList.item(i).namedItem("enclosure").toElement();
697     newsItem.eUrl = enclosureElem.attribute("url");
698     newsItem.eType = enclosureElem.attribute("type");
699     newsItem.eLength = enclosureElem.attribute("length");
700 
701     if (newsItem.title.isEmpty()) {
702       newsItem.title = toPlainText(newsItem.description);
703       if (newsItem.title.size() > 50) {
704         newsItem.title.resize(50);
705         newsItem.title = newsItem.title % "...";
706       }
707     }
708 
709     addRssNewsIntoBase(&newsItem);
710   }
711 }
712 
addRssNewsIntoBase(NewsItemStruct * newsItem)713 void ParseObject::addRssNewsIntoBase(NewsItemStruct *newsItem)
714 {
715   Common::sleep(5);
716 
717   // search news duplicates in base
718   QSqlQuery q(db_);
719   q.setForwardOnly(true);
720   QString qStr;
721   qDebug() << "guid:     " << newsItem->id;
722   qDebug() << "link_href:" << newsItem->link;
723   qDebug() << "title:"     << newsItem->title;
724   qDebug() << "published:" << newsItem->updated;
725 
726   bool isDuplicate = false;
727   for (int i = 0; i < guidList_.count(); ++i) {
728     if (!newsItem->id.isEmpty()) {         // search by guid if present
729       if (guidList_.at(i) == newsItem->id) {
730         if (!newsItem->updated.isEmpty()) {  // search by pubDate if present
731           if (!duplicateNewsMode_) {
732             if (publishedList_.at(i) == newsItem->updated)
733               isDuplicate = true;
734           }
735           else {
736             isDuplicate = true;
737           }
738         } else {                            // ... or by title
739           if (!newsItem->title.isEmpty() && (titleList_.at(i) == newsItem->title))
740             isDuplicate = true;
741         }
742       }
743       if (!isDuplicate) {
744         if (!newsItem->updated.isEmpty()) {
745           if ((publishedList_.at(i) == newsItem->updated) &&
746               (titleList_.at(i) == newsItem->title)) {
747             isDuplicate = true;
748           }
749         }
750       }
751     }
752     else if (!newsItem->link.isEmpty()) {   // search by link_href
753       if (linkList_.at(i) == newsItem->link) {
754         if (!newsItem->updated.isEmpty()) {  // search by pubDate if present
755           if (!duplicateNewsMode_) {
756             if (publishedList_.at(i) == newsItem->updated)
757               isDuplicate = true;
758           }
759           else {
760             isDuplicate = true;
761           }
762         } else {                            // ... or by title
763           if (!newsItem->title.isEmpty() && (titleList_.at(i) == newsItem->title))
764             isDuplicate = true;
765         }
766       }
767       if (!isDuplicate) {
768         if (!newsItem->updated.isEmpty()) {
769           if ((publishedList_.at(i) == newsItem->updated) &&
770               (titleList_.at(i) == newsItem->title)) {
771             isDuplicate = true;
772           }
773         }
774       }
775     }
776     else {                                // guid is absent
777       if (!newsItem->updated.isEmpty()) {  // search by pubDate if present
778         if (!duplicateNewsMode_) {
779           if (publishedList_.at(i) == newsItem->updated)
780             isDuplicate = true;
781         }
782         else {
783           isDuplicate = true;
784         }
785       } else {                            // ... or by title
786         if (!newsItem->title.isEmpty() && (titleList_.at(i) == newsItem->title))
787           isDuplicate = true;
788       }
789       if (!isDuplicate) {
790         if (!newsItem->updated.isEmpty()) {
791           if ((publishedList_.at(i) == newsItem->updated) &&
792               (titleList_.at(i) == newsItem->title)) {
793             isDuplicate = true;
794           }
795         }
796       }
797     }
798     if (isDuplicate) break;
799   }
800 
801   // Verify old news before a date to avoid adding them to base
802   bool isOld = false;
803   QDateTime pubDate_ = QDateTime::fromString(newsItem->updated, "yyyy-MM-ddTHH:mm:ss");
804   QDateTime avoidedDate_ = QDateTime(mainApp->mainWindow()->avoidedOldNewsDate_);
805   if (!addSingleNewsAnyDate_) {      //
806     if (avoidedOldSingleNews_ ) {     // avoid adding old single news
807       if (QDateTime(avoidedOldSingleNewsDate_) > pubDate_)
808         isOld = true;
809       } else if (mainApp->mainWindow()->avoidOldNews_ && avoidedDate_ > pubDate_) {   // avoid adding old news
810               isOld = true;
811       }
812    }
813 
814  // if duplicates not found And old news, add them into base
815  if (!isDuplicate && !isOld) {
816     bool read = false;
817     if (mainApp->mainWindow()->markIdenticalNewsRead_) {
818       q.prepare("SELECT id FROM news WHERE title LIKE :title AND feedId!=:id");
819       q.bindValue(":id", parseFeedId_);
820       q.bindValue(":title", newsItem->title);
821       q.exec();
822       if (q.first()) read = true;
823     }
824 
825     qStr = QString("INSERT INTO news("
826                    "feedId, description, content, guid, title, author_name, "
827                    "published, received, link_href, category, comments, "
828                    "enclosure_url, enclosure_type, enclosure_length, new, read) "
829                    "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
830     q.prepare(qStr);
831     q.addBindValue(parseFeedId_);
832     q.addBindValue(newsItem->description);
833     q.addBindValue(newsItem->content);
834     q.addBindValue(newsItem->id);
835     q.addBindValue(newsItem->title);
836     q.addBindValue(newsItem->author);
837     QString updated = newsItem->updated;
838     if (updated.isEmpty())
839       updated = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
840     q.addBindValue(updated);
841     q.addBindValue(QDateTime::currentDateTime().toString(Qt::ISODate));
842     q.addBindValue(newsItem->link);
843     q.addBindValue(newsItem->category);
844     q.addBindValue(newsItem->comments);
845     q.addBindValue(newsItem->eUrl);
846     q.addBindValue(newsItem->eType);
847     q.addBindValue(newsItem->eLength);
848     q.addBindValue(read ? 0 : 1);
849     q.addBindValue(read ? 2 : 0);
850     if (!q.exec()) {
851       qWarning() << __PRETTY_FUNCTION__ << __LINE__
852                  << "q.lastError(): " << q.lastError().text();
853     }
854     q.finish();
855     qDebug() << "q.exec(" << q.lastQuery() << ")";
856     qDebug() << "       " << parseFeedId_;
857     qDebug() << "       " << newsItem->description;
858     qDebug() << "       " << newsItem->content;
859     qDebug() << "       " << newsItem->id;
860     qDebug() << "       " << newsItem->title;
861     qDebug() << "       " << newsItem->author;
862     qDebug() << "       " << newsItem->updated;
863     qDebug() << "       " << QDateTime::currentDateTime().toString();
864     qDebug() << "       " << newsItem->link;
865     qDebug() << "       " << newsItem->category;
866     qDebug() << "       " << newsItem->comments;
867     qDebug() << "       " << newsItem->eUrl;
868     qDebug() << "       " << newsItem->eType;
869     qDebug() << "       " << newsItem->eLength;
870 
871     if (lastBuildDate_ < QDateTime::fromString(newsItem->updated, Qt::ISODate))
872       lastBuildDate_ = QDateTime::fromString(newsItem->updated, Qt::ISODate);
873     feedChanged_ = true;
874   }
875 }
876 
toPlainText(const QString & text)877 QString ParseObject::toPlainText(const QString &text)
878 {
879   return QTextDocumentFragment::fromHtml(text).toPlainText().simplified();
880 }
881 
fromPlainText(QString text)882 QString ParseObject::fromPlainText(QString text)
883 {
884   text = text.replace("\r\n", "<br>");
885   text = text.replace("\n", "<br>");
886   return text;
887 }
888 
getCommunity(const QDomNode & nodeContent)889 QString ParseObject::getCommunity(const QDomNode &nodeContent)
890 {
891   QString community;
892   if (!nodeContent.isNull()) {
893     QString count = nodeContent.namedItem("media:starRating").toElement().attribute("count");
894     QString average = nodeContent.namedItem("media:starRating").toElement().attribute("average");
895     QString min = nodeContent.namedItem("media:starRating").toElement().attribute("min");
896     QString max = nodeContent.namedItem("media:starRating").toElement().attribute("max");
897     QString views = nodeContent.namedItem("media:statistics").toElement().attribute("views");
898     if (!count.isEmpty())
899       community = QString("Count: %1, average: %2, min: %3, max: %4<br>").
900           arg(count).arg(average).arg(min).arg(max);
901     if (!views.isEmpty())
902       community += QString("Views: %1").arg(views);
903     if (!community.isEmpty())
904       community = "<p><i>" + community + "</i></p>";
905   }
906   return community;
907 }
908 
909 /** @brief Date/time string parsing
910  *----------------------------------------------------------------------------*/
parseDate(const QString & dateString,const QString & urlString)911 QString ParseObject::parseDate(const QString &dateString, const QString &urlString)
912 {
913   QDateTime dt;
914   QString temp;
915   QString timeZone;
916 
917   if (dateString.isEmpty()) return QString();
918 
919   QDateTime dtLocalTime = QDateTime::currentDateTime();
920   QDateTime dtUTC = QDateTime(dtLocalTime.date(), dtLocalTime.time(), Qt::UTC);
921   int nTimeShift = dtLocalTime.secsTo(dtUTC)/3600;
922 
923   QString ds = dateString.simplified();
924   QLocale locale(QLocale::C);
925 
926   if (ds.indexOf(',') != -1) {
927     ds = ds.remove(0, ds.indexOf(',')+1).simplified();
928   }
929 
930   for (int i = 0; i < 2; i++, locale = QLocale::system()) {
931     temp     = ds.left(23);
932     timeZone = ds.mid(temp.length(), 3);
933     if (timeZone.isEmpty()) timeZone = QString::number(nTimeShift);
934     dt = locale.toDateTime(temp, "yyyy-MM-ddTHH:mm:ss.z");
935     if (dt.isValid()) return locale.toString(dt.addSecs(timeZone.toInt() * -3600), "yyyy-MM-ddTHH:mm:ss");
936 
937     temp     = ds.left(19);
938     timeZone = ds.mid(temp.length(), 3);
939     if (timeZone.isEmpty()) timeZone = QString::number(nTimeShift);
940     dt = locale.toDateTime(temp, "yyyy-MM-ddTHH:mm:ss");
941     if (dt.isValid()) return locale.toString(dt.addSecs(timeZone.toInt() * -3600), "yyyy-MM-ddTHH:mm:ss");
942 
943     temp = ds.left(23);
944     timeZone = ds.mid(temp.length()+1, 3);
945     if (timeZone.isEmpty()) timeZone = QString::number(nTimeShift);
946     dt = locale.toDateTime(temp, "yyyy-MM-dd HH:mm:ss.z");
947     if (dt.isValid()) return locale.toString(dt.addSecs(timeZone.toInt() * -3600), "yyyy-MM-ddTHH:mm:ss");
948 
949     temp = ds.left(19);
950     timeZone = ds.mid(temp.length()+1, 3);
951     if (timeZone.isEmpty()) timeZone = QString::number(nTimeShift);
952     dt = locale.toDateTime(temp, "yyyy-MM-dd HH:mm:ss");
953     if (dt.isValid()) return locale.toString(dt.addSecs(timeZone.toInt() * -3600), "yyyy-MM-ddTHH:mm:ss");
954 
955     temp = ds.left(20);
956     timeZone = ds.mid(temp.length()+1, 3);
957     if (timeZone.contains("EDT"))
958       timeZone="-4";
959     if (timeZone.isEmpty()) timeZone = QString::number(nTimeShift);
960     dt = locale.toDateTime(temp, "dd MMM yyyy HH:mm:ss");
961     if (dt.isValid()) return locale.toString(dt.addSecs(timeZone.toInt() * -3600), "yyyy-MM-ddTHH:mm:ss");
962 
963     temp = ds.left(19);
964     timeZone = ds.mid(temp.length()+1, 3);
965     if (timeZone.isEmpty()) timeZone = QString::number(nTimeShift);
966     dt = locale.toDateTime(temp, "d MMM yyyy HH:mm:ss");
967     if (dt.isValid()) return locale.toString(dt.addSecs(timeZone.toInt() * -3600), "yyyy-MM-ddTHH:mm:ss");
968 
969     temp = ds.left(11);
970     timeZone = ds.mid(temp.length()+1, 3);
971     if (timeZone.isEmpty()) timeZone = QString::number(nTimeShift);
972     dt = locale.toDateTime(temp, "dd MMM yyyy");
973     if (dt.isValid()) return locale.toString(dt.addSecs(timeZone.toInt() * -3600), "yyyy-MM-ddTHH:mm:ss");
974 
975     temp = ds.left(10);
976     timeZone = ds.mid(temp.length()+1, 3);
977     if (timeZone.isEmpty()) timeZone = QString::number(nTimeShift);
978     dt = locale.toDateTime(temp, "d MMM yyyy");
979     if (dt.isValid()) return locale.toString(dt.addSecs(timeZone.toInt() * -3600), "yyyy-MM-ddTHH:mm:ss");
980 
981     temp = ds.left(10);
982     timeZone = ds.mid(temp.length(), 3);
983     if (timeZone.isEmpty()) timeZone = QString::number(nTimeShift);
984     dt = locale.toDateTime(temp, "yyyy-MM-dd");
985     if (dt.isValid()) return locale.toString(dt.addSecs(timeZone.toInt() * -3600), "yyyy-MM-ddTHH:mm:ss");
986 
987     // @HACK(arhohryakov:2012.01.01):
988     // "dd MMM yy HH:mm:ss" format doesn/t parse automatically
989     // Reformat it to "dd MMM yyyy HH:mm:ss"
990     QString temp2;
991     temp2 = ds;  // save ds for output in case of error
992     if (70 < ds.mid(7, 2).toInt()) temp2.insert(7, "19");
993     else temp2.insert(7, "20");
994     temp = temp2.left(20);
995     timeZone = ds.mid(temp.length()+1-2, 3);  // "-2", cause 2 symbols inserted
996     if (timeZone.isEmpty()) timeZone = QString::number(nTimeShift);
997     dt = locale.toDateTime(temp, "dd MMM yyyy HH:mm:ss");
998     if (dt.isValid()) return locale.toString(dt.addSecs(timeZone.toInt() * -3600), "yyyy-MM-ddTHH:mm:ss");
999   }
1000 
1001   qDebug() << __LINE__ << "parseDate: error with" << dateString << urlString;
1002   return QString();
1003 }
1004 
1005 /** @brief Apply user filters
1006  * @param feedId - Feed Id
1007  * @param filterId - Id of particular filter
1008  *---------------------------------------------------------------------------*/
runUserFilter(int feedId,int filterId)1009 void ParseObject::runUserFilter(int feedId, int filterId)
1010 {
1011   QSqlQuery q(db_);
1012   bool isAllFilters = true;
1013 
1014   if (filterId != -1) {
1015     isAllFilters = false;
1016     q.exec(QString("SELECT enable, type FROM filters WHERE id='%1' AND feeds LIKE '%,%2,%'").
1017            arg(filterId).arg(feedId));
1018   } else {
1019     q.exec(QString("SELECT enable, type, id FROM filters WHERE feeds LIKE '%,%1,%' ORDER BY num").
1020            arg(feedId));
1021   }
1022 
1023   while (q.next()) {
1024     if ((q.value(0).toInt() == 0) && isAllFilters) continue;
1025 
1026     if (isAllFilters)
1027       filterId = q.value(2).toInt();
1028     int filterType = q.value(1).toInt();
1029 
1030     QString qStr("UPDATE news SET");
1031     QString qStr1;
1032     QString qStr2;
1033     QString whereStr;
1034     QList<int> idLabelsList;
1035     QStringList soundList;
1036     QStringList colorList;
1037 
1038     QSqlQuery q1(db_);
1039     q1.exec(QString("SELECT action, params FROM filterActions "
1040                     "WHERE idFilter=='%1'").arg(filterId));
1041     while (q1.next()) {
1042       switch (q1.value(0).toInt()) {
1043       case 0: // action -> Mark news as read
1044         if (!qStr1.isNull()) qStr1.append(",");
1045         qStr1.append(" new=0, read=2");
1046         break;
1047       case 1: // action -> Add star
1048         if (!qStr1.isNull()) qStr1.append(",");
1049         qStr1.append(" starred=1");
1050         break;
1051       case 2: // action -> Delete
1052         if (!qStr1.isNull()) qStr1.append(",");
1053         qStr1.append(" new=0, read=2, deleted=1, ");
1054         qStr1.append(QString("deleteDate='%1'").
1055                      arg(QDateTime::currentDateTime().toString(Qt::ISODate)));
1056         break;
1057       case 3: // action -> Add Label
1058         idLabelsList.append(q1.value(1).toInt());
1059         break;
1060       case 4: // action -> Play Sound
1061         soundList.append(q1.value(1).toString());
1062         break;
1063       case 5: // action -> Show News in Notifier
1064         colorList.append(q1.value(1).toString());
1065         break;
1066       }
1067     }
1068 
1069     if (qStr1.isEmpty())
1070       qStr.clear();
1071     else
1072       qStr.append(qStr1);
1073 
1074     whereStr = QString(" WHERE feedId='%1' AND deleted=0").arg(feedId);
1075 
1076     qStr2.clear();
1077     switch (filterType) {
1078     case 1: // Match all conditions
1079       qStr2.append("AND ");
1080       break;
1081     case 2: // Match any condition
1082       qStr2.append("OR ");
1083       break;
1084     }
1085 
1086     if ((filterType == 1) || (filterType == 2)) {
1087       whereStr.append(" AND ( ");
1088       qStr1.clear();
1089 
1090       q1.exec(QString("SELECT field, condition, content FROM filterConditions "
1091                       "WHERE idFilter=='%1'").arg(filterId));
1092       while (q1.next()) {
1093         if (!qStr1.isNull()) qStr1.append(qStr2);
1094         QString content = q1.value(2).toString().replace("'", "''");
1095         switch (q1.value(0).toInt()) {
1096         case 0: // field -> Title
1097           switch (q1.value(1).toInt()) {
1098           case 0: // condition -> contains
1099             qStr1.append(QString("UPPER(title) LIKE '%%1%' ").arg(content.toUpper()));
1100             break;
1101           case 1: // condition -> doesn't contains
1102             qStr1.append(QString("UPPER(title) NOT LIKE '%%1%' ").arg(content.toUpper()));
1103             break;
1104           case 2: // condition -> is
1105             qStr1.append(QString("UPPER(title) LIKE '%1' ").arg(content.toUpper()));
1106             break;
1107           case 3: // condition -> isn't
1108             qStr1.append(QString("UPPER(title) NOT LIKE '%1' ").arg(content.toUpper()));
1109             break;
1110           case 4: // condition -> begins with
1111             qStr1.append(QString("UPPER(title) LIKE '%1%' ").arg(content.toUpper()));
1112             break;
1113           case 5: // condition -> ends with
1114             qStr1.append(QString("UPPER(title) LIKE '%%1' ").arg(content.toUpper()));
1115             break;
1116           case 6: // condition -> regExp
1117             qStr1.append(QString("title REGEXP '%1' ").arg(content));
1118             break;
1119           }
1120           break;
1121         case 1: // field -> Description
1122           switch (q1.value(1).toInt()) {
1123           case 0: // condition -> contains
1124             qStr1.append(QString("UPPER(description) LIKE '%%1%' ").arg(content.toUpper()));
1125             break;
1126           case 1: // condition -> doesn't contains
1127             qStr1.append(QString("UPPER(description) NOT LIKE '%%1%' ").arg(content.toUpper()));
1128             break;
1129           case 2: // condition -> regExp
1130             qStr1.append(QString("description REGEXP '%1' ").arg(content));
1131             break;
1132           }
1133           break;
1134         case 2: // field -> Author
1135           switch (q1.value(1).toInt()) {
1136           case 0: // condition -> contains
1137             qStr1.append(QString("UPPER(author_name) LIKE '%%1%' ").arg(content.toUpper()));
1138             break;
1139           case 1: // condition -> doesn't contains
1140             qStr1.append(QString("UPPER(author_name) NOT LIKE '%%1%' ").arg(content.toUpper()));
1141             break;
1142           case 2: // condition -> is
1143             qStr1.append(QString("UPPER(author_name) LIKE '%1' ").arg(content.toUpper()));
1144             break;
1145           case 3: // condition -> isn't
1146             qStr1.append(QString("UPPER(author_name) NOT LIKE '%1' ").arg(content.toUpper()));
1147             break;
1148           case 4: // condition -> regExp
1149             qStr1.append(QString("author_name REGEXP '%1' ").arg(content));
1150             break;
1151           }
1152           break;
1153         case 3: // field -> Category
1154           switch (q1.value(1).toInt()) {
1155           case 0: // condition -> contains
1156             qStr1.append(QString("UPPER(category) LIKE '%%1%' ").arg(content.toUpper()));
1157             break;
1158           case 1: // condition -> doesn't contains
1159             qStr1.append(QString("UPPER(category) NOT LIKE '%%1%' ").arg(content.toUpper()));
1160             break;
1161           case 2: // condition -> is
1162             qStr1.append(QString("UPPER(category) LIKE '%1' ").arg(content.toUpper()));
1163             break;
1164           case 3: // condition -> isn't
1165             qStr1.append(QString("UPPER(category) NOT LIKE '%1' ").arg(content.toUpper()));
1166             break;
1167           case 4: // condition -> begins with
1168             qStr1.append(QString("UPPER(category) LIKE '%1%' ").arg(content.toUpper()));
1169             break;
1170           case 5: // condition -> ends with
1171             qStr1.append(QString("UPPER(category) LIKE '%%1' ").arg(content.toUpper()));
1172             break;
1173           case 6: // condition -> regExp
1174             qStr1.append(QString("category REGEXP '%1' ").arg(content));
1175             break;
1176           }
1177           break;
1178         case 4: // field -> Status
1179           if (q1.value(1).toInt() == 0) { // Status -> is
1180             switch (q1.value(2).toInt()) {
1181             case 0:
1182               qStr1.append("new==1 ");
1183               break;
1184             case 1:
1185               qStr1.append("read>=1 ");
1186               break;
1187             case 2:
1188               qStr1.append("starred==1 ");
1189               break;
1190             }
1191           } else { // Status -> isn't
1192             switch (q1.value(2).toInt()) {
1193             case 0:
1194               qStr1.append("new==0 ");
1195               break;
1196             case 1:
1197               qStr1.append("read==0 ");
1198               break;
1199             case 2:
1200               qStr1.append("starred==0 ");
1201               break;
1202             }
1203           }
1204           break;
1205         case 5: // field -> Link
1206           switch (q1.value(1).toInt()) {
1207           case 0: // condition -> contains
1208             qStr1.append(QString("link_href LIKE '%%1%' ").arg(content));
1209             break;
1210           case 1: // condition -> doesn't contains
1211             qStr1.append(QString("link_href NOT LIKE '%%1%' ").arg(content));
1212             break;
1213           case 2: // condition -> is
1214             qStr1.append(QString("link_href LIKE '%1' ").arg(content));
1215             break;
1216           case 3: // condition -> isn't
1217             qStr1.append(QString("link_href NOT LIKE '%1' ").arg(content));
1218             break;
1219           case 4: // condition -> begins with
1220             qStr1.append(QString("link_href LIKE '%1%' ").arg(content));
1221             break;
1222           case 5: // condition -> ends with
1223             qStr1.append(QString("link_href LIKE '%%1' ").arg(content));
1224             break;
1225           case 6: // condition -> regExp
1226             qStr1.append(QString("link_href REGEXP '%1' ").arg(content));
1227             break;
1228           }
1229           break;
1230         case 6: // field -> News
1231           switch (q1.value(1).toInt()) {
1232           case 0: // condition -> contains
1233             qStr1.append(QString("(UPPER(title) LIKE '%%1%' OR UPPER(description) LIKE '%%1%') ").arg(content.toUpper()));
1234             break;
1235           case 1: // condition -> doesn't contains
1236             qStr1.append(QString("(UPPER(title) NOT LIKE '%%1%' OR UPPER(description) NOT LIKE '%%1%') ").arg(content.toUpper()));
1237             break;
1238           case 2: // condition -> regExp
1239             qStr1.append(QString("(title REGEXP '%1' OR description REGEXP '%1') ").arg(content));
1240             break;
1241           }
1242           break;
1243         }
1244       }
1245       whereStr.append(qStr1).append(")");
1246     }
1247 
1248     if (q1.exec(QString("SELECT id, label FROM news").append(whereStr))) {
1249       QSqlQuery q2(db_);
1250       bool isPlaySound = false;
1251 
1252       while (q1.next()) {
1253         if (!qStr.isEmpty()) {
1254           qStr1 = qStr % QString(" WHERE id='%1'").arg(q1.value(0).toInt());
1255           if (!q2.exec(qStr1)) {
1256             qWarning() << __PRETTY_FUNCTION__ << __LINE__
1257                        << "q.lastError(): " << q2.lastError().text();
1258           }
1259         }
1260 
1261         if (!idLabelsList.isEmpty()) {
1262           QString idLabelsStr = q1.value(1).toString();
1263           foreach (int idLabel, idLabelsList) {
1264             if (idLabelsStr.contains(QString(",%1,").arg(idLabel))) continue;
1265             if (idLabelsStr.isEmpty()) idLabelsStr.append(",");
1266             idLabelsStr.append(QString("%1,").arg(idLabel));
1267 
1268           }
1269           qStr1 = QString("UPDATE news SET label='%1' WHERE id='%2'").arg(idLabelsStr).
1270               arg(q1.value(0).toInt());
1271           if (!q2.exec(qStr1)) {
1272             qWarning() << __PRETTY_FUNCTION__ << __LINE__
1273                        << "q.lastError(): " << q2.lastError().text();
1274           }
1275         }
1276 
1277         if (!colorList.isEmpty()) {
1278           emit signalAddColorList(q1.value(0).toInt(), colorList.at(0));
1279         }
1280 
1281         isPlaySound = true;
1282       }
1283 
1284       if (isPlaySound && !soundList.isEmpty())
1285         emit signalPlaySound(soundList.at(0));
1286     } else {
1287       qWarning() << __PRETTY_FUNCTION__ << __LINE__
1288                  << "q.lastError(): " << q1.lastError().text();
1289     }
1290 
1291   }
1292 }
1293 
1294 /** @brief Update feed counts and all its parent categories
1295  *
1296  *  Update fields: unread news number,
1297  *  new news number, categories last update date/time
1298  * @param feedId - Feed Id
1299  * @param feedUrl - Feed URL
1300  * @param updated - Time feed updated
1301  *----------------------------------------------------------------------------*/
recountFeedCounts(int feedId,const QString & feedUrl,const QString & updated,const QString & lastBuildDate)1302 int ParseObject::recountFeedCounts(int feedId, const QString &feedUrl,
1303                                    const QString &updated, const QString &lastBuildDate)
1304 {
1305   QSqlQuery q(db_);
1306   q.setForwardOnly(true);
1307   QString qStr;
1308   QString htmlUrl;
1309   QString title;
1310 
1311   int feedParId = 0;
1312   q.exec(QString("SELECT parentId, htmlUrl, title FROM feeds WHERE id=='%1'").arg(feedId));
1313   if (q.first()) {
1314     feedParId = q.value(0).toInt();
1315     htmlUrl = q.value(1).toString();
1316     title = q.value(2).toString();
1317   }
1318 
1319   FeedCountStruct counts;
1320   int undeleteCount = 0;
1321   int unreadCount = 0;
1322   int newNewsCount = 0;
1323 
1324   // Count all news (not marked Deleted)
1325   qStr = QString("SELECT count(id) FROM news WHERE feedId=='%1' AND deleted==0").
1326       arg(feedId);
1327   q.exec(qStr);
1328   if (q.first()) undeleteCount = q.value(0).toInt();
1329 
1330   // Count unread news
1331   qStr = QString("SELECT count(read) FROM news WHERE feedId=='%1' AND read==0 AND deleted==0").
1332       arg(feedId);
1333   q.exec(qStr);
1334   if (q.first()) unreadCount = q.value(0).toInt();
1335 
1336   // Count new news
1337   qStr = QString("SELECT count(new) FROM news WHERE feedId=='%1' AND new==1 AND deleted==0").
1338       arg(feedId);
1339   q.exec(qStr);
1340   if (q.first()) newNewsCount = q.value(0).toInt();
1341 
1342   int unreadCountOld = 0;
1343   int newCountOld = 0;
1344   int undeleteCountOld = 0;
1345   qStr = QString("SELECT unread, newCount, undeleteCount FROM feeds WHERE id=='%1'").
1346       arg(feedId);
1347   q.exec(qStr);
1348   if (q.first()) {
1349     unreadCountOld = q.value(0).toInt();
1350     newCountOld = q.value(1).toInt();
1351     undeleteCountOld = q.value(2).toInt();
1352   }
1353 
1354   if ((unreadCount == unreadCountOld) && (newNewsCount == newCountOld) &&
1355       (undeleteCount == undeleteCountOld)) {
1356     counts.feedId = feedId;
1357     counts.unreadCount = unreadCount;
1358     counts.newCount = newNewsCount;
1359     counts.undeleteCount = undeleteCount;
1360     counts.updated = updated;
1361     counts.lastBuildDate = lastBuildDate;
1362 
1363     emit feedCountsUpdate(counts);
1364     return 0;
1365   }
1366 
1367   // Set number unread, new and all(undelete) news for feed
1368   qStr = QString("UPDATE feeds SET unread='%1', newCount='%2', undeleteCount='%3' "
1369                  "WHERE id=='%4'").
1370       arg(unreadCount).arg(newNewsCount).arg(undeleteCount).arg(feedId);
1371   q.exec(qStr);
1372 
1373   counts.feedId = feedId;
1374   counts.unreadCount = unreadCount;
1375   counts.newCount = newNewsCount;
1376   counts.undeleteCount = undeleteCount;
1377   counts.updated = updated;
1378   counts.lastBuildDate = lastBuildDate;
1379   counts.htmlUrl = htmlUrl;
1380   counts.xmlUrl = feedUrl;
1381   counts.title = title;
1382 
1383   emit feedCountsUpdate(counts);
1384 
1385   // Recount counters for all feed parents
1386   int l_feedParId = feedParId;
1387   while (l_feedParId) {
1388     QString updatedParent;
1389     int newCount = 0;
1390 
1391     qStr = QString("SELECT sum(unread), sum(newCount), sum(undeleteCount), "
1392                    "max(updated) FROM feeds WHERE parentId=='%1'").
1393         arg(l_feedParId);
1394     q.exec(qStr);
1395     if (q.first()) {
1396       unreadCount   = q.value(0).toInt();
1397       newCount      = q.value(1).toInt();
1398       undeleteCount = q.value(2).toInt();
1399       updatedParent = q.value(3).toString();
1400     }
1401     qStr = QString("UPDATE feeds SET unread='%1', newCount='%2', undeleteCount='%3', "
1402                    "updated='%4' WHERE id=='%5'").
1403         arg(unreadCount).arg(newCount).arg(undeleteCount).arg(updatedParent).
1404         arg(l_feedParId);
1405     q.exec(qStr);
1406 
1407     FeedCountStruct counts;
1408     counts.feedId = l_feedParId;
1409 
1410     q.exec(QString("SELECT parentId FROM feeds WHERE id==%1").arg(l_feedParId));
1411     if (q.first()) l_feedParId = q.value(0).toInt();
1412 
1413     counts.unreadCount = unreadCount;
1414     counts.newCount = newCount;
1415     counts.undeleteCount = undeleteCount;
1416     counts.updated = updatedParent;
1417 
1418     emit feedCountsUpdate(counts);
1419   }
1420 
1421   return (newNewsCount - newCountOld);
1422 }
1423