// 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.Utils : GLib.Object { private static Soup.Session? m_session; public static Soup.Session getSession() { if(m_session == null) { m_session = new Soup.Session(); m_session.user_agent = Constants.USER_AGENT; m_session.ssl_strict = false; m_session.timeout = 5; } return m_session; } public static void generatePreviews(Gee.List
articles) { string noPreview = _("No Preview Available"); var db = DataBase.readOnly(); foreach(var Article in articles) { if(!db.article_exists(Article.getArticleID())) { if(Article.getPreview() != null && Article.getPreview() != "") { continue; } if(!db.preview_empty(Article.getArticleID())) { continue; } else if(Article.getHTML() != "" && Article.getHTML() != null) { Logger.debug("Utils: generate preview for article: " + Article.getArticleID()); string output = Utils.UTF8fix(Article.getHTML(), true); if(output != null) { output = output.strip(); } if(output == "" || output == null) { Logger.info("generatePreviews: no Preview"); Article.setPreview(noPreview); continue; } string xml = "'); output = output.slice(end+1, output.length).chug(); output = output.strip(); } output = output.replace("\n"," "); output = output.replace("_"," "); Article.setPreview(output.chug()); } else { Logger.debug("no html to create preview from"); Article.setPreview(noPreview); } Article.setTitle(Utils.UTF8fix(Article.getTitle(), true)); } } } public static void checkHTML(Gee.List
articles) { var db = DataBase.readOnly(); foreach(var Article in articles) { if(!db.article_exists(Article.getArticleID())) { string modified_html = _("No Text available for this article :("); if(Article.getHTML() != "") { modified_html = Article.getHTML().replace("src=\"//","src=\"http://"); } Article.setHTML(modified_html); } } } public static string UTF8fix(string? old_string, bool remove_html = false) { if(old_string == null) { Logger.warning("Utils.UTF8fix: string is NULL"); return "NULL"; } string output = old_string; if (remove_html) { output = Htmlclean.strip_html(output); } // Strip and replace chars after HTML cleaning because the HTML cleaner // can potentially inserting newlines, whitespace or invalid chars output = output.make_valid().replace("\n"," ").strip(); return output; } public static string[] getDefaultExpandedCategories() { return {CategoryID.MASTER.to_string(), CategoryID.TAGS.to_string()}; } /*public static GLib.DateTime convertStringToDate(string date) { return new GLib.DateTime( new TimeZone.local(), int.parse(date.substring(0, date.index_of_nth_char(4))), // year int.parse(date.substring(date.index_of_nth_char(5), date.index_of_nth_char(7) - date.index_of_nth_char(5))), // month int.parse(date.substring(date.index_of_nth_char(8), date.index_of_nth_char(10) - date.index_of_nth_char(8))), // day int.parse(date.substring(date.index_of_nth_char(11), date.index_of_nth_char(13) - date.index_of_nth_char(11))), // hour int.parse(date.substring(date.index_of_nth_char(14), date.index_of_nth_char(16) - date.index_of_nth_char(14))), // min int.parse(date.substring(date.index_of_nth_char(17), date.index_of_nth_char(19) - date.index_of_nth_char(17))) // sec ); }*/ public static bool springCleaningNecessary() { var lastClean = new DateTime.from_unix_local(Settings.state().get_int("last-spring-cleaning")); var now = new DateTime.now_local(); var difference = now.difference(lastClean); bool doCleaning = false; Logger.debug("last clean: %s".printf(lastClean.format("%Y-%m-%d %H:%M:%S"))); Logger.debug("now: %s".printf(now.format("%Y-%m-%d %H:%M:%S"))); Logger.debug("difference: %f".printf(difference/GLib.TimeSpan.DAY)); if((difference/GLib.TimeSpan.DAY) >= Settings.general().get_int("spring-clean-after")) { doCleaning = true; } return doCleaning; } // thanks to // http://kuikie.com/snippet/79-8/vala/strings/vala-generate-random-string/%7B$ROOT_URL%7D/terms/ public static string string_random(int length = 8, string charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890") { string random = ""; for(int i=0; i= 0) { errors += "GENERIC_ERROR "; flags -= GLib.TlsCertificateFlags.VALIDATE_ALL; } if(flags - GLib.TlsCertificateFlags.INSECURE >= 0) { errors += "INSECURE "; flags -= GLib.TlsCertificateFlags.INSECURE; } if(flags - GLib.TlsCertificateFlags.REVOKED >= 0) { errors += "REVOKED "; flags -= GLib.TlsCertificateFlags.REVOKED; } if(flags - GLib.TlsCertificateFlags.EXPIRED >= 0) { errors += "EXPIRED "; flags -= GLib.TlsCertificateFlags.EXPIRED; } if(flags - GLib.TlsCertificateFlags.NOT_ACTIVATED >= 0) { errors += "NOT_ACTIVATED "; flags -= GLib.TlsCertificateFlags.NOT_ACTIVATED; } if(flags - GLib.TlsCertificateFlags.BAD_IDENTITY >= 0) { errors += "BAD_IDENTITY "; flags -= GLib.TlsCertificateFlags.BAD_IDENTITY; } if(flags - GLib.TlsCertificateFlags.UNKNOWN_CA >= 0) { errors += "UNKNOWN_CA "; flags -= GLib.TlsCertificateFlags.UNKNOWN_CA; } return errors; } public static bool ping(string link) { Logger.debug("Ping: " + link); var uri = new Soup.URI(link); if(uri == null) { Logger.error(@"Ping failed: can't parse url $link! Seems to be not valid."); return false; } var message = new Soup.Message.from_uri("HEAD", uri); if(message == null) { Logger.error(@"Ping failed: can't send message to $link! Seems to be not valid."); return false; } var status = getSession().send_message(message); Logger.debug(@"Ping: status $status"); if(status >= 200 && status <= 208) { Logger.debug("Ping successful"); return true; } Logger.error(@"Ping: failed %u - %s".printf(status, Soup.Status.get_phrase(status))); return false; } public static bool remove_directory(string path, uint level = 0) { ++level; bool flag = false; try { var directory = GLib.File.new_for_path(path); var enumerator = directory.enumerate_children(GLib.FileAttribute.STANDARD_NAME, 0); GLib.FileInfo file_info; while((file_info = enumerator.next_file()) != null) { string file_name = file_info.get_name(); if((file_info.get_file_type()) == GLib.FileType.DIRECTORY) { remove_directory(path + file_name + "/", level); } var file = directory.get_child(file_name); file.delete(); } if(level == 1) { directory.delete(); } } catch (IOError.NOT_FOUND e) { } catch(GLib.Error e) { Logger.error("Utils - remove_directory: " + e.message); } return flag; } public static string shortenURL(string url) { string longURL = url; if(longURL.has_prefix("https://")) { longURL = longURL.substring(8); } else if(longURL.has_prefix("http://")) { longURL = longURL.substring(7); } if(longURL.has_prefix("www.")) { longURL = longURL.substring(4); } if(longURL.has_suffix("api/")) { longURL = longURL.substring(0, longURL.length - 4); } return longURL; } // thx to geary :) public static string prepareSearchQuery(string raw_query) { // Two goals here: // 1) append an * after every term so it becomes a prefix search // (see ), and // 2) strip out common words/operators that might get interpreted as // search operators. // We ignore everything inside quotes to give the user a way to // override our algorithm here. The idea is to offer one search query // syntax for Geary that we can use locally and via IMAP, etc. string quote_balanced = parseSearchTerm(raw_query).replace("'", " "); if(countChar(raw_query, '"') % 2 != 0) { // Remove the last quote if it's not balanced. This has the // benefit of showing decent results as you type a quoted phrase. int last_quote = raw_query.last_index_of_char('"'); assert(last_quote >= 0); quote_balanced = raw_query.splice(last_quote, last_quote + 1, " "); } string[] words = quote_balanced.split_set(" \t\r\n:()%*\\"); bool in_quote = false; StringBuilder prepared_query = new StringBuilder(); foreach(string s in words) { s = s.strip(); int quotes = countChar(s, '"'); if(!in_quote && quotes > 0) { in_quote = true; --quotes; } if(!in_quote) { string lower = s.down(); if(lower == "" || lower == "and" || lower == "or" || lower == "not" || lower == "near" || lower.has_prefix("near/")) { continue; } if(s.has_prefix("-")) { s = s.substring(1); } if(s == "") { continue; } s = "\"" + s + "*\""; } if(in_quote && quotes % 2 != 0) { in_quote = false; } prepared_query.append(s); prepared_query.append(" "); } assert(!in_quote); return prepared_query.str.strip(); } public static int countChar(string s, unichar c) { int count = 0; for (int index = 0; (index = s.index_of_char(c, index)) >= 0; ++index, ++count) { ; } return count; } public static string parseSearchTerm(string searchTerm) { if(searchTerm.has_prefix("title: ")) { return searchTerm.substring(7); } if(searchTerm.has_prefix("author: ")) { return searchTerm.substring(8); } if(searchTerm.has_prefix("content: ")) { return searchTerm.substring(9); } return searchTerm; } public static bool categoryIsPopulated(string catID, Gee.List feeds) { foreach(Feed feed in feeds) { var ids = feed.getCatIDs(); foreach(string id in ids) { if(id == catID) { return true; } } } return false; } public static uint categoryGetUnread(string catID, Gee.List feeds) { uint unread = 0; foreach(Feed feed in feeds) { var ids = feed.getCatIDs(); foreach(string id in ids) { if(id == catID) { unread += feed.getUnread(); break; } } } return unread; } public static void resetSettings(GLib.Settings settings) { Logger.warning("Resetting setting " + settings.schema_id); var keys = settings.list_keys(); foreach(string key in keys) { settings.reset(key); } } public static string URLtoFeedName(string? url) { if(url == null) { return ""; } var feedname = new GLib.StringBuilder(url); if(feedname.str.has_suffix("/")) { feedname.erase(feedname.str.char_count()-1); } if(feedname.str.has_prefix("https://")) { feedname.erase(0, 8); } if(feedname.str.has_prefix("http://")) { feedname.erase(0, 7); } if(feedname.str.has_prefix("www.")) { feedname.erase(0, 4); } return feedname.str; } public static async bool file_exists(string path_str, FileType expected_type) { var path = GLib.File.new_for_path(path_str); try { var info = yield path.query_info_async("standard::type", FileQueryInfoFlags.NONE); return info.get_file_type () == expected_type; } catch(Error e) { return false; } } public static async bool ensure_path(string path_str) { var path = GLib.File.new_for_path(path_str); if(yield file_exists(path_str, FileType.DIRECTORY)) { return true; } try { path.make_directory_with_parents(); return true; } catch(IOError.EXISTS e) { return true; } catch(Error e) { Logger.error(@"ensure_path: Failed to create folder $path_str: " + e.message); return false; } } public static string gsettingReadString(GLib.Settings setting, string key) { string val = setting.get_string(key); if(val == "") { Logger.warning("Utils.gsettingReadString: failed to read %s %s".printf(setting.schema_id, key)); } return val; } public static void gsettingWriteString(GLib.Settings setting, string key, string val) { if(val == "" || val == null) { Logger.warning("Utils.gsettingWriteString: resetting %s %s".printf(setting.schema_id, key)); } if(!setting.set_string(key, val)) { Logger.error("Utils.gsettingWriteString: writing %s %s failed".printf(setting.schema_id, key)); } } public static async uint8[] inputStreamToArray(InputStream stream, Cancellable? cancellable=null) throws Error { Array result = new Array(); uint8[] buffer = new uint8[1024]; while(true) { size_t bytesRead = 0; yield stream.read_all_async(buffer, Priority.DEFAULT_IDLE, cancellable, out bytesRead); if (bytesRead == 0) { break; } result.append_vals(buffer, (uint)bytesRead); } return result.data; } public static string buildArticle(string html, string title, string url, string? author, string date, string feedID) { var article = new GLib.StringBuilder(); string author_date = ""; if(author != null) { author_date += _("posted by: %s, ").printf(author); } author_date += date; try { uint8[] contents; var file = File.new_for_uri("resource:///org/gnome/FeedReader/ArticleView/article.html"); file.load_contents(null, out contents, null); article.assign((string)contents); } catch(GLib.Error e) { Logger.error("Utils.buildArticle: %s".printf(e.message)); } string html_id = "$HTML"; int html_pos = article.str.index_of(html_id); article.erase(html_pos, html_id.length); article.insert(html_pos, html); string author_id = "$AUTHOR"; int author_pos = article.str.index_of(author_id); article.erase(author_pos, author_id.length); article.insert(author_pos, author_date); string title_id = "$TITLE"; int title_pos = article.str.index_of(title_id); article.erase(title_pos, title_id.length); article.insert(title_pos, title); string url_id = "$URL"; int url_pos = article.str.index_of(url_id); article.erase(url_pos, url_id.length); article.insert(url_pos, url); string feed_id = "$FEED"; int feed_pos = article.str.index_of(feed_id); article.erase(feed_pos, feed_id.length); var feed = DataBase.readOnly().read_feed(feedID); article.insert(feed_pos, feed != null ? feed.getTitle() : ""); string theme = "theme "; switch(Settings.general().get_enum("article-theme")) { case ArticleTheme.DEFAULT: theme += "default"; break; case ArticleTheme.SPRING: theme += "spring"; break; case ArticleTheme.MIDNIGHT: theme += "midnight"; break; case ArticleTheme.PARCHMENT: theme += "parchment"; break; case ArticleTheme.GRUVBOX: theme += "gruvbox"; break; } string theme_id = "$THEME"; int theme_pos = article.str.index_of(theme_id); article.erase(theme_pos, theme_id.length); article.insert(theme_pos, theme); string select_id = "$UNSELECTABLE"; int select_pos = article.str.index_of(select_id); if(Settings.tweaks().get_boolean("article-select-text")) { article.erase(select_pos-1, select_id.length+1); } else { article.erase(select_pos, select_id.length); article.insert(select_pos, "unselectable"); } // A list of fonts we should try to use in order of preference // We will pass all of these to CSS in order var font_options = new Gee.ArrayList(); // Try to use the configured font if it exists var font_setting = Settings.general().get_value("font").get_maybe(); if (font_setting != null) { font_options.add(font_setting.get_string()); } // If there is no configured font, or it's broken, use the system default font var system_font = new GLib.Settings("org.gnome.desktop.interface").get_string("document-font-name"); if (system_font != null) { font_options.add(system_font); } // Backup if the system font is broken too font_options.add("sans"); var font_families = new Gee.ArrayList(); uint? font_size = null; // Find the first non-broken font from our options foreach (var font in font_options) { var desc = Pango.FontDescription.from_string(font); font_families.add(desc.get_family()); if (font_size == null && desc.get_size() > 0) { font_size = (uint)GLib.Math.roundf(desc.get_size()); } } if (font_size == null) { font_size = 12; } font_size = font_size / Pango.SCALE; string font_family = StringUtils.join(font_families, ", "); Logger.info(@"Font: $font_family @ $font_size"); string small_size = (font_size - 2).to_string(); string large_size = (font_size * 2).to_string(); string normal_size = font_size.to_string(); string fontfamily_id = "$FONTFAMILY"; int fontfamilly_pos = article.str.index_of(fontfamily_id); article.erase(fontfamilly_pos, fontfamily_id.length); article.insert(fontfamilly_pos, font_family + ", sans"); string fontsize_id = "$FONTSIZE"; string sourcefontsize_id = "$SMALLSIZE"; int fontsize_pos = article.str.index_of(fontsize_id); article.erase(fontsize_pos, fontsize_id.length); article.insert(fontsize_pos, normal_size); string largesize_id = "$LARGESIZE"; int largesize_pos = article.str.index_of(largesize_id); article.erase(largesize_pos, largesize_id.length); article.insert(largesize_pos, large_size); for(int i = article.str.index_of(sourcefontsize_id, 0); i != -1; i = article.str.index_of(sourcefontsize_id, i)) { article.erase(i, sourcefontsize_id.length); article.insert(i, small_size); } try { uint8[] contents; var file = File.new_for_uri("resource:///org/gnome/FeedReader/ArticleView/style.css"); file.load_contents(null, out contents, null); string css_id = "$CSS"; int css_pos = article.str.index_of(css_id); article.erase(css_pos, css_id.length); article.insert(css_pos, (string)contents); } catch(GLib.Error e) { Logger.error("Utils.buildArticle: load CSS: " + e.message); } return article.str; } public static bool canManipulateContent(bool? online = null) { // if backend = local RSS -> return true; if(Settings.general().get_string("plugin") == "local") { return true; } if(!FeedReaderBackend.get_default().supportFeedManipulation()) { return false; } // when we already know wheather feedreader is online or offline if(online != null) { if(online) { return true; } else { return false; } } // otherwise check if online return FeedReaderBackend.get_default().isOnline(); } public static GLib.Menu getMenu() { var urlMenu = new GLib.Menu(); urlMenu.append(Menu.bugs, "win.bugs"); urlMenu.append(Menu.bounty, "win.bounty"); var aboutMenu = new GLib.Menu(); aboutMenu.append(Menu.shortcuts, "win.shortcuts"); aboutMenu.append(Menu.about, "win.about"); var menu = new GLib.Menu(); menu.append(Menu.settings, "win.settings"); menu.append(Menu.reset, "win.reset"); menu.append_section("", urlMenu); menu.append_section("", aboutMenu); return menu; } public static bool onlyShowFeeds() { if(Settings.general().get_boolean("only-feeds")) { return true; } var db = DataBase.readOnly(); if(!db.haveCategories() && !FeedReaderBackend.get_default().supportTags() && !db.haveFeedsWithoutCat()) { return true; } return false; } public static void saveImageDialog(string imagePath) { try { string articleName = "Article.pdf"; string? articleID = ColumnView.get_default().displayedArticle(); if(articleID != null) { articleName = DataBase.readOnly().read_article(articleID).getTitle(); } var file = GLib.File.new_for_path(imagePath); var mimeType = file.query_info("standard::content-type", 0, null).get_content_type(); var filter = new Gtk.FileFilter(); filter.add_mime_type(mimeType); var map = new Gee.HashMap(); map.set("image/gif", ".gif"); map.set("image/jpeg", ".jpeg"); map.set("image/png", ".png"); map.set("image/x-icon", ".ico"); var save_dialog = new Gtk.FileChooserDialog("Save Image", MainWindow.get_default(), Gtk.FileChooserAction.SAVE, _("Cancel"), Gtk.ResponseType.CANCEL, _("Save"), Gtk.ResponseType.ACCEPT); save_dialog.set_do_overwrite_confirmation(true); save_dialog.set_modal(true); save_dialog.set_current_folder(GLib.Environment.get_user_data_dir()); save_dialog.set_current_name(articleName + map.get(mimeType)); save_dialog.set_filter(filter); save_dialog.response.connect((dialog, response_id) => { switch(response_id) { case Gtk.ResponseType.ACCEPT: try { var savefile = save_dialog.get_file(); uint8[] data; string etag; file.load_contents(null, out data, out etag); savefile.replace_contents(data, null, false, GLib.FileCreateFlags.REPLACE_DESTINATION, null, null); } catch(Error e) { Logger.debug("imagePopup: save file: " + e.message); } break; case Gtk.ResponseType.CANCEL: default: break; } save_dialog.destroy(); }); save_dialog.show(); } catch(GLib.Error e) { Logger.error("Utils.saveImageDialog: %s".printf(e.message)); } } public static void playMedia(string[] args, string url) { Gtk.init(ref args); Gst.init(ref args); Logger.init(true); var window = new Gtk.Window(); window.set_size_request(800, 600); window.destroy.connect(Gtk.main_quit); var header = new Gtk.HeaderBar(); header.show_close_button = true; Gtk.CssProvider provider = new Gtk.CssProvider(); provider.load_from_resource("/org/gnome/FeedReader/gtk-css/basics.css"); weak Gdk.Display display = Gdk.Display.get_default(); weak Gdk.Screen screen = display.get_default_screen(); Gtk.StyleContext.add_provider_for_screen(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER); var player = new FeedReader.MediaPlayer(url); window.add(player); window.set_titlebar(header); window.show_all(); Gtk.main(); } public static Gtk.Image checkIcon(string name, string fallback, Gtk.IconSize size) { Gtk.Image icon = null; if(Gtk.IconTheme.get_default().lookup_icon(name, 0, Gtk.IconLookupFlags.FORCE_SVG) != null) { icon = new Gtk.Image.from_icon_name(name, size); } else { icon = new Gtk.Image.from_icon_name(fallback, size); } return icon; } public static void openInGedit(string text) { try { string filename = "file:///tmp/FeedReader_crashed_html.txt"; FileUtils.set_contents(filename, text); Gtk.show_uri_on_window(MainWindow.get_default(), filename, Gdk.CURRENT_TIME); } catch(GLib.Error e) { Logger.error("Utils.openInGedit(): %s".printf(e.message)); } } public static uint getRelevantArticles() { var interfacestate = MainWindow.get_default().getInterfaceState(); string[] selectedRow = interfacestate.getFeedListSelectedRow().split(" ", 2); ArticleListState state = interfacestate.getArticleListState(); string searchTerm = interfacestate.getSearchTerm(); string? topRow = interfacestate.getArticleListTopRow(); FeedListType IDtype = FeedListType.FEED; Logger.debug("selectedRow 0: %s".printf(selectedRow[0])); Logger.debug("selectedRow 1: %s".printf(selectedRow[1])); switch(selectedRow[0]) { case "feed": IDtype = FeedListType.FEED; break; case "cat": IDtype = FeedListType.CATEGORY; break; case "tag": IDtype = FeedListType.TAG; break; } int count = 0; if(topRow != null) { count = DataBase.readOnly().getArticleCountNewerThanID(topRow, selectedRow[1], IDtype, state, searchTerm); } Logger.debug(@"getRelevantArticles: $count"); return count; } }