// 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;
}
}