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