// This file is part of FeedReader. // // FeedReader is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // FeedReader is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with FeedReader. If not, see . public class FeedReader.DataBase : DataBaseReadOnly { public static new DataBase writeAccess() { var database = new DataBase(); if(database.uninitialized()) { database.init(); } return database; } public static new DataBaseReadOnly readOnly() { return writeAccess() as DataBaseReadOnly; } public DataBase(string dbFile = "feedreader-%01i.db".printf(Constants.DB_SCHEMA_VERSION)) { base(dbFile); } public void checkpoint() { m_db.checkpoint(); } public bool resetDB() { Logger.warning("resetDB"); m_db.simple_query("DROP TABLE main.fts_table"); m_db.simple_query("DROP TABLE main.taggings"); m_db.simple_query("DROP TABLE main.Enclosures"); m_db.simple_query("DROP TABLE main.CachedActions"); m_db.simple_query("DROP TABLE main.tags"); m_db.simple_query("DROP TABLE main.articles"); m_db.simple_query("DROP TABLE main.categories"); m_db.simple_query("DROP TABLE main.feeds"); m_db.simple_query("VACUUM"); string query = "PRAGMA INTEGRITY_CHECK"; Sqlite.Statement stmt = m_db.prepare(query); int cols = stmt.column_count (); while (stmt.step () == Sqlite.ROW) { for (int i = 0; i < cols; i++) { if(stmt.column_text(i) != "ok") { Logger.error("resetting the database failed"); return false; } } } stmt.reset(); return true; } public void updateFTS() { m_db.simple_query("INSERT INTO fts_table(fts_table) VALUES('rebuild')"); } public void springCleaning() { m_db.simple_query("VACUUM"); var now = new DateTime.now_local(); Settings.state().set_int("last-spring-cleaning", (int)now.to_unix()); } public void dropOldArticles(int max_age_days) { var query = new QueryBuilder(QueryType.SELECT, "main.articles"); query.select_field("articleID"); query.select_field("feedID"); query.where(@"datetime(date, 'unixepoch', 'localtime') <= datetime('now', '-$(max_age_days) days')"); query.where_equal_int("marked", ArticleStatus.UNMARKED.to_int()); if(FeedServer.get_default().useMaxArticles()) { int syncCount = Settings.general().get_int("max-articles"); query.where(@"rowid BETWEEN 1 AND (SELECT rowid FROM articles ORDER BY rowid DESC LIMIT 1 OFFSET $syncCount)"); } Sqlite.Statement stmt = m_db.prepare(query.to_string()); while (stmt.step () == Sqlite.ROW) { delete_article(stmt.column_text(0), stmt.column_text(1)); } } private void delete_article(string articleID, string feedID) { Logger.info(@"Deleting article \"$articleID\""); m_db.execute("DELETE FROM main.articles WHERE articleID = ?", { articleID }); m_db.execute("DELETE FROM main.Enclosures WHERE articleID = ?", { articleID }); string folder_path = GLib.Environment.get_user_data_dir() + @"/feedreader/data/images/$feedID/$articleID/"; Utils.remove_directory(folder_path); } public void dropTag(Tag tag) { m_db.execute("DELETE FROM main.tags WHERE tagID = ?", { tag.getTagID() }); m_db.execute("DELETE FROM main.taggings WHERE tagID = ?", { tag.getTagID() }); } public void write_feeds(Gee.Collection feeds) { m_db.simple_query("BEGIN TRANSACTION"); var query = new QueryBuilder(QueryType.INSERT_OR_REPLACE, "main.feeds"); query.insert_param("feed_id", "$FEEDID"); query.insert_param("name", "$FEEDNAME"); query.insert_param("url", "$FEEDURL"); query.insert_param("category_id", "$CATID"); query.insert_int("subscribed", 1); query.insert_param("xmlURL", "$XMLURL"); query.insert_param("iconURL", "$ICONURL"); Sqlite.Statement stmt = m_db.prepare(query.to_string()); int feedID_pos = stmt.bind_parameter_index("$FEEDID"); int feedName_pos = stmt.bind_parameter_index("$FEEDNAME"); int feedURL_pos = stmt.bind_parameter_index("$FEEDURL"); int catID_pos = stmt.bind_parameter_index("$CATID"); int xmlURL_pos = stmt.bind_parameter_index("$XMLURL"); int iconURL_pos = stmt.bind_parameter_index("$ICONURL"); assert (feedID_pos > 0); assert (feedName_pos > 0); assert (feedURL_pos > 0); assert (catID_pos > 0); assert (xmlURL_pos > 0); assert (iconURL_pos > 0); foreach(var feed_item in feeds) { stmt.bind_text(feedID_pos, feed_item.getFeedID()); stmt.bind_text(feedName_pos, feed_item.getTitle()); stmt.bind_text(feedURL_pos, feed_item.getURL()); stmt.bind_text(catID_pos, StringUtils.join(feed_item.getCatIDs(), ",")); stmt.bind_text(xmlURL_pos, feed_item.getXmlUrl()); stmt.bind_text(iconURL_pos, feed_item.getIconURL()); while(stmt.step() == Sqlite.ROW) {} stmt.reset(); } m_db.simple_query("COMMIT TRANSACTION"); } public void write_tag(Tag tag) { var list = new Gee.ArrayList(); list.add(tag); write_tags(list); } public void write_tags(Gee.Collection tags) { m_db.simple_query("BEGIN TRANSACTION"); var query = new QueryBuilder(QueryType.INSERT_OR_IGNORE, "main.tags"); query.insert_param("tagID", "$TAGID"); query.insert_param("title", "$LABEL"); query.insert_int("\"exists\"", 1); query.insert_param("color", "$COLOR"); Sqlite.Statement stmt = m_db.prepare(query.to_string()); int tagID_position = stmt.bind_parameter_index("$TAGID"); int label_position = stmt.bind_parameter_index("$LABEL"); int color_position = stmt.bind_parameter_index("$COLOR"); assert (tagID_position > 0); assert (label_position > 0); assert (color_position > 0); foreach(var tag_item in tags) { stmt.bind_text(tagID_position, tag_item.getTagID()); stmt.bind_text(label_position, tag_item.getTitle()); stmt.bind_int (color_position, tag_item.getColor()); while(stmt.step() == Sqlite.ROW) {} stmt.reset (); } m_db.simple_query("COMMIT TRANSACTION"); } public void update_tag(Tag tag) { update_tags(ListUtils.single(tag)); if(FeedServer.get_default().tagIDaffectedByNameChange()) { string newID = tag.getTagID().replace(tag.getTitle(), tag.getTitle()); m_db.execute("UPDATE tags SET tagID = ? WHERE tagID = ?", { newID, tag.getTagID() }); } } public void update_tags(Gee.List tags) { m_db.simple_query("BEGIN TRANSACTION"); var query = new QueryBuilder(QueryType.UPDATE, "main.tags"); query.update_param("title", "$TITLE"); query.update_int("\"exists\"", 1); query.where_equal_param("tagID", "$TAGID"); Sqlite.Statement stmt = m_db.prepare(query.to_string()); int title_position = stmt.bind_parameter_index("$TITLE"); int tagID_position = stmt.bind_parameter_index("$TAGID"); assert (title_position > 0); assert (tagID_position > 0); foreach(var tag_item in tags) { stmt.bind_text(title_position, tag_item.getTitle()); stmt.bind_text(tagID_position, tag_item.getTagID()); while (stmt.step () == Sqlite.ROW) {} stmt.reset (); } m_db.simple_query("COMMIT TRANSACTION"); } public void write_categories(Gee.List categories) { m_db.simple_query("BEGIN TRANSACTION"); var query = new QueryBuilder(QueryType.INSERT_OR_REPLACE, "main.categories"); query.insert_param("categorieID", "$CATID"); query.insert_param("title", "$FEEDNAME"); query.insert_param("orderID", "$ORDERID"); query.insert_int("\"exists\"", 1); query.insert_param("Parent", "$PARENT"); query.insert_param("Level", "$LEVEL"); Sqlite.Statement stmt = m_db.prepare(query.to_string()); int catID_position = stmt.bind_parameter_index("$CATID"); int feedName_position = stmt.bind_parameter_index("$FEEDNAME"); int orderID_position = stmt.bind_parameter_index("$ORDERID"); int parent_position = stmt.bind_parameter_index("$PARENT"); int level_position = stmt.bind_parameter_index("$LEVEL"); assert (catID_position > 0); assert (feedName_position > 0); assert (orderID_position > 0); assert (parent_position > 0); assert (level_position > 0); foreach(var cat_item in categories) { stmt.bind_text(catID_position, cat_item.getCatID()); stmt.bind_text(feedName_position, cat_item.getTitle()); stmt.bind_int (orderID_position, cat_item.getOrderID()); stmt.bind_text(parent_position, cat_item.getParent()); stmt.bind_int (level_position, cat_item.getLevel()); while (stmt.step () == Sqlite.ROW) {} stmt.reset (); } m_db.simple_query("COMMIT TRANSACTION"); } public void updateArticlesByID(Gee.List ids, string field) { // first reset all articles var reset_query = new QueryBuilder(QueryType.UPDATE, "main.articles"); if(field == "unread") { reset_query.update_int(field, ArticleStatus.READ.to_int()); } else if(field == "marked") { reset_query.update_int(field, ArticleStatus.UNMARKED.to_int()); } m_db.simple_query(reset_query.to_string()); m_db.simple_query("BEGIN TRANSACTION"); // then reapply states of the synced articles var update_query = new QueryBuilder(QueryType.UPDATE, "main.articles"); if(field == "unread") { update_query.update_int(field, ArticleStatus.UNREAD.to_int()); } else if(field == "marked") { update_query.update_int(field, ArticleStatus.MARKED.to_int()); } update_query.where_equal_param("articleID", "$ARTICLEID"); Sqlite.Statement stmt = m_db.prepare(update_query.to_string()); int articleID_position = stmt.bind_parameter_index("$ARTICLEID"); assert (articleID_position > 0); foreach(string id in ids) { stmt.bind_text(articleID_position, id); while(stmt.step() != Sqlite.DONE) {} stmt.reset(); } m_db.simple_query("COMMIT TRANSACTION"); } public void writeContent(Article article) { var update_query = new QueryBuilder(QueryType.UPDATE, "main.articles"); update_query.update_param("html", "$HTML"); update_query.update_param("preview", "$PREVIEW"); update_query.update_int("contentFetched", 1); update_query.where_equal_string("articleID", article.getArticleID()); Sqlite.Statement stmt = m_db.prepare(update_query.to_string()); int html_position = stmt.bind_parameter_index("$HTML"); int preview_position = stmt.bind_parameter_index("$PREVIEW"); assert (html_position > 0); assert (preview_position > 0); stmt.bind_text(html_position, article.getHTML()); stmt.bind_text(preview_position, article.getPreview()); while(stmt.step() != Sqlite.DONE) {} stmt.reset(); } public void update_article(Article article) { update_articles(ListUtils.single(article)); } public void update_articles(Gee.List
articles) { m_db.simple_query("BEGIN TRANSACTION"); var update_query = new QueryBuilder(QueryType.UPDATE, "main.articles"); update_query.update_param("unread", "$UNREAD"); update_query.update_param("marked", "$MARKED"); update_query.update_param("lastModified", "$LASTMODIFIED"); update_query.where_equal_param("articleID", "$ARTICLEID"); Sqlite.Statement stmt = m_db.prepare(update_query.to_string()); int unread_position = stmt.bind_parameter_index("$UNREAD"); int marked_position = stmt.bind_parameter_index("$MARKED"); int modified_position = stmt.bind_parameter_index("$LASTMODIFIED"); int articleID_position = stmt.bind_parameter_index("$ARTICLEID"); assert (unread_position > 0); assert (marked_position > 0); assert (modified_position > 0); assert (articleID_position > 0); foreach(Article a in articles) { var unread = ActionCache.get_default().checkRead(a); var marked = ActionCache.get_default().checkStarred(a.getArticleID(), a.getMarked()); if(unread != ArticleStatus.READ && unread != ArticleStatus.UNREAD) { Logger.warning(@"DataBase.update_articles: writing invalid unread status $unread for article " + a.getArticleID()); } if(marked != ArticleStatus.MARKED && marked != ArticleStatus.UNMARKED) { Logger.warning(@"DataBase.update_articles: writing invalid marked status $marked for article " + a.getArticleID()); } stmt.bind_int (unread_position, unread); stmt.bind_int (marked_position, marked); stmt.bind_int (modified_position, a.getLastModified()); stmt.bind_text(articleID_position, a.getArticleID()); while(stmt.step() != Sqlite.DONE) {} stmt.reset(); write_taggings(a); } m_db.simple_query("COMMIT TRANSACTION"); } public void write_articles(Gee.List
articles) { Utils.generatePreviews(articles); Utils.checkHTML(articles); m_db.simple_query("BEGIN TRANSACTION"); var query = new QueryBuilder(QueryType.INSERT_OR_IGNORE, "main.articles"); query.insert_param("articleID", "$ARTICLEID"); query.insert_param("feedID", "$FEEDID"); query.insert_param("title", "$TITLE"); query.insert_param("author", "$AUTHOR"); query.insert_param("url", "$URL"); query.insert_param("html", "$HTML"); query.insert_param("preview", "$PREVIEW"); query.insert_param("unread", "$UNREAD"); query.insert_param("marked", "$MARKED"); query.insert_param("date", "$DATE"); query.insert_param("guidHash", "$GUIDHASH"); query.insert_param("lastModified", "$LASTMODIFIED"); query.insert_int("contentFetched", 0); Sqlite.Statement stmt = m_db.prepare(query.to_string()); int articleID_position = stmt.bind_parameter_index("$ARTICLEID"); int feedID_position = stmt.bind_parameter_index("$FEEDID"); int url_position = stmt.bind_parameter_index("$URL"); int unread_position = stmt.bind_parameter_index("$UNREAD"); int marked_position = stmt.bind_parameter_index("$MARKED"); int title_position = stmt.bind_parameter_index("$TITLE"); int html_position = stmt.bind_parameter_index("$HTML"); int preview_position = stmt.bind_parameter_index("$PREVIEW"); int author_position = stmt.bind_parameter_index("$AUTHOR"); int date_position = stmt.bind_parameter_index("$DATE"); int guidHash_position = stmt.bind_parameter_index("$GUIDHASH"); int modified_position = stmt.bind_parameter_index("$LASTMODIFIED"); assert (articleID_position > 0); assert (feedID_position > 0); assert (url_position > 0); assert (unread_position > 0); assert (marked_position > 0); assert (title_position > 0); assert (html_position > 0); assert (preview_position > 0); assert (author_position > 0); assert (date_position > 0); assert (guidHash_position > 0); assert (modified_position > 0); DateTime? drop_date = ((DropArticles)Settings.general().get_enum("drop-articles-after")).to_start_date(); foreach(var article in articles) { // if article time is in the future var now = new GLib.DateTime.now_local(); if(article.getDate().compare(now) == 1) { article.SetDate(now); } if(drop_date != null && article.getDate().compare(drop_date) == -1) { Logger.info("Ignoring old article: %s".printf(article.getTitle())); continue; } stmt.bind_text(articleID_position, article.getArticleID()); stmt.bind_text(feedID_position, article.getFeedID()); stmt.bind_text(url_position, article.getURL()); stmt.bind_int (unread_position, article.getUnread()); stmt.bind_int (marked_position, article.getMarked()); stmt.bind_text(title_position, Utils.UTF8fix(article.getTitle())); stmt.bind_text(html_position, article.getHTML()); stmt.bind_text(preview_position, Utils.UTF8fix(article.getPreview(), true)); stmt.bind_text(author_position, article.getAuthor()); stmt.bind_int64(date_position, article.getDate().to_unix()); stmt.bind_text(guidHash_position, article.getHash()); stmt.bind_int (modified_position, article.getLastModified()); while(stmt.step() != Sqlite.DONE) {} stmt.reset(); write_enclosures(article); write_taggings(article); } m_db.simple_query("COMMIT TRANSACTION"); } private void write_taggings(Article article) { var query = new QueryBuilder(QueryType.INSERT_OR_REPLACE, "main.taggings"); query.insert_param("articleID", "$ARTICLEID"); query.insert_param("tagID", "$TAGID"); Sqlite.Statement stmt = m_db.prepare(query.to_string()); int articleID_position = stmt.bind_parameter_index("$ARTICLEID"); int tagID_position = stmt.bind_parameter_index("$TAGID"); assert (articleID_position > 0); assert (tagID_position > 0); foreach(string tagID in article.getTagIDs()) { stmt.bind_text(articleID_position, article.getArticleID()); stmt.bind_text(tagID_position, tagID); while(stmt.step() != Sqlite.DONE) {} stmt.reset(); } } private void write_enclosures(Article article) { var query = new QueryBuilder(QueryType.INSERT_OR_REPLACE, "main.Enclosures"); query.insert_param("articleID", "$ARTICLEID"); query.insert_param("url", "$URL"); query.insert_param("type", "$TYPE"); Sqlite.Statement stmt = m_db.prepare(query.to_string()); int articleID_position = stmt.bind_parameter_index("$ARTICLEID"); int url_position = stmt.bind_parameter_index("$URL"); int type_position = stmt.bind_parameter_index("$TYPE"); assert (articleID_position > 0); assert (url_position > 0); assert (type_position > 0); foreach(Enclosure enc in article.getEnclosures()) { stmt.bind_text(articleID_position, article.getArticleID()); stmt.bind_text(url_position, enc.get_url()); stmt.bind_int (type_position, enc.get_enclosure_type()); while(stmt.step() != Sqlite.DONE) {} stmt.reset(); } } public void markCategorieRead(string catID) { var query = new QueryBuilder(QueryType.UPDATE, "main.articles"); query.update_int("unread", ArticleStatus.READ.to_int()); query.where_in_strings("feedID", getFeedIDofCategorie(catID)); m_db.simple_query(query.to_string()); } public void markFeedRead(string feedID) { m_db.execute("UPDATE main.articles SET unread = ? WHERE feedID = ?", { ArticleStatus.READ, feedID }); } public void markAllRead() { m_db.execute("UPDATE main.articles SET unread = ?", { ArticleStatus.READ }); } public void reset_subscribed_flag() { m_db.simple_query("UPDATE main.feeds SET \"subscribed\" = 0"); } public void reset_exists_tag() { m_db.simple_query("UPDATE main.tags SET \"exists\" = 0"); } public void reset_exists_flag() { m_db.simple_query("UPDATE main.categories SET \"exists\" = 0"); } public void delete_unsubscribed_feeds() { Logger.warning("DataBase: Deleting unsubscribed feeds"); m_db.simple_query("DELETE FROM main.feeds WHERE \"subscribed\" = 0"); } public void delete_nonexisting_categories() { Logger.warning("DataBase: Deleting nonexisting categories"); m_db.simple_query("DELETE FROM main.categories WHERE \"exists\" = 0"); } public void delete_nonexisting_tags() { Logger.warning("DataBase: Deleting nonexisting tags"); m_db.simple_query("DELETE FROM main.tags WHERE \"exists\" = 0"); } public void delete_articles_without_feed() { Logger.warning("DataBase: Deleting articles without feed"); var query = new QueryBuilder(QueryType.SELECT, "main.feeds"); query.select_field("feed_id"); query.where_equal_int("subscribed", 0); Sqlite.Statement stmt = m_db.prepare(query.to_string()); while(stmt.step () == Sqlite.ROW) { delete_articles(stmt.column_text(0)); } } public void delete_articles(string feedID) { Logger.warning(@"DataBase: Deleting all articles of feed \"$feedID\""); m_db.execute("DELETE FROM main.articles WHERE feedID = ?", { feedID }); m_db.execute("DELETE FROM main.Enclosures WHERE articleID IN(SELECT articleID FROM main.articles WHERE feedID = ?)", { feedID }); string folder_path = GLib.Environment.get_user_data_dir() + @"/feedreader/data/images/$feedID/"; Utils.remove_directory(folder_path); } public void delete_category(string catID) { m_db.execute("DELETE FROM main.categories WHERE categorieID = ?", { catID }); } public void rename_category(string catID, string newName) { if(FeedServer.get_default().tagIDaffectedByNameChange()) { var cat = read_category(catID); string newID = catID.replace(cat.getTitle(), newName); var query = "UPDATE categories SET categorieID = ?, title = ? WHERE categorieID = ?"; m_db.execute(query, {newID, newName, catID }); query = "UPDATE feeds SET category_id = replace(category_id, ?, ?) WHERE instr(category_id, ?)"; m_db.execute(query, { catID, newID, catID }); } else { var query = "UPDATE categories SET title = ? WHERE categorieID = ?"; m_db.execute(query, { newName, catID }); } } public void move_category(string catID, string newParentID) { var parent = read_category(newParentID); var query = "UPDATE categories SET Parent = ?, Level = ? WHERE categorieID = ?"; m_db.execute(query, { newParentID, parent.getLevel() + 1, catID }); } public void rename_feed(string feedID, string newName) { var query = "UPDATE feeds SET name = ? WHERE feed_id = ?"; m_db.execute(query, { newName, feedID }); } public void move_feed(string feedID, string currentCatID, string? newCatID = null) { var Feed = read_feed(feedID); var categories = Feed.getCatIDs(); categories.remove(currentCatID); if(newCatID != null) { categories.add(newCatID); } string catString = StringUtils.join(categories, ","); var query = "UPDATE feeds SET category_id = ? WHERE feed_id = ?"; m_db.execute(query, { catString, feedID }); } public void removeCatFromFeed(string feedID, string catID) { var feed = read_feed(feedID); m_db.execute("UPDATE feeds SET category_id = ? WHERE feed_id = ?", { feed.getCatString().replace(catID + ",", ""), feedID }); } public void delete_feed(string feedID) { m_db.execute("DELETE FROM feeds WHERE feed_id = ?", { feedID }); delete_articles(feedID); } public void addCachedAction(CachedActions action, string id, string? argument = "") { m_db.simple_query("BEGIN TRANSACTION"); var query = new QueryBuilder(QueryType.INSERT_OR_IGNORE, "main.CachedActions"); query.insert_param("action", "$ACTION"); query.insert_param("id", "$ID"); query.insert_param("argument", "$ARGUMENT"); Sqlite.Statement stmt = m_db.prepare(query.to_string()); int action_position = stmt.bind_parameter_index("$ACTION"); int id_position = stmt.bind_parameter_index("$ID"); int argument_position = stmt.bind_parameter_index("$ARGUMENT"); assert (action_position > 0); assert (id_position > 0); assert (argument_position > 0); stmt.bind_int (action_position, action); stmt.bind_text(id_position, id); stmt.bind_text(argument_position, argument); while (stmt.step () == Sqlite.ROW) {} stmt.reset (); m_db.simple_query("COMMIT TRANSACTION"); } public Gee.List readCachedActions() { var query = "SELECT * FROM CachedActions"; var rows = m_db.execute(query); var actions = new Gee.ArrayList(); foreach(var row in rows) { var action = new CachedAction( (CachedActions)row[0].to_int(), row[1].to_string(), row[2].to_string()); action.print(); actions.add(action); } return actions; } public void resetCachedActions() { Logger.warning("resetCachedActions"); m_db.simple_query("DELETE FROM CachedActions"); } public bool cachedActionNecessary(CachedAction action) { var query = "SELECT COUNT(*) FROM CachedActions WHERE argument = ? AND id = ? AND action = ?"; var rows = m_db.execute(query, { action.getArgument(), action.getID(), action.opposite() }); assert(rows.size == 1 && rows[0].size == 1); return rows[0][0].to_int() == 0; } public void deleteOppositeCachedAction(CachedAction action) { var query = "DELETE FROM CachedActions WHERE argument = ? AND id = ? AND action = ?"; m_db.execute(query, { action.getArgument(), action.getID(), action.opposite() }); } }