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 "database.h"
19 
20 #include "common.h"
21 #include "mainapplication.h"
22 #include "mainwindow.h"
23 #include "settings.h"
24 #include "VersionNo.h"
25 #include "sqlitedriver.h"
26 
27 #include <sqlite3.h>
28 
29 const int versionDB = 17;
30 
31 const QString kCreateFeedsTableQuery(
32     "CREATE TABLE feeds("
33     "id integer primary key, "
34     "text varchar, "             // Feed text (replaces title at the moment)
35     "title varchar, "            // Feed title
36     "description varchar, "      // Feed description
37     "xmlUrl varchar, "           // URL-link of the feed
38     "htmlUrl varchar, "          // URL-link site, that contains the feed
39     "language varchar, "         // Feed language
40     "copyrights varchar, "       // Feed copyrights
41     "author_name varchar, "      // Feed author: name
42     "author_email varchar, "     //              e-mail
43     "author_uri varchar, "       //              personal web page
44     "webMaster varchar, "        // e-mail of feed's technical support
45     "pubdate varchar, "          // Feed publication timestamp
46     "lastBuildDate varchar, "    // Timestamp of last modification of the feed
47     "category varchar, "         // Categories of content of the feed
48     "contributor varchar, "      // Feed contributors (tab separated)
49     "generator varchar, "        // Application has used to generate the feed
50     "docs varchar, "             // URL-link to document describing RSS-standart
51     "cloud_domain varchar, "     // Web-service providing rssCloud interface
52     "cloud_port varchar, "       //   .
53     "cloud_path varchar, "       //   .
54     "cloud_procedure varchar, "  //   .
55     "cloud_protocal varchar, "   //   .
56     "ttl integer, "              // Time in minutes the feed can be cached
57     "skipHours varchar, "        // Tip for aggregators, not to update the feed (specify hours of the day that can be skipped)
58     "skipDays varchar, "         // Tip for aggregators, not to update the feed (specify day of the week that can be skipped)
59     "image blob, "               // gif, jpeg, png picture, that can be associated with the feed
60     "unread integer, "           // number of unread news
61     "newCount integer, "         // number of new news
62     "currentNews integer, "      // current displayed news
63     "label varchar, "            // user purpose label(s)
64     "undeleteCount integer, "    // number of all news (not marked deleted)
65     "tags varchar, "             // user purpose tags
66     // --- Categories ---
67     "hasChildren integer default 0, "  // Children presence. Default - none
68     "parentId integer default 0, "     // parent id of the feed. Default - tree root
69     "rowToParent integer, "            // sequence number relative to parent
70     // --- General ---
71     "updateIntervalEnable int, "    // auto update enable flag
72     "updateInterval int, "          // auto update interval
73     "updateIntervalType varchar, "  // auto update interval type(minutes, hours,...)
74     "updateOnStartup int, "         // update the feed on application startup
75     "displayOnStartup int, "        // show the feed in separate tab in application startup
76     // --- Reading ---
77     "markReadAfterSecondsEnable int, "    // Enable "Read" timer
78     "markReadAfterSeconds int, "          // Number of seconds that must elapse to mark news "Read"
79     "markReadInNewspaper int, "           // mark Read when Newspaper layout
80     "markDisplayedOnSwitchingFeed int, "  // mark Read on switching to another feed
81     "markDisplayedOnClosingTab int, "     // mark Read on tab closing
82     "markDisplayedOnMinimize int, "       // mark Read on minimizing to tray
83     // --- Display ---
84     "layout text, "      // news display layout
85     "filter text, "      // news display filter
86     "groupBy int, "      // column number to sort by
87     "displayNews int, "  // 0 - display content from news; 1 - download content from link
88     "displayEmbeddedImages integer default 1, "  // display images embedded in news
89     "loadTypes text, "                           // type of content to load ("images" or "images sounds" - images only or images and sound)
90     "openLinkOnEmptyContent int, "               // load link, if content is empty
91     // --- Columns ---
92     "columns text, "  // columns list and order of the news displayed in list
93     "sort text, "     // column name to sort by
94     "sortType int, "  // sort type (ascend, descend)
95     // --- Clean Up ---
96     "maximumToKeep int, "           // maximum number of news to keep
97     "maximumToKeepEnable int, "     // enable limitation
98     "maximumAgeOfNews int, "        // maximum store time of the news
99     "maximumAgoOfNewEnable int, "   // enable limitation
100     "deleteReadNews int, "          // delete read news
101     "neverDeleteUnreadNews int, "   // don't delete unread news
102     "neverDeleteStarredNews int, "  // don't delete starred news
103     "neverDeleteLabeledNews int, "  // don't delete labeled news
104     // --- Status ---
105     "status text, "                 // last update result
106     "created text, "                // feed creation timestamp
107     "updated text, "                // last update timestamp
108     "lastDisplayed text, "           // last display timestamp
109     "f_Expanded integer default 1, "  // expand folder flag
110     "flags text, "                    // more flags (example "focused", "hidden")
111     "authentication integer default 0, "    // enable authentification, sets on feed creation
112     "duplicateNewsMode integer default 0, " // news duplicates process mode
113     "addSingleNewsAnyDateOn integer default 1, " // enable adding news with any date into the database
114     "avoidedOldSingleNewsDateOn integer default 0, " // avoid adding news before this date into the database
115     "avoidedOldSingleNewsDate varchar, " // date to avoid
116     "typeFeed integer default 0, "          // reserved for future purposes
117     "showNotification integer default 0, "  //
118     "disableUpdate integer default 0, "     // disable update feed
119     "javaScriptEnable integer default 1, "  //
120     // version 16
121     "layoutDirection integer default 0, "    // 0 - ltr; 1 - rtl
122     // Version 17
123     "SingleClickAction integer default 0, " // ENewsClickAction
124     "DoubleClickAction integer default 0, " // ENewsClickAction
125     "MiddleClickAction integer default 0 "  // ENewsClickAction
126     ")");
127 
128 const QString kCreateNewsTableQuery(
129     "CREATE TABLE news("
130     "id integer primary key, "
131     "feedId integer, "                     // feed id from feed table
132     "guid varchar, "                       // news unique number
133     "guidislink varchar default 'true', "  // flag shows that news unique number is URL-link to news
134     "description varchar, "                // brief description
135     "content varchar, "                    // full content (atom)
136     "title varchar, "                      // title
137     "published varchar, "                  // publish timestamp
138     "modified varchar, "                   // modification timestamp
139     "received varchar, "                   // receive news timestamp (set on receive)
140     "author_name varchar, "                // author name
141     "author_uri varchar, "                 // author web page (atom)
142     "author_email varchar, "               // author e-mail (atom)
143     "category varchar, "                   // category. May be several item tabs separated
144     "label varchar, "                      // label (user purpose label(s))
145     "new integer default 1, "              // Flag "new". Set on receive, reset on application close
146     "read integer default 0, "             // Flag "read". Set after news has been focused
147     "starred integer default 0, "          // Flag "sticky". Set by user
148     "deleted integer default 0, "          // Flag "deleted". News is marked deleted by remains in DB,
149                                            //   for purpose not to display after next update.
150                                            //   News are deleted by cleanup process only
151     "attachment varchar, "                 // Links to attachments (tabs separated)
152     "comments varchar, "                   // News comments page URL-link
153     "enclosure_length, "                   // Media-object, associated to news:
154     "enclosure_type, "                     //   length, type,
155     "enclosure_url, "                      //   URL-address
156     "source varchar, "                     // source, incese of republication (atom: <link via>)
157     "link_href varchar, "                  // URL-link to news (atom: <link self>)
158     "link_enclosure varchar, "             // URL-link to huge amoun of data,
159                                            //   that can't be received in the news
160     "link_related varchar, "               // URL-link for related data of the news (atom)
161     "link_alternate varchar, "             // URL-link to alternative news representation
162     "contributor varchar, "                // contributors (tabs separated)
163     "rights varchar, "                     // copyrights
164     "deleteDate varchar, "                 // news delete timestamp
165     "feedParentId integer default 0 "      // parent feed id from feed table
166     ")");
167 
168 const QString kCreateFiltersTable(
169     "CREATE TABLE filters("
170     "id integer primary key, "
171     "name varchar, "              // filter name
172     "type integer, "              // filter type (and, or, for all)
173     "feeds varchar, "             // feed list, that are using the filter
174     "enable integer default 1, "  // 1 - filter used; 0 - filter not used
175     "num integer "                // Sequence number. Used to sort filters
176     ")");
177 
178 const QString kCreateFilterConditionsTable(
179     "CREATE TABLE filterConditions("
180     "id integer primary key, "
181     "idFilter int, "            // filter Id
182     "field varchar, "           // field to filter by
183     "condition varchar, "       // condition has applied to filed
184     "content varchar "          // field content that is used by filter
185     ")");
186 
187 const QString kCreateFilterActionsTable(
188     "CREATE TABLE filterActions("
189     "id integer primary key, "
190     "idFilter int, "            // filter Id
191     "action varchar, "          // action that has appled for filter
192     "params varchar "           // action parameters
193     ")");
194 
195 const QString kCreateLabelsTable(
196     "CREATE TABLE labels("
197     "id integer primary key, "
198     "name varchar, "            // label name
199     "image blob, "              // label image
200     "color_text varchar, "      // news text color displayed in news list
201     "color_bg varchar, "        // news background color displayed in news list
202     "num integer, "             // sequence number to sort with
203     "currentNews integer "      // current displayed news
204     ")");
205 
206 const QString kCreatePasswordsTable(
207     "CREATE TABLE passwords("
208     "id integer primary key, "
209     "server varchar, "          // server
210     "username varchar, "        // username
211     "password varchar "         // password
212     ")");
213 
214 const QString kAddColumnsFeedsTableQuery(
215         "ALTER TABLE feeds ADD COLUMN addSingleNewsAnyDateOn integer default 1;"
216         "ALTER TABLE feeds ADD COLUMN avoidedOldSingleNewsDateOn integer default 0;"
217         "ALTER TABLE feeds ADD COLUMN avoidedOldSingleNewsDate varchar;"
218         );
219 
version()220 int Database::version()
221 {
222   return versionDB;
223 }
224 
initialization()225 void Database::initialization()
226 {
227   prepareDatabase();
228 
229   SQLiteDriver *driver = new SQLiteDriver();
230   QSqlDatabase db = QSqlDatabase::addDatabase(driver);
231   if (mainApp->storeDBMemory())
232     db.setDatabaseName(":memory:");
233   else
234     db.setDatabaseName(mainApp->dbFileName());
235   if (db.open()) {
236     setPragma(db);
237 
238     if (mainApp->storeDBMemory()) {
239       sqliteDBMemFile(db, false);
240     }
241   }
242 }
243 
setPragma(QSqlDatabase & db)244 void Database::setPragma(QSqlDatabase &db)
245 {
246   Settings settings;
247   QSqlQuery q(db);
248   q.setForwardOnly(true);
249   q.exec("PRAGMA encoding = \"UTF-8\"");
250 
251   QString sync = settings.value("synchronousDB", "FULL").toString();
252   q.exec(QString("PRAGMA synchronous = %1").arg(sync));
253 //  q.exec("PRAGMA journal_mode = MEMORY");
254 //  q.exec("PRAGMA temp_store = MEMORY");
255 
256   q.exec("PRAGMA page_size = 4096");
257   q.exec("PRAGMA cache_size = 16384");
258   q.finish();
259 }
260 
prepareDatabase()261 void Database::prepareDatabase()
262 {
263   {
264     QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "initialization");
265     db.setDatabaseName(mainApp->dbFileName());
266     if (!db.open()) {
267       QString message = QString("Cannot open SQLite database! \n"
268                                 "Error: %1").arg(db.lastError().text());
269       qCritical() << message;
270       QMessageBox::critical(mainApp->mainWindow(), QObject::tr("Error"), message);
271     } else {
272       setPragma(db);
273       QSqlQuery q(db);
274       q.setForwardOnly(true);
275 
276       if (!mainApp->dbFileExists()) {
277         qWarning() << "Creating database";
278 
279         createTables(db);
280         createLabels(db);
281         q.prepare("INSERT INTO info(name, value) VALUES ('version', :version)");
282         q.bindValue(":version", version());
283         q.exec();
284         q.prepare("INSERT INTO info(name, value) VALUES('appVersion', :appVersion)");
285         q.bindValue(":appVersion", STRPRODUCTVER);
286         q.exec();
287       } else {
288         qWarning() << "Preparation database";
289 
290         addColumnsToFeedsTables(db);
291 
292         // Version DB > 0.12.1
293         Settings settings;
294 
295         int dbVersion = -1;
296         q.exec("SELECT value FROM info WHERE name='version'");
297         if (q.first()) {
298           dbVersion = q.value(0).toInt();
299         }
300 
301         QString appVersion = QString();
302         q.exec("SELECT value FROM info WHERE name='appVersion'");
303         if (q.first()) {
304           appVersion = q.value(0).toString();
305         }
306 
307         // Create backups for DB and Settings
308         if (appVersion != STRPRODUCTVER) {
309           Common::createFileBackup(mainApp->dbFileName(), appVersion);
310           Common::createFileBackup(settings.fileName(), appVersion);
311         }
312 
313         if (dbVersion < 14) {
314           q.exec("ALTER TABLE feeds ADD COLUMN showNotification integer default 0");
315           q.exec("ALTER TABLE feeds ADD COLUMN disableUpdate integer default 0");
316           q.exec("ALTER TABLE feeds ADD COLUMN javaScriptEnable integer default 1");
317         }
318         if (dbVersion < 16) {
319           q.exec("ALTER TABLE feeds ADD COLUMN layoutDirection integer default 0");
320         }
321 
322         if (dbVersion < 17)
323         {
324           q.exec("ALTER table feeds ADD COLUMN SingleClickAction integer default 0");
325           q.exec("ALTER table feeds ADD COLUMN DoubleClickAction integer default 0");
326           q.exec("ALTER table feeds ADD COLUMN MiddleClickAction integer default 0");
327         }
328 
329         // Update appVersion anyway
330         if (appVersion.isEmpty()) {
331           q.prepare("INSERT INTO info(name, value) VALUES('appVersion', :appVersion)");
332           q.bindValue(":appVersion", STRPRODUCTVER);
333           q.exec();
334         } else if (appVersion != STRPRODUCTVER) {
335           q.prepare("UPDATE info SET value=:appVersion WHERE name='appVersion'");
336           q.bindValue(":appVersion", STRPRODUCTVER);
337           q.exec();
338         }
339 
340         if (dbVersion == -1) {
341           q.prepare("INSERT INTO info(name, value) VALUES('version', :version)");
342           q.bindValue(":version", version());
343           q.exec();
344         } else if (dbVersion < version()) {
345           q.prepare("UPDATE info SET value=:version WHERE name='version'");
346           q.bindValue(":version", version());
347           q.exec();
348         }
349 
350         settings.setValue("VersionDB", version());
351       }
352 
353       q.finish();
354       db.close();
355     }
356   }
357   QSqlDatabase::removeDatabase("initialization");
358 }
359 
createTables(QSqlDatabase & db)360 void Database::createTables(QSqlDatabase &db)
361 {
362   db.transaction();
363 
364   db.exec(kCreateFeedsTableQuery);
365   db.exec(kAddColumnsFeedsTableQuery);
366   db.exec(kCreateNewsTableQuery);
367   // Create index for feedId field
368   db.exec("CREATE INDEX feedId ON news(feedId)");
369 
370   // Create extra feeds table just in case
371   db.exec("CREATE TABLE feeds_ex(id integer primary key, "
372           "feedId integer, "  // feed Id
373           "name varchar, "    // parameter name
374           "value varchar "    // parameter value
375           ")");
376   // Create extra news table just in case
377   db.exec("CREATE TABLE news_ex(id integer primary key, "
378           "feedId integer, "  // feed Id
379           "newsId integer, "  // news Id
380           "name varchar, "    // parameter name
381           "value varchar "    // parameter value
382           ")");
383   // Create filters table
384   db.exec(kCreateFiltersTable);
385   db.exec(kCreateFilterConditionsTable);
386   db.exec(kCreateFilterActionsTable);
387   // Create extra filters just in case
388   db.exec("CREATE TABLE filters_ex(id integer primary key, "
389           "idFilter integer, "  // filter Id
390           "name text, "         // parameter name
391           "value text"          // parameter value
392           ")");
393   // Create labels table
394   db.exec(kCreateLabelsTable);
395   // Create password table
396   db.exec(kCreatePasswordsTable);
397   //
398   db.exec("CREATE TABLE info(id integer primary key, name varchar, value varchar)");
399 
400   db.commit();
401 }
402 
createLabels(QSqlDatabase & db)403 void Database::createLabels(QSqlDatabase &db)
404 {
405   QSqlQuery q(db);
406   for (int i = 0; i < 6; i++) {
407     q.prepare("INSERT INTO labels(name, image) "
408               "VALUES (:name, :image)");
409     q.bindValue(":name", MainWindow::nameLabels().at(i));
410 
411     QFile file(QString(":/images/label_%1").arg(i+1));
412     file.open(QFile::ReadOnly);
413     q.bindValue(":image", file.readAll());
414     file.close();
415 
416     q.exec();
417 
418     int labelId = q.lastInsertId().toInt();
419     q.exec(QString("UPDATE labels SET num='%1' WHERE id=='%1'").arg(labelId));
420   }
421 }
422 
addColumnsToFeedsTables(QSqlDatabase & db)423 void Database::addColumnsToFeedsTables(QSqlDatabase &db)
424 {
425     QStringList columnsList;
426     // Version > 0.18.12
427     columnsList.append(" addSingleNewsAnyDateOn integer default 1;");
428     columnsList.append(" avoidedOldSingleNewsDateOn integer default 0;");
429     columnsList.append(" avoidedOldSingleNewsDate varchar;");
430 
431     db.transaction();
432     foreach (QString col, columnsList) {
433         db.exec("ALTER TABLE feeds ADD COLUMN" + col);
434     }
435     db.commit();
436 }
437 
connection(const QString & connectionName)438 QSqlDatabase Database::connection(const QString &connectionName)
439 {
440   QSqlDatabase db;
441   if (mainApp->storeDBMemory()) {
442     db = QSqlDatabase::database();
443   }
444   else {
445     db = QSqlDatabase::database(connectionName, true);
446     if (!db.isValid()) {
447       SQLiteDriver *driver = new SQLiteDriver();
448       db = QSqlDatabase::addDatabase(driver, connectionName);
449       db.setDatabaseName(mainApp->dbFileName());
450       db.open();
451       setPragma(db);
452     }
453   }
454   return db;
455 }
456 
sqliteDBMemFile(QSqlDatabase & db,bool save)457 void Database::sqliteDBMemFile(QSqlDatabase &db, bool save)
458 {
459   if (save) qWarning() << "sqliteDBMemFile(): from memory to file...";
460   else qWarning() << "sqliteDBMemFile(): from file to memory...";
461 
462   int rc = -1;                   /* Function return code */
463   QVariant v = db.driver()->handle();
464   if (v.isValid() && qstrcmp(v.typeName(),"sqlite3*") == 0) {
465     // v.data() returns a pointer to the handle
466     sqlite3 *handle = *static_cast<sqlite3 **>(v.data());
467     if (handle != 0) {  // check that it is not NULL
468       sqlite3 *pInMemory = handle;
469       sqlite3 *pFile;           /* Database connection opened on zFilename */
470       sqlite3_backup *pBackup;  /* Backup object used to copy data */
471       sqlite3 *pTo;             /* Database to copy to (pFile or pInMemory) */
472       sqlite3 *pFrom;           /* Database to copy from (pFile or pInMemory) */
473 
474       /* Open the database file identified by zFilename. Exit early if this fails
475       ** for any reason. */
476       rc = sqlite3_open(mainApp->dbFileName().toUtf8().data(), &pFile);
477       if (rc == SQLITE_OK) {
478         /* If this is a 'load' operation (isSave==0), then data is copied
479         ** from the database file just opened to database pInMemory.
480         ** Otherwise, if this is a 'save' operation (isSave==1), then data
481         ** is copied from pInMemory to pFile.  Set the variables pFrom and
482         ** pTo accordingly. */
483         pFrom = (save ? pInMemory : pFile);
484         pTo   = (save ? pFile     : pInMemory);
485 
486         /* Set up the backup procedure to copy from the "main" database of
487         ** connection pFile to the main database of connection pInMemory.
488         ** If something goes wrong, pBackup will be set to NULL and an error
489         ** code and  message left in connection pTo.
490         **
491         ** If the backup object is successfully created, call backup_step()
492         ** to copy data from pFile to pInMemory. Then call backup_finish()
493         ** to release resources associated with the pBackup object.  If an
494         ** error occurred, then  an error code and message will be left in
495         ** connection pTo. If no error occurred, then the error code belonging
496         ** to pTo is set to SQLITE_OK.
497         */
498 
499         pBackup = sqlite3_backup_init(pTo, "main", pFrom, "main");
500 
501         /* Each iteration of this loop copies 5 database pages from database
502         ** pDb to the backup database. If the return value of backup_step()
503         ** indicates that there are still further pages to copy, sleep for
504         ** 250 ms before repeating. */
505         do {
506           rc = sqlite3_backup_step(pBackup, 10000);
507 
508           if (!mainApp->isNoDebugOutput()) {
509             int remaining = sqlite3_backup_remaining(pBackup);
510             int pagecount = sqlite3_backup_pagecount(pBackup);
511             qDebug() << rc << "backup" << pagecount << "remain" << remaining;
512           }
513 
514           if ((rc == SQLITE_OK) || (rc == SQLITE_BUSY) || (rc == SQLITE_LOCKED))
515             sqlite3_sleep(100);
516         } while ((rc == SQLITE_OK) || (rc == SQLITE_BUSY) || (rc == SQLITE_LOCKED));
517 
518         /* Release resources allocated by backup_init(). */
519         (void)sqlite3_backup_finish(pBackup);
520 
521         if (rc != SQLITE_DONE)
522           qCritical() << "sqliteDBMemFile(): return code =" << rc;
523       } else {
524         qCritical() << "sqliteDBMemFile(): error open =" << rc;
525       }
526 
527       /* Close the database connection opened on database file zFilename
528       ** and return the result of this function. */
529       (void)sqlite3_close(pFile);
530     }
531   }
532   qWarning() << "sqliteDBMemFile(): finished!";
533 }
534 
setVacuum()535 void Database::setVacuum()
536 {
537   {
538     QSqlDatabase dbFile = QSqlDatabase::addDatabase("QSQLITE", "vacuum");
539     dbFile.setDatabaseName(mainApp->dbFileName());
540     dbFile.open();
541     setPragma(dbFile);
542     dbFile.exec("VACUUM");
543     dbFile.close();
544   }
545   QSqlDatabase::removeDatabase("vacuum");
546 }
547