remove a lot of stuff from global context (functions.php), add a few helper classes instead

This commit is contained in:
Andrew Dolgov 2020-09-22 09:04:33 +03:00
parent d04ac399ff
commit 74568df4ff
39 changed files with 1379 additions and 1473 deletions

View File

@ -67,7 +67,7 @@
return;
}
load_user_plugins( $_SESSION["uid"]);
UserHelper::load_user_plugins( $_SESSION["uid"]);
}
$method = strtolower($_REQUEST["op"]);

View File

@ -41,7 +41,7 @@
}
if (SINGLE_USER_MODE) {
authenticate_user( "admin", null);
UserHelper::authenticate( "admin", null);
}
if ($_SESSION["uid"]) {
@ -50,7 +50,7 @@
print error_json(6);
return;
}
load_user_plugins( $_SESSION["uid"]);
UserHelper::load_user_plugins($_SESSION["uid"]);
}
$purge_intervals = array(

View File

@ -74,10 +74,10 @@ class API extends Handler {
}
if (get_pref("ENABLE_API_ACCESS", $uid)) {
if (authenticate_user($login, $password, false, Auth_Base::AUTH_SERVICE_API)) { // try login with normal password
if (UserHelper::authenticate($login, $password, false, Auth_Base::AUTH_SERVICE_API)) { // try login with normal password
$this->wrap(self::STATUS_OK, array("session_id" => session_id(),
"api_level" => self::API_LEVEL));
} else if (authenticate_user($login, $password_base64, false, Auth_Base::AUTH_SERVICE_API)) { // else try with base64_decoded password
} else if (UserHelper::authenticate($login, $password_base64, false, Auth_Base::AUTH_SERVICE_API)) { // else try with base64_decoded password
$this->wrap(self::STATUS_OK, array("session_id" => session_id(),
"api_level" => self::API_LEVEL));
} else { // else we are not logged in
@ -91,7 +91,7 @@ class API extends Handler {
}
function logout() {
logout_user();
Pref_Users::logout_user();
$this->wrap(self::STATUS_OK, array("status" => "OK"));
}
@ -343,7 +343,7 @@ class API extends Handler {
);
if ($sanitize_content) {
$article["content"] = sanitize(
$article["content"] = Sanitizer::sanitize(
$line["content"],
API::param_to_bool($line['hide_images']),
false, $line["site_url"], false, $line["id"]);
@ -748,7 +748,7 @@ class API extends Handler {
if ($show_content) {
if ($sanitize_content) {
$headline_row["content"] = sanitize(
$headline_row["content"] = Sanitizer::sanitize(
$line["content"],
API::param_to_bool($line['hide_images']),
false, $line["site_url"], false, $line["id"]);

View File

@ -19,8 +19,8 @@ class Backend extends Handler_Protected {
$topic = basename(clean($_REQUEST["topic"])); // only one for now
if ($topic == "main") {
$info = get_hotkeys_info();
$imap = get_hotkeys_map();
$info = RPC::get_hotkeys_info();
$imap = RPC::get_hotkeys_map();
$omap = array();
foreach ($imap[1] as $sequence => $action) {

View File

@ -113,4 +113,13 @@ class Db
return self::$instance->pdo;
}
public static function sql_random_function() {
if (DB_TYPE == "mysql") {
return "RAND()";
} else {
return "RANDOM()";
}
}
}

View File

@ -269,7 +269,7 @@ class DiskCache {
header("Content-Disposition: inline; filename=\"${filename}${fake_extension}\"");
return send_local_file($this->getFullPath($filename));
return $this->send_local_file($this->getFullPath($filename));
}
public function getUrl($filename) {
@ -359,4 +359,56 @@ class DiskCache {
}
}
}
/* this is essentially a wrapper for readfile() which allows plugins to hook
output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
hook function should return true if request was handled (or at least attempted to)
note that this can be called without user context so the plugin to handle this
should be loaded systemwide in config.php */
function send_local_file($filename) {
if (file_exists($filename)) {
if (is_writable($filename)) touch($filename);
$mimetype = mime_content_type($filename);
// this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
// video files are detected as octet-stream by mime_content_type()
if ($mimetype == "application/octet-stream")
$mimetype = "video/mp4";
# block SVG because of possible embedded javascript (.....)
$mimetype_blacklist = [ "image/svg+xml" ];
/* only serve video and images */
if (!preg_match("/(image|video)\//", $mimetype) || in_array($mimetype, $mimetype_blacklist)) {
http_response_code(400);
header("Content-type: text/plain");
print "Stored file has disallowed content type ($mimetype)";
return false;
}
$tmppluginhost = new PluginHost();
$tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
$tmppluginhost->load_data();
foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
if ($plugin->hook_send_local_file($filename)) return true;
}
header("Content-type: $mimetype");
$stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
header("Last-Modified: $stamp", true);
return readfile($filename);
} else {
return false;
}
}
}

View File

@ -305,7 +305,7 @@ class Feeds extends Handler_Protected {
$line["buttons"] .= $p->hook_article_button($line);
}
$line["content"] = sanitize($line["content"],
$line["content"] = Sanitizer::sanitize($line["content"],
$line['hide_images'], false, $line["site_url"], $highlight_words, $line["id"]);
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE_CDM) as $p) {
@ -550,7 +550,7 @@ class Feeds extends Handler_Protected {
"disable_cache" => (bool) $disable_cache];
// this is parsed by handleRpcJson() on first viewfeed() to set cdm expanded, etc
$reply['runtime-info'] = make_runtime_info();
$reply['runtime-info'] = RPC::make_runtime_info();
$reply_json = json_encode($reply);
@ -1124,11 +1124,11 @@ class Feeds extends Handler_Protected {
$pdo = Db::pdo();
$url = validate_url($url);
$url = UrlHelper::validate($url);
if (!$url) return array("code" => 2);
$contents = @fetch_file_contents($url, false, $auth_login, $auth_pass);
$contents = @UrlHelper::fetch($url, false, $auth_login, $auth_pass);
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SUBSCRIBE_FEED) as $plugin) {
$contents = $plugin->hook_subscribe_feed($contents, $url, $auth_login, $auth_pass);
@ -1924,7 +1924,7 @@ class Feeds extends Handler_Protected {
}
static function get_feeds_from_html($url, $content) {
$url = validate_url($url);
$url = UrlHelper::validate($url);
$baseUrl = substr($url, 0, strrpos($url, '/') + 1);
$feedUrls = [];

View File

@ -81,7 +81,7 @@ class Handler_Public extends Handler {
$tpl->setVariable('SELF_URL', htmlspecialchars(get_self_url_prefix()), true);
while ($line = $result->fetch()) {
$line["content_preview"] = sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) {
$line = $p->hook_query_headlines($line);
@ -98,7 +98,7 @@ class Handler_Public extends Handler {
$tpl->setVariable('ARTICLE_TITLE', htmlspecialchars($line['title']), true);
$tpl->setVariable('ARTICLE_EXCERPT', $line["content_preview"], true);
$content = sanitize($line["content"], false, $owner_uid,
$content = Sanitizer::sanitize($line["content"], false, $owner_uid,
$feed_site_url, false, $line["id"]);
$content = DiskCache::rewriteUrls($content);
@ -180,7 +180,7 @@ class Handler_Public extends Handler {
while ($line = $result->fetch()) {
$line["content_preview"] = sanitize(truncate_string(strip_tags($line["content_preview"]), 100, '...'));
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content_preview"]), 100, '...'));
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) {
$line = $p->hook_query_headlines($line, 100);
@ -196,7 +196,7 @@ class Handler_Public extends Handler {
$article['link'] = $line['link'];
$article['title'] = $line['title'];
$article['excerpt'] = $line["content_preview"];
$article['content'] = sanitize($line["content"], false, $owner_uid, $feed_site_url, false, $line["id"]);
$article['content'] = Sanitizer::sanitize($line["content"], false, $owner_uid, $feed_site_url, false, $line["id"]);
$article['updated'] = date('c', strtotime($line["updated"]));
if ($line['note']) $article['note'] = $line['note'];
@ -284,7 +284,7 @@ class Handler_Public extends Handler {
function logout() {
if (validate_csrf($_POST["csrf_token"])) {
logout_user();
Pref_Users::logout_user();
header("Location: index.php");
} else {
header("Content-Type: text/json");
@ -343,7 +343,7 @@ class Handler_Public extends Handler {
$line["tags"] = Article::get_article_tags($id, $owner_uid, $line["tag_cache"]);
unset($line["tag_cache"]);
$line["content"] = sanitize($line["content"],
$line["content"] = Sanitizer::sanitize($line["content"],
$line['hide_images'],
$owner_uid, $line["site_url"], false, $line["id"]);
@ -470,7 +470,7 @@ class Handler_Public extends Handler {
if (!$format) $format = 'atom';
if (SINGLE_USER_MODE) {
authenticate_user("admin", null);
UserHelper::authenticate("admin", null);
}
$owner_id = false;
@ -508,7 +508,7 @@ class Handler_Public extends Handler {
function sharepopup() {
if (SINGLE_USER_MODE) {
login_sequence();
UserHelper::login_sequence();
}
header('Content-Type: text/html; charset=utf-8');
@ -681,7 +681,7 @@ class Handler_Public extends Handler {
@session_set_cookie_params(0);
}
if (authenticate_user($login, $password)) {
if (UserHelper::authenticate($login, $password)) {
$_POST["password"] = "";
if (get_schema_version() >= 120) {
@ -729,7 +729,7 @@ class Handler_Public extends Handler {
function subscribe() {
if (SINGLE_USER_MODE) {
login_sequence();
UserHelper::login_sequence();
}
if ($_SESSION["uid"]) {
@ -878,7 +878,7 @@ class Handler_Public extends Handler {
print "</div></div></body></html>";
} else {
render_login_form();
$this->render_login_form();
}
}
@ -1092,7 +1092,7 @@ class Handler_Public extends Handler {
if (!SINGLE_USER_MODE && $_SESSION["access_level"] < 10) {
$_SESSION["login_error_msg"] = __("Your access level is insufficient to run this script.");
render_login_form();
$this->render_login_form();
exit;
}
@ -1272,5 +1272,13 @@ class Handler_Public extends Handler {
print error_json(14);
}
}
static function render_login_form() {
header('Cache-Control: public');
require_once "login_form.php";
exit;
}
}
?>

View File

@ -1703,7 +1703,7 @@ class Pref_Feeds extends Handler_Protected {
foreach ($feeds as $feed) {
$feed = trim($feed);
if (validate_url($feed)) {
if (UrlHelper::validate($feed)) {
$this->pdo->beginTransaction();

View File

@ -257,7 +257,7 @@ class Pref_Prefs extends Handler_Protected {
AND owner_uid = :uid");
$sth->execute([":profile" => $_SESSION['profile'], ":uid" => $_SESSION['uid']]);
initialize_user_prefs($_SESSION["uid"], $_SESSION["profile"]);
$this->initialize_user_prefs($_SESSION["uid"], $_SESSION["profile"]);
echo __("Your preferences are now set to default values.");
}
@ -590,9 +590,9 @@ class Pref_Prefs extends Handler_Protected {
if ($profile) {
print_notice(__("Some preferences are only available in default profile."));
initialize_user_prefs($_SESSION["uid"], $profile);
$this->initialize_user_prefs($_SESSION["uid"], $profile);
} else {
initialize_user_prefs($_SESSION["uid"]);
$this->initialize_user_prefs($_SESSION["uid"]);
}
$prefs_available = [];
@ -1366,4 +1366,57 @@ class Pref_Prefs extends Handler_Protected {
$this->appPasswordList();
}
static function initialize_user_prefs($uid, $profile = false) {
if (get_schema_version() < 63) $profile_qpart = "";
$pdo = Db::pdo();
$in_nested_tr = false;
try {
$pdo->beginTransaction();
} catch (Exception $e) {
$in_nested_tr = true;
}
$sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
if (!is_numeric($profile) || !$profile || get_schema_version() < 63) $profile = null;
$u_sth = $pdo->prepare("SELECT pref_name
FROM ttrss_user_prefs WHERE owner_uid = :uid AND
(profile = :profile OR (:profile IS NULL AND profile IS NULL))");
$u_sth->execute([':uid' => $uid, ':profile' => $profile]);
$active_prefs = array();
while ($line = $u_sth->fetch()) {
array_push($active_prefs, $line["pref_name"]);
}
while ($line = $sth->fetch()) {
if (array_search($line["pref_name"], $active_prefs) === false) {
// print "adding " . $line["pref_name"] . "<br>";
if (get_schema_version() < 63) {
$i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
(owner_uid,pref_name,value) VALUES
(?, ?, ?)");
$i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
} else {
$i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
(owner_uid,pref_name,value, profile) VALUES
(?, ?, ?, ?)");
$i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
}
}
}
if (!$in_nested_tr) $pdo->commit();
}
}

View File

@ -259,7 +259,7 @@ class Pref_Users extends Handler_Protected {
print T_sprintf("Added user %s with password %s",
$login, $tmp_user_pwd);
initialize_user($new_uid);
$this->initialize_user($new_uid);
} else {
@ -443,4 +443,25 @@ class Pref_Users extends Handler_Protected {
return $default;
}
// this is called after user is created to initialize default feeds, labels
// or whatever else
// user preferences are checked on every login, not here
static function initialize_user($uid) {
$pdo = Db::pdo();
$sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
values (?, 'Tiny Tiny RSS: Forum',
'https://tt-rss.org/forum/rss.php')");
$sth->execute([$uid]);
}
static function logout_user() {
@session_destroy();
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time()-42000, '/');
}
session_commit();
}
}

View File

@ -52,7 +52,7 @@ class RPC extends Handler_Protected {
$profile_id = $row['id'];
if ($profile_id) {
initialize_user_prefs($_SESSION["uid"], $profile_id);
Pref_Prefs::initialize_user_prefs($_SESSION["uid"], $profile_id);
}
}
}
@ -279,7 +279,7 @@ class RPC extends Handler_Protected {
];
if ($seq % 2 == 0)
$reply['runtime-info'] = make_runtime_info();
$reply['runtime-info'] = $this->make_runtime_info();
print json_encode($reply);
}
@ -323,8 +323,8 @@ class RPC extends Handler_Protected {
$reply['error'] = sanity_check();
if ($reply['error']['code'] == 0) {
$reply['init-params'] = make_init_params();
$reply['runtime-info'] = make_runtime_info();
$reply['init-params'] = $this->make_init_params();
$reply['runtime-info'] = $this->make_runtime_info();
}
print json_encode($reply);
@ -461,7 +461,7 @@ class RPC extends Handler_Protected {
$updstart_thresh_qpart = "AND (ttrss_feeds.last_update_started IS NULL OR ttrss_feeds.last_update_started < DATE_SUB(NOW(), INTERVAL 5 MINUTE))";
}
$random_qpart = sql_random_function();
$random_qpart = Db::sql_random_function();
$pdo = Db::pdo();
@ -602,7 +602,7 @@ class RPC extends Handler_Protected {
get_version($git_commit, $git_timestamp);
if (defined('CHECK_FOR_UPDATES') && CHECK_FOR_UPDATES && $_SESSION["access_level"] >= 10 && $git_timestamp) {
$content = @fetch_file_contents(["url" => "https://tt-rss.org/version.json"]);
$content = @UrlHelper::fetch(["url" => "https://tt-rss.org/version.json"]);
if ($content) {
$content = json_decode($content, true);
@ -620,4 +620,292 @@ class RPC extends Handler_Protected {
print json_encode($rv);
}
private function make_init_params() {
$params = array();
foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
"ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
"CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
"HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
$params[strtolower($param)] = (int) get_pref($param);
}
$params["check_for_updates"] = CHECK_FOR_UPDATES;
$params["icons_url"] = ICONS_URL;
$params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
$params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
$params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
$params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
$params["bw_limit"] = (int) $_SESSION["bw_limit"];
$params["is_default_pw"] = Pref_Prefs::isdefaultpassword();
$params["label_base_index"] = (int) LABEL_BASE_INDEX;
$theme = get_pref( "USER_CSS_THEME", false, false);
$params["theme"] = theme_exists($theme) ? $theme : "";
$params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
$params["php_platform"] = PHP_OS;
$params["php_version"] = PHP_VERSION;
$params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
ttrss_feeds WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
$max_feed_id = $row["mid"];
$num_feeds = $row["nf"];
$params["self_url_prefix"] = get_self_url_prefix();
$params["max_feed_id"] = (int) $max_feed_id;
$params["num_feeds"] = (int) $num_feeds;
$params["hotkeys"] = $this->get_hotkeys_map();
$params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
$params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE;
$params["icon_indicator_white"] = $this->image_to_base64("images/indicator_white.gif");
$params["labels"] = Labels::get_all_labels($_SESSION["uid"]);
return $params;
}
private function image_to_base64($filename) {
if (file_exists($filename)) {
$ext = pathinfo($filename, PATHINFO_EXTENSION);
return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
} else {
return "";
}
}
static function make_runtime_info() {
$data = array();
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
ttrss_feeds WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
$max_feed_id = $row['mid'];
$num_feeds = $row['nf'];
$data["max_feed_id"] = (int) $max_feed_id;
$data["num_feeds"] = (int) $num_feeds;
$data['cdm_expanded'] = get_pref('CDM_EXPANDED');
$data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
if (LOG_DESTINATION == 'sql' && $_SESSION['access_level'] >= 10) {
if (DB_TYPE == 'pgsql') {
$log_interval = "created_at > NOW() - interval '1 hour'";
} else {
$log_interval = "created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)";
}
$sth = $pdo->prepare("SELECT COUNT(id) AS cid FROM ttrss_error_log WHERE $log_interval");
$sth->execute();
if ($row = $sth->fetch()) {
$data['recent_log_events'] = $row['cid'];
}
}
if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
$data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
if (time() - $_SESSION["daemon_stamp_check"] > 30) {
$stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
if ($stamp) {
$stamp_delta = time() - $stamp;
if ($stamp_delta > 1800) {
$stamp_check = 0;
} else {
$stamp_check = 1;
$_SESSION["daemon_stamp_check"] = time();
}
$data['daemon_stamp_ok'] = $stamp_check;
$stamp_fmt = date("Y.m.d, G:i", $stamp);
$data['daemon_stamp'] = $stamp_fmt;
}
}
}
return $data;
}
static function get_hotkeys_info() {
$hotkeys = array(
__("Navigation") => array(
"next_feed" => __("Open next feed"),
"prev_feed" => __("Open previous feed"),
"next_article_or_scroll" => __("Open next article (in combined mode, scroll down)"),
"prev_article_or_scroll" => __("Open previous article (in combined mode, scroll up)"),
"next_headlines_page" => __("Scroll headlines by one page down"),
"prev_headlines_page" => __("Scroll headlines by one page up"),
"next_article_noscroll" => __("Open next article"),
"prev_article_noscroll" => __("Open previous article"),
"next_article_noexpand" => __("Move to next article (don't expand)"),
"prev_article_noexpand" => __("Move to previous article (don't expand)"),
"search_dialog" => __("Show search dialog"),
"cancel_search" => __("Cancel active search")),
__("Article") => array(
"toggle_mark" => __("Toggle starred"),
"toggle_publ" => __("Toggle published"),
"toggle_unread" => __("Toggle unread"),
"edit_tags" => __("Edit tags"),
"open_in_new_window" => __("Open in new window"),
"catchup_below" => __("Mark below as read"),
"catchup_above" => __("Mark above as read"),
"article_scroll_down" => __("Scroll down"),
"article_scroll_up" => __("Scroll up"),
"article_page_down" => __("Scroll down page"),
"article_page_up" => __("Scroll up page"),
"select_article_cursor" => __("Select article under cursor"),
"email_article" => __("Email article"),
"close_article" => __("Close/collapse article"),
"toggle_expand" => __("Toggle article expansion (combined mode)"),
"toggle_widescreen" => __("Toggle widescreen mode"),
"toggle_full_text" => __("Toggle full article text via Readability")),
__("Article selection") => array(
"select_all" => __("Select all articles"),
"select_unread" => __("Select unread"),
"select_marked" => __("Select starred"),
"select_published" => __("Select published"),
"select_invert" => __("Invert selection"),
"select_none" => __("Deselect everything")),
__("Feed") => array(
"feed_refresh" => __("Refresh current feed"),
"feed_unhide_read" => __("Un/hide read feeds"),
"feed_subscribe" => __("Subscribe to feed"),
"feed_edit" => __("Edit feed"),
"feed_catchup" => __("Mark as read"),
"feed_reverse" => __("Reverse headlines"),
"feed_toggle_vgroup" => __("Toggle headline grouping"),
"feed_debug_update" => __("Debug feed update"),
"feed_debug_viewfeed" => __("Debug viewfeed()"),
"catchup_all" => __("Mark all feeds as read"),
"cat_toggle_collapse" => __("Un/collapse current category"),
"toggle_cdm_expanded" => __("Toggle auto expand in combined mode"),
"toggle_combined_mode" => __("Toggle combined mode")),
__("Go to") => array(
"goto_all" => __("All articles"),
"goto_fresh" => __("Fresh"),
"goto_marked" => __("Starred"),
"goto_published" => __("Published"),
"goto_read" => __("Recently read"),
"goto_tagcloud" => __("Tag cloud"),
"goto_prefs" => __("Preferences")),
__("Other") => array(
"create_label" => __("Create label"),
"create_filter" => __("Create filter"),
"collapse_sidebar" => __("Un/collapse sidebar"),
"help_dialog" => __("Show help dialog"))
);
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
$hotkeys = $plugin->hook_hotkey_info($hotkeys);
}
return $hotkeys;
}
// {3} - 3 panel mode only
// {C} - combined mode only
static function get_hotkeys_map() {
$hotkeys = array(
"k" => "next_feed",
"j" => "prev_feed",
"n" => "next_article_noscroll",
"p" => "prev_article_noscroll",
"N" => "article_page_down",
"P" => "article_page_up",
"*(33)|Shift+PgUp" => "article_page_up",
"*(34)|Shift+PgDn" => "article_page_down",
"{3}(38)|Up" => "prev_article_or_scroll",
"{3}(40)|Down" => "next_article_or_scroll",
"*(38)|Shift+Up" => "article_scroll_up",
"*(40)|Shift+Down" => "article_scroll_down",
"^(38)|Ctrl+Up" => "prev_article_noscroll",
"^(40)|Ctrl+Down" => "next_article_noscroll",
"/" => "search_dialog",
"\\" => "cancel_search",
"s" => "toggle_mark",
"S" => "toggle_publ",
"u" => "toggle_unread",
"T" => "edit_tags",
"o" => "open_in_new_window",
"c p" => "catchup_below",
"c n" => "catchup_above",
"a W" => "toggle_widescreen",
"a e" => "toggle_full_text",
"e" => "email_article",
"a q" => "close_article",
"a a" => "select_all",
"a u" => "select_unread",
"a U" => "select_marked",
"a p" => "select_published",
"a i" => "select_invert",
"a n" => "select_none",
"f r" => "feed_refresh",
"f a" => "feed_unhide_read",
"f s" => "feed_subscribe",
"f e" => "feed_edit",
"f q" => "feed_catchup",
"f x" => "feed_reverse",
"f g" => "feed_toggle_vgroup",
"f D" => "feed_debug_update",
"f G" => "feed_debug_viewfeed",
"f C" => "toggle_combined_mode",
"f c" => "toggle_cdm_expanded",
"Q" => "catchup_all",
"x" => "cat_toggle_collapse",
"g a" => "goto_all",
"g f" => "goto_fresh",
"g s" => "goto_marked",
"g p" => "goto_published",
"g r" => "goto_read",
"g t" => "goto_tagcloud",
"g P" => "goto_prefs",
"r" => "select_article_cursor",
"c l" => "create_label",
"c f" => "create_filter",
"c s" => "collapse_sidebar",
"?" => "help_dialog",
);
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
$hotkeys = $plugin->hook_hotkey_map($hotkeys);
}
$prefixes = array();
foreach (array_keys($hotkeys) as $hotkey) {
$pair = explode(" ", $hotkey, 2);
if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
array_push($prefixes, $pair[0]);
}
}
return array($prefixes, $hotkeys);
}
}

View File

@ -218,7 +218,7 @@ class RSSUtils {
}
if (!$basic_info) {
$feed_data = fetch_file_contents($fetch_url, false,
$feed_data = UrlHelper::fetch($fetch_url, false,
$auth_login, $auth_pass, false,
FEED_FETCH_TIMEOUT,
0);
@ -268,8 +268,6 @@ class RSSUtils {
*/
static function update_rss_feed($feed, $no_cache = false) {
reset_fetch_domain_quota();
Debug::log("start", Debug::$LOG_VERBOSE);
$pdo = Db::pdo();
@ -400,7 +398,7 @@ class RSSUtils {
Debug::log("fetching [$fetch_url] (force_refetch: $force_refetch)...", Debug::$LOG_VERBOSE);
$feed_data = fetch_file_contents([
$feed_data = UrlHelper::fetch([
"url" => $fetch_url,
"login" => $auth_login,
"pass" => $auth_pass,
@ -1225,7 +1223,7 @@ class RSSUtils {
global $fetch_last_error_code;
global $fetch_last_error;
$file_content = fetch_file_contents(array("url" => $src,
$file_content = UrlHelper::fetch(array("url" => $src,
"http_referrer" => $src,
"max_size" => MAX_CACHE_FILE_SIZE));
@ -1255,7 +1253,7 @@ class RSSUtils {
global $fetch_last_error_code;
global $fetch_last_error;
$file_content = fetch_file_contents(array("url" => $url,
$file_content = UrlHelper::fetch(array("url" => $url,
"http_referrer" => $url,
"max_size" => MAX_CACHE_FILE_SIZE));
@ -1517,7 +1515,7 @@ class RSSUtils {
static function housekeeping_user($owner_uid) {
$tmph = new PluginHost();
load_user_plugins($owner_uid, $tmph);
UserHelper::load_user_plugins($owner_uid, $tmph);
$tmph->run_hooks(PluginHost::HOOK_HOUSE_KEEPING, "hook_house_keeping", "");
}
@ -1546,7 +1544,7 @@ class RSSUtils {
if ($favicon_url) {
// Limiting to "image" type misses those served with text/plain
$contents = fetch_file_contents($favicon_url); // , "image");
$contents = UrlHelper::fetch($favicon_url); // , "image");
if ($contents) {
// Crude image type matching.
@ -1719,7 +1717,7 @@ class RSSUtils {
$favicon_url = false;
if ($html = @fetch_file_contents($url)) {
if ($html = @UrlHelper::fetch($url)) {
$doc = new DOMDocument();
if ($doc->loadHTML($html)) {

217
classes/sanitizer.php Normal file
View File

@ -0,0 +1,217 @@
<?php
class Sanitizer {
private static function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
$xpath = new DOMXPath($doc);
$entries = $xpath->query('//*');
foreach ($entries as $entry) {
if (!in_array($entry->nodeName, $allowed_elements)) {
$entry->parentNode->removeChild($entry);
}
if ($entry->hasAttributes()) {
$attrs_to_remove = array();
foreach ($entry->attributes as $attr) {
if (strpos($attr->nodeName, 'on') === 0) {
array_push($attrs_to_remove, $attr);
}
if (strpos($attr->nodeName, "data-") === 0) {
array_push($attrs_to_remove, $attr);
}
if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
array_push($attrs_to_remove, $attr);
}
if (in_array($attr->nodeName, $disallowed_attributes)) {
array_push($attrs_to_remove, $attr);
}
}
foreach ($attrs_to_remove as $attr) {
$entry->removeAttributeNode($attr);
}
}
}
return $doc;
}
public static function iframe_whitelisted($entry) {
@$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
if ($src) {
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_IFRAME_WHITELISTED) as $plugin) {
if ($plugin->hook_iframe_whitelisted($src))
return true;
}
}
return false;
}
public static function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
if (!$owner) $owner = $_SESSION["uid"];
$res = trim($str); if (!$res) return '';
$doc = new DOMDocument();
$doc->loadHTML('<?xml encoding="UTF-8">' . $res);
$xpath = new DOMXPath($doc);
$rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
$entries = $xpath->query('(//a[@href]|//img[@src]|//source[@srcset|@src])');
foreach ($entries as $entry) {
if ($entry->hasAttribute('href')) {
$entry->setAttribute('href',
rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
$entry->setAttribute('rel', 'noopener noreferrer');
$entry->setAttribute("target", "_blank");
}
if ($entry->hasAttribute('src')) {
$entry->setAttribute('src',
rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src')));
}
if ($entry->nodeName == 'img') {
$entry->setAttribute('referrerpolicy', 'no-referrer');
$entry->setAttribute('loading', 'lazy');
}
if ($entry->hasAttribute('srcset')) {
$matches = RSSUtils::decode_srcset($entry->getAttribute('srcset'));
for ($i = 0; $i < count($matches); $i++) {
$matches[$i]["url"] = rewrite_relative_url($rewrite_base_url, $matches[$i]["url"]);
}
$entry->setAttribute("srcset", RSSUtils::encode_srcset($matches));
}
if ($entry->hasAttribute('src') &&
($owner && get_pref("STRIP_IMAGES", $owner)) || $force_remove_images || $_SESSION["bw_limit"]) {
$p = $doc->createElement('p');
$a = $doc->createElement('a');
$a->setAttribute('href', $entry->getAttribute('src'));
$a->appendChild(new DOMText($entry->getAttribute('src')));
$a->setAttribute('target', '_blank');
$a->setAttribute('rel', 'noopener noreferrer');
$p->appendChild($a);
if ($entry->nodeName == 'source') {
if ($entry->parentNode && $entry->parentNode->parentNode)
$entry->parentNode->parentNode->replaceChild($p, $entry->parentNode);
} else if ($entry->nodeName == 'img') {
if ($entry->parentNode)
$entry->parentNode->replaceChild($p, $entry);
}
}
}
$entries = $xpath->query('//iframe');
foreach ($entries as $entry) {
if (!Sanitizer::iframe_whitelisted($entry)) {
$entry->setAttribute('sandbox', 'allow-scripts');
} else {
if (is_prefix_https()) {
$entry->setAttribute("src",
str_replace("http://", "https://",
$entry->getAttribute("src")));
}
}
}
$allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
'caption', 'cite', 'center', 'code', 'col', 'colgroup',
'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
'dt', 'em', 'footer', 'figure', 'figcaption',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
'ol', 'p', 'picture', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
$disallowed_attributes = array('id', 'style', 'class', 'width', 'height', 'allow');
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
$retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
if (is_array($retval)) {
$doc = $retval[0];
$allowed_elements = $retval[1];
$disallowed_attributes = $retval[2];
} else {
$doc = $retval;
}
}
$doc->removeChild($doc->firstChild); //remove doctype
$doc = Sanitizer::strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
$entries = $xpath->query('//iframe');
foreach ($entries as $entry) {
$div = $doc->createElement('div');
$div->setAttribute('class', 'embed-responsive');
$entry->parentNode->replaceChild($div, $entry);
$div->appendChild($entry);
}
if ($highlight_words && is_array($highlight_words)) {
foreach ($highlight_words as $word) {
// http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
$elements = $xpath->query("//*/text()");
foreach ($elements as $child) {
$fragment = $doc->createDocumentFragment();
$text = $child->textContent;
while (($pos = mb_stripos($text, $word)) !== false) {
$fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
$word = mb_substr($text, $pos, mb_strlen($word));
$highlight = $doc->createElement('span');
$highlight->appendChild(new DomText($word));
$highlight->setAttribute('class', 'highlight');
$fragment->appendChild($highlight);
$text = mb_substr($text, $pos + mb_strlen($word));
}
if (!empty($text)) $fragment->appendChild(new DomText($text));
$child->parentNode->replaceChild($fragment, $child);
}
}
}
$res = $doc->saveHTML();
/* strip everything outside of <body>...</body> */
$res_frag = array();
if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
return $res_frag[1];
} else {
return $res;
}
}
}

474
classes/urlhelper.php Normal file
View File

@ -0,0 +1,474 @@
<?php
class UrlHelper {
static function build_url($parts) {
$tmp = $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
if (isset($parts['query'])) $tmp .= '?' . $parts['query'];
if (isset($parts['fragment'])) $tmp .= '#' . $parts['fragment'];
return $tmp;
}
/**
* Converts a (possibly) relative URL to a absolute one.
*
* @param string $url Base URL (i.e. from where the document is)
* @param string $rel_url Possibly relative URL in the document
*
* @return string Absolute URL
*/
public static function rewrite_relative($url, $rel_url) {
$rel_parts = parse_url($rel_url);
if ($rel_parts['host'] && $rel_parts['scheme']) {
return UrlHelper::validate($rel_url);
} else if (strpos($rel_url, "//") === 0) {
# protocol-relative URL (rare but they exist)
return UrlHelper::validate("https:" . $rel_url);
} else if (strpos($rel_url, "magnet:") === 0) {
# allow magnet links
return $rel_url;
} else {
$parts = parse_url($url);
$rel_parts['host'] = $parts['host'];
$rel_parts['scheme'] = $parts['scheme'];
if (strpos($rel_parts['path'], '/') !== 0)
$rel_parts['path'] = '/' . $rel_parts['path'];
$rel_parts['path'] = str_replace("/./", "/", $rel_parts['path']);
$rel_parts['path'] = str_replace("//", "/", $rel_parts['path']);
return UrlHelper::validate(UrlHelper::build_url($rel_parts));
}
}
// extended filtering involves validation for safe ports and loopback
static function validate($url, $extended_filtering = false) {
$url = clean($url);
# fix protocol-relative URLs
if (strpos($url, "//") === 0)
$url = "https:" . $url;
if (filter_var($url, FILTER_VALIDATE_URL) === false)
return false;
$tokens = parse_url($url);
if (!$tokens['host'])
return false;
if (!in_array(strtolower($tokens['scheme']), ['http', 'https']))
return false;
if ($extended_filtering) {
if (!in_array($tokens['port'], [80, 443, '']))
return false;
if (strtolower($tokens['host']) == 'localhost' || $tokens['host'] == '::1' || strpos($tokens['host'], '127.') === 0)
return false;
}
//convert IDNA hostname to punycode if possible
if (function_exists("idn_to_ascii")) {
if (mb_detect_encoding($tokens['host']) != 'ASCII') {
$parts['host'] = idn_to_ascii($tokens['host']);
$url = UrlHelper::build_url($tokens);
}
}
return $url;
}
static function resolve_redirects($url, $timeout, $nest = 0) {
// too many redirects
if ($nest > 10)
return false;
if (version_compare(PHP_VERSION, '7.1.0', '>=')) {
$context_options = array(
'http' => array(
'header' => array(
'Connection: close'
),
'method' => 'HEAD',
'timeout' => $timeout,
'protocol_version'=> 1.1)
);
if (defined('_HTTP_PROXY')) {
$context_options['http']['request_fulluri'] = true;
$context_options['http']['proxy'] = _HTTP_PROXY;
}
$context = stream_context_create($context_options);
$headers = get_headers($url, 0, $context);
} else {
$headers = get_headers($url, 0);
}
if (is_array($headers)) {
$headers = array_reverse($headers); // last one is the correct one
foreach($headers as $header) {
if (stripos($header, 'Location:') === 0) {
$url = UrlHelper::rewrite_relative($url, trim(substr($header, strlen('Location:'))));
return resolve_redirects($url, $timeout, $nest + 1);
}
}
return $url;
}
// request failed?
return false;
}
// TODO: max_size currently only works for CURL transfers
// TODO: multiple-argument way is deprecated, first parameter is a hash now
public static function fetch($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
global $fetch_last_error;
global $fetch_last_error_code;
global $fetch_last_error_content;
global $fetch_last_content_type;
global $fetch_last_modified;
global $fetch_effective_url;
global $fetch_effective_ip_addr;
global $fetch_curl_used;
global $fetch_domain_hits;
$fetch_last_error = false;
$fetch_last_error_code = -1;
$fetch_last_error_content = "";
$fetch_last_content_type = "";
$fetch_curl_used = false;
$fetch_last_modified = "";
$fetch_effective_url = "";
$fetch_effective_ip_addr = "";
if (!is_array($fetch_domain_hits))
$fetch_domain_hits = [];
if (!is_array($options)) {
// falling back on compatibility shim
$option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
$tmp = [];
for ($i = 0; $i < func_num_args(); $i++) {
$tmp[$option_names[$i]] = func_get_arg($i);
}
$options = $tmp;
/*$options = array(
"url" => func_get_arg(0),
"type" => @func_get_arg(1),
"login" => @func_get_arg(2),
"pass" => @func_get_arg(3),
"post_query" => @func_get_arg(4),
"timeout" => @func_get_arg(5),
"timestamp" => @func_get_arg(6),
"useragent" => @func_get_arg(7)
); */
}
$url = $options["url"];
$type = isset($options["type"]) ? $options["type"] : false;
$login = isset($options["login"]) ? $options["login"] : false;
$pass = isset($options["pass"]) ? $options["pass"] : false;
$post_query = isset($options["post_query"]) ? $options["post_query"] : false;
$timeout = isset($options["timeout"]) ? $options["timeout"] : false;
$last_modified = isset($options["last_modified"]) ? $options["last_modified"] : "";
$useragent = isset($options["useragent"]) ? $options["useragent"] : false;
$followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
$max_size = isset($options["max_size"]) ? $options["max_size"] : MAX_DOWNLOAD_FILE_SIZE; // in bytes
$http_accept = isset($options["http_accept"]) ? $options["http_accept"] : false;
$http_referrer = isset($options["http_referrer"]) ? $options["http_referrer"] : false;
$url = ltrim($url, ' ');
$url = str_replace(' ', '%20', $url);
$url = UrlHelper::validate($url, true);
if (!$url) {
$fetch_last_error = "Requested URL failed extended validation.";
return false;
}
$url_host = parse_url($url, PHP_URL_HOST);
$ip_addr = gethostbyname($url_host);
if (!$ip_addr || strpos($ip_addr, "127.") === 0) {
$fetch_last_error = "URL hostname failed to resolve or resolved to a loopback address ($ip_addr)";
return false;
}
$fetch_domain_hits[$url_host] += 1;
/*if ($fetch_domain_hits[$url_host] > MAX_FETCH_REQUESTS_PER_HOST) {
user_error("Exceeded fetch request quota for $url_host: " . $fetch_domain_hits[$url_host], E_USER_WARNING);
#return false;
}*/
if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
$fetch_curl_used = true;
$ch = curl_init($url);
$curl_http_headers = [];
if ($last_modified && !$post_query)
array_push($curl_http_headers, "If-Modified-Since: $last_modified");
if ($http_accept)
array_push($curl_http_headers, "Accept: " . $http_accept);
if (count($curl_http_headers) > 0)
curl_setopt($ch, CURLOPT_HTTPHEADER, $curl_http_headers);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout ? $timeout : FILE_FETCH_CONNECT_TIMEOUT);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ? $timeout : FILE_FETCH_TIMEOUT);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, !ini_get("open_basedir") && $followlocation);
curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent :
SELF_USER_AGENT);
curl_setopt($ch, CURLOPT_ENCODING, "");
if ($http_referrer)
curl_setopt($ch, CURLOPT_REFERER, $http_referrer);
if ($max_size) {
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_BUFFERSIZE, 16384); // needed to get 5 arguments in progress function?
// holy shit closures in php
// download & upload are *expected* sizes respectively, could be zero
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($curl_handle, $download_size, $downloaded, $upload_size, $uploaded) use( &$max_size) {
Debug::log("[curl progressfunction] $downloaded $max_size", Debug::$LOG_EXTENDED);
return ($downloaded > $max_size) ? 1 : 0; // if max size is set, abort when exceeding it
});
}
if (!ini_get("open_basedir")) {
curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
}
if (defined('_HTTP_PROXY')) {
curl_setopt($ch, CURLOPT_PROXY, _HTTP_PROXY);
}
if ($post_query) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_query);
}
if ($login && $pass)
curl_setopt($ch, CURLOPT_USERPWD, "$login:$pass");
$ret = @curl_exec($ch);
$headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = explode("\r\n", substr($ret, 0, $headers_length));
$contents = substr($ret, $headers_length);
foreach ($headers as $header) {
if (strstr($header, ": ") !== false) {
list ($key, $value) = explode(": ", $header);
if (strtolower($key) == "last-modified") {
$fetch_last_modified = $value;
}
}
if (substr(strtolower($header), 0, 7) == 'http/1.') {
$fetch_last_error_code = (int) substr($header, 9, 3);
$fetch_last_error = $header;
}
}
if (curl_errno($ch) === 23 || curl_errno($ch) === 61) {
curl_setopt($ch, CURLOPT_ENCODING, 'none');
$contents = @curl_exec($ch);
}
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$fetch_effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
if (!UrlHelper::validate($fetch_effective_url, true)) {
$fetch_last_error = "URL received after redirection failed extended validation.";
return false;
}
$fetch_effective_ip_addr = gethostbyname(parse_url($fetch_effective_url, PHP_URL_HOST));
if (!$fetch_effective_ip_addr || strpos($fetch_effective_ip_addr, "127.") === 0) {
$fetch_last_error = "URL hostname received after redirection failed to resolve or resolved to a loopback address ($fetch_effective_ip_addr)";
return false;
}
$fetch_last_error_code = $http_code;
if ($http_code != 200 || $type && strpos($fetch_last_content_type, "$type") === false) {
if (curl_errno($ch) != 0) {
$fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
}
$fetch_last_error_content = $contents;
curl_close($ch);
return false;
}
if (!$contents) {
$fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
curl_close($ch);
return false;
}
curl_close($ch);
$is_gzipped = RSSUtils::is_gzipped($contents);
if ($is_gzipped) {
$tmp = @gzdecode($contents);
if ($tmp) $contents = $tmp;
}
return $contents;
} else {
$fetch_curl_used = false;
if ($login && $pass){
$url_parts = array();
preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
$pass = urlencode($pass);
if ($url_parts[1] && $url_parts[2]) {
$url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
}
}
// TODO: should this support POST requests or not? idk
$context_options = array(
'http' => array(
'header' => array(
'Connection: close'
),
'method' => 'GET',
'ignore_errors' => true,
'timeout' => $timeout ? $timeout : FILE_FETCH_TIMEOUT,
'protocol_version'=> 1.1)
);
if (!$post_query && $last_modified)
array_push($context_options['http']['header'], "If-Modified-Since: $last_modified");
if ($http_accept)
array_push($context_options['http']['header'], "Accept: $http_accept");
if ($http_referrer)
array_push($context_options['http']['header'], "Referer: $http_referrer");
if (defined('_HTTP_PROXY')) {
$context_options['http']['request_fulluri'] = true;
$context_options['http']['proxy'] = _HTTP_PROXY;
}
$context = stream_context_create($context_options);
$old_error = error_get_last();
$fetch_effective_url = resolve_redirects($url, $timeout ? $timeout : FILE_FETCH_CONNECT_TIMEOUT);
if (!UrlHelper::validate($fetch_effective_url, true)) {
$fetch_last_error = "URL received after redirection failed extended validation.";
return false;
}
$fetch_effective_ip_addr = gethostbyname(parse_url($fetch_effective_url, PHP_URL_HOST));
if (!$fetch_effective_ip_addr || strpos($fetch_effective_ip_addr, "127.") === 0) {
$fetch_last_error = "URL hostname received after redirection failed to resolve or resolved to a loopback address ($fetch_effective_ip_addr)";
return false;
}
$data = @file_get_contents($url, false, $context);
if (isset($http_response_header) && is_array($http_response_header)) {
foreach ($http_response_header as $header) {
if (strstr($header, ": ") !== false) {
list ($key, $value) = explode(": ", $header);
$key = strtolower($key);
if ($key == 'content-type') {
$fetch_last_content_type = $value;
// don't abort here b/c there might be more than one
// e.g. if we were being redirected -- last one is the right one
} else if ($key == 'last-modified') {
$fetch_last_modified = $value;
} else if ($key == 'location') {
$fetch_effective_url = $value;
}
}
if (substr(strtolower($header), 0, 7) == 'http/1.') {
$fetch_last_error_code = (int) substr($header, 9, 3);
$fetch_last_error = $header;
}
}
}
if ($fetch_last_error_code != 200) {
$error = error_get_last();
if ($error['message'] != $old_error['message']) {
$fetch_last_error .= "; " . $error["message"];
}
$fetch_last_error_content = $data;
return false;
}
$is_gzipped = RSSUtils::is_gzipped($data);
if ($is_gzipped) {
$tmp = @gzdecode($data);
if ($tmp) $data = $tmp;
}
return $data;
}
}
}

141
classes/userhelper.php Normal file
View File

@ -0,0 +1,141 @@
<?php
class UserHelper {
static function authenticate($login, $password, $check_only = false, $service = false) {
if (!SINGLE_USER_MODE) {
$user_id = false;
$auth_module = false;
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_AUTH_USER) as $plugin) {
$user_id = (int) $plugin->authenticate($login, $password, $service);
if ($user_id) {
$auth_module = strtolower(get_class($plugin));
break;
}
}
if ($user_id && !$check_only) {
session_start();
session_regenerate_id(true);
$_SESSION["uid"] = $user_id;
$_SESSION["auth_module"] = $auth_module;
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
WHERE id = ?");
$sth->execute([$user_id]);
$row = $sth->fetch();
$_SESSION["name"] = $row["login"];
$_SESSION["access_level"] = $row["access_level"];
$_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
$usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
$usth->execute([$user_id]);
$_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
$_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
$_SESSION["pwd_hash"] = $row["pwd_hash"];
Pref_Prefs::initialize_user_prefs($_SESSION["uid"]);
return true;
}
return false;
} else {
$_SESSION["uid"] = 1;
$_SESSION["name"] = "admin";
$_SESSION["access_level"] = 10;
$_SESSION["hide_hello"] = true;
$_SESSION["hide_logout"] = true;
$_SESSION["auth_module"] = false;
if (!$_SESSION["csrf_token"])
$_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
$_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
Pref_Prefs::initialize_user_prefs($_SESSION["uid"]);
return true;
}
}
static function load_user_plugins($owner_uid, $pluginhost = false) {
if (!$pluginhost) $pluginhost = PluginHost::getInstance();
if ($owner_uid && SCHEMA_VERSION >= 100 && !$_SESSION["safe_mode"]) {
$plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
$pluginhost->load($plugins, PluginHost::KIND_USER, $owner_uid);
if (get_schema_version() > 100) {
$pluginhost->load_data();
}
}
}
static function login_sequence() {
$pdo = Db::pdo();
if (SINGLE_USER_MODE) {
@session_start();
UserHelper::authenticate("admin", null);
startup_gettext();
UserHelper::load_user_plugins($_SESSION["uid"]);
} else {
if (!validate_session()) $_SESSION["uid"] = false;
if (!$_SESSION["uid"]) {
if (AUTH_AUTO_LOGIN && UserHelper::authenticate(null, null)) {
$_SESSION["ref_schema_version"] = get_schema_version(true);
} else {
UserHelper::authenticate(null, null, true);
}
if (!$_SESSION["uid"]) {
Pref_Users::logout_user();
Handler_Public::render_login_form();
exit;
}
} else {
/* bump login timestamp */
$sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
$sth->execute([$_SESSION['uid']]);
$_SESSION["last_login_update"] = time();
}
if ($_SESSION["uid"]) {
startup_gettext();
UserHelper::load_user_plugins($_SESSION["uid"]);
}
}
}
static function print_user_stylesheet() {
$value = get_pref('USER_STYLESHEET');
if ($value) {
print "<style type='text/css' id='user_css_style'>";
print str_replace("<br/>", "\n", $value);
print "</style>";
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,7 @@
if (!init_plugins()) return;
login_sequence();
UserHelper::login_sequence();
header('Content-Type: text/html; charset=utf-8');
@ -51,7 +51,7 @@
const __csrf_token = "<?php echo $_SESSION["csrf_token"]; ?>";
</script>
<?php print_user_stylesheet() ?>
<?php UserHelper::print_user_stylesheet() ?>
<style type="text/css">
<?php

View File

@ -13,7 +13,7 @@ class Af_Comics_Cad extends Af_ComicFilter {
$doc = new DOMDocument();
$res = fetch_file_contents($article["link"], false, false, false,
$res = UrlHelper::fetch($article["link"], false, false, false,
false, false, 0,
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0");

View File

@ -11,7 +11,7 @@ class Af_Comics_ComicClass extends Af_ComicFilter {
// lol at people who block clients by user agent
// oh noes my ad revenue Q_Q
$res = fetch_file_contents($article["link"], false, false, false,
$res = UrlHelper::fetch($article["link"], false, false, false,
false, false, 0,
"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)");

View File

@ -18,7 +18,7 @@ class Af_Comics_ComicPress extends Af_ComicFilter {
// lol at people who block clients by user agent
// oh noes my ad revenue Q_Q
$res = fetch_file_contents(["url" => $article["link"],
$res = UrlHelper::fetch(["url" => $article["link"],
"useragent" => "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)"]);
$doc = new DOMDocument();
@ -37,7 +37,7 @@ class Af_Comics_ComicPress extends Af_ComicFilter {
if ($webtoon_link) {
$res = fetch_file_contents(["url" => $webtoon_link->getAttribute("href"),
$res = UrlHelper::fetch(["url" => $webtoon_link->getAttribute("href"),
"useragent" => "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)"]);
if (@$doc->loadHTML($res)) {

View File

@ -9,7 +9,7 @@ class Af_Comics_DarkLegacy extends Af_ComicFilter {
if (strpos($article["guid"], "darklegacycomics.com") !== false) {
$res = fetch_file_contents($article["link"], false, false, false,
$res = UrlHelper::fetch($article["link"], false, false, false,
false, false, 0,
"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)");

View File

@ -10,7 +10,7 @@ class Af_Comics_Dilbert extends Af_ComicFilter {
if (strpos($article["link"], "dilbert.com") !== false ||
strpos($article["link"], "/DilbertDailyStrip") !== false) {
$res = fetch_file_contents($article["link"], false, false, false,
$res = UrlHelper::fetch($article["link"], false, false, false,
false, false, 0,
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0");

View File

@ -11,7 +11,7 @@ class Af_Comics_Explosm extends Af_ComicFilter {
$doc = new DOMDocument();
if (@$doc->loadHTML(fetch_file_contents($article["link"]))) {
if (@$doc->loadHTML(UrlHelper::fetch($article["link"]))) {
$xpath = new DOMXPath($doc);
$basenode = $xpath->query('(//img[@id="main-comic"])')->item(0);

View File

@ -29,7 +29,7 @@ class Af_Comics_Gocomics extends Af_ComicFilter {
$article_link = $site_url . date('/Y/m/d');
$body = fetch_file_contents(array('url' => $article_link, 'type' => 'text/html', 'followlocation' => false));
$body = UrlHelper::fetch(array('url' => $article_link, 'type' => 'text/html', 'followlocation' => false));
$feed_title = htmlspecialchars($comic[1]);
$site_url = htmlspecialchars($site_url);

View File

@ -37,7 +37,7 @@ class Af_Comics_Gocomics_FarSide extends Af_ComicFilter {
$tpl->setVariable('FEED_URL', htmlspecialchars($url), true);
$tpl->setVariable('SELF_URL', htmlspecialchars($url), true);
$body = fetch_file_contents(['url' => $article_link, 'type' => 'text/html', 'followlocation' => false]);
$body = UrlHelper::fetch(['url' => $article_link, 'type' => 'text/html', 'followlocation' => false]);
if ($body) {
$doc = new DOMDocument();

View File

@ -10,7 +10,7 @@ class Af_Comics_Pa extends Af_ComicFilter {
$doc = new DOMDocument();
if ($doc->loadHTML(fetch_file_contents($article["link"]))) {
if ($doc->loadHTML(UrlHelper::fetch($article["link"]))) {
$xpath = new DOMXPath($doc);
$basenode = $xpath->query('(//div[@id="comicFrame"])')->item(0);
@ -25,7 +25,7 @@ class Af_Comics_Pa extends Af_ComicFilter {
if (strpos($article["link"], "penny-arcade.com") !== false && strpos($article["title"], "News Post:") !== false) {
$doc = new DOMDocument();
if ($doc->loadHTML(fetch_file_contents($article["link"]))) {
if ($doc->loadHTML(UrlHelper::fetch($article["link"]))) {
$xpath = new DOMXPath($doc);
$entries = $xpath->query('(//div[@class="post"])');

View File

@ -8,7 +8,7 @@ class Af_Comics_Pvp extends Af_ComicFilter {
function process(&$article) {
if (strpos($article["guid"], "pvponline.com") !== false) {
$res = fetch_file_contents($article["link"], false, false, false,
$res = UrlHelper::fetch($article["link"], false, false, false,
false, false, 0,
"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)");

View File

@ -8,7 +8,7 @@ class Af_Comics_Tfd extends Af_ComicFilter {
function process(&$article) {
if (strpos($article["link"], "toothpastefordinner.com") !== false ||
strpos($article["link"], "marriedtothesea.com") !== false) {
$res = fetch_file_contents($article["link"], false, false, false,
$res = UrlHelper::fetch($article["link"], false, false, false,
false, false, 0,
"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)");
@ -16,7 +16,7 @@ class Af_Comics_Tfd extends Af_ComicFilter {
$doc = new DOMDocument();
if (@$doc->loadHTML(fetch_file_contents($article["link"]))) {
if (@$doc->loadHTML(UrlHelper::fetch($article["link"]))) {
$xpath = new DOMXPath($doc);
$basenode = $xpath->query('//img[contains(@src, ".gif")]')->item(0);

View File

@ -11,7 +11,7 @@ class Af_Comics_Twp extends Af_ComicFilter {
$doc = new DOMDocument();
if (@$doc->loadHTML(fetch_file_contents($article["link"]))) {
if (@$doc->loadHTML(UrlHelper::fetch($article["link"]))) {
$xpath = new DOMXpath($doc);
$basenode = $xpath->query("//td/center/img")->item(0);

View File

@ -8,7 +8,7 @@ class Af_Comics_Whomp extends Af_ComicFilter {
function process(&$article) {
if (strpos($article["guid"], "whompcomic.com") !== false) {
$res = fetch_file_contents($article["link"], false, false, false,
$res = UrlHelper::fetch($article["link"], false, false, false,
false, false, 0,
"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)");

View File

@ -48,7 +48,7 @@ class Af_Proxy_Http extends Plugin {
}
public function imgproxy() {
$url = validate_url(clean($_REQUEST["url"]));
$url = UrlHelper::validate(clean($_REQUEST["url"]));
// called without user context, let's just redirect to original URL
if (!$_SESSION["uid"] || $_REQUEST['af_proxy_http_token'] != $_SESSION['af_proxy_http_token']) {
@ -62,7 +62,7 @@ class Af_Proxy_Http extends Plugin {
header("Location: " . $this->cache->getUrl($local_filename));
return;
} else {
$data = fetch_file_contents(["url" => $url, "max_size" => MAX_CACHE_FILE_SIZE]);
$data = UrlHelper::fetch(["url" => $url, "max_size" => MAX_CACHE_FILE_SIZE]);
if ($data) {
if ($this->cache->put($local_filename, $data)) {
@ -125,7 +125,7 @@ class Af_Proxy_Http extends Plugin {
foreach (explode(" " , $this->ssl_known_whitelist) as $host) {
if (substr(strtolower($parts['host']), -strlen($host)) === strtolower($host)) {
$parts['scheme'] = 'https';
$url = build_url($parts);
$url = UrlHelper::build_url($parts);
if ($all_remote && $is_remote) {
break;
} else {

View File

@ -176,7 +176,7 @@ class Af_Readability extends Plugin {
global $fetch_effective_url;
$tmp = fetch_file_contents([
$tmp = UrlHelper::fetch([
"url" => $url,
"http_accept" => "text/*",
"type" => "text/html"]);
@ -235,7 +235,7 @@ class Af_Readability extends Plugin {
$extracted_content = $this->extract_content($article["link"]);
# let's see if there's anything of value in there
$content_test = trim(strip_tags(sanitize($extracted_content)));
$content_test = trim(strip_tags(Sanitizer::sanitize($extracted_content)));
if ($content_test) {
$article["content"] = $extracted_content;
@ -264,7 +264,7 @@ class Af_Readability extends Plugin {
$extracted_content = $this->extract_content($link);
# let's see if there's anything of value in there
$content_test = trim(strip_tags(sanitize($extracted_content)));
$content_test = trim(strip_tags(Sanitizer::sanitize($extracted_content)));
if ($content_test) {
return $extracted_content;
@ -303,7 +303,7 @@ class Af_Readability extends Plugin {
$ret = [];
if ($row = $sth->fetch()) {
$ret["content"] = sanitize($this->extract_content($row["link"]));
$ret["content"] = Sanitizer::sanitize($this->extract_content($row["link"]));
}
print json_encode($ret);

View File

@ -103,7 +103,7 @@ class Af_RedditImgur extends Plugin {
if (!$found && preg_match("/^https?:\/\/twitter.com\/(.*?)\/status\/(.*)/", $entry->getAttribute("href"), $matches)) {
Debug::log("handling as twitter: " . $matches[1] . " " . $matches[2], Debug::$LOG_VERBOSE);
$oembed_result = fetch_file_contents("https://publish.twitter.com/oembed?url=" . urlencode($entry->getAttribute("href")));
$oembed_result = UrlHelper::fetch("https://publish.twitter.com/oembed?url=" . urlencode($entry->getAttribute("href")));
if ($oembed_result) {
$oembed_result = json_decode($oembed_result, true);
@ -165,7 +165,7 @@ class Af_RedditImgur extends Plugin {
$source_stream = false;
if ($source_article_url) {
$j = json_decode(fetch_file_contents($source_article_url.".json"), true);
$j = json_decode(UrlHelper::fetch($source_article_url.".json"), true);
if ($j) {
foreach ($j as $listing) {
@ -195,7 +195,7 @@ class Af_RedditImgur extends Plugin {
Debug::log("Handling as Streamable", Debug::$LOG_VERBOSE);
$tmp = fetch_file_contents($entry->getAttribute("href"));
$tmp = UrlHelper::fetch($entry->getAttribute("href"));
if ($tmp) {
$tmpdoc = new DOMDocument();
@ -285,7 +285,7 @@ class Af_RedditImgur extends Plugin {
Debug::log("handling as imgur page/whatever", Debug::$LOG_VERBOSE);
$content = fetch_file_contents(["url" => $entry->getAttribute("href"),
$content = UrlHelper::fetch(["url" => $entry->getAttribute("href"),
"http_accept" => "text/*"]);
if ($content) {
@ -331,7 +331,7 @@ class Af_RedditImgur extends Plugin {
if (!$found) {
Debug::log("looking for meta og:image", Debug::$LOG_VERBOSE);
$content = fetch_file_contents(["url" => $entry->getAttribute("href"),
$content = UrlHelper::fetch(["url" => $entry->getAttribute("href"),
"http_accept" => "text/*"]);
if ($content) {

View File

@ -40,16 +40,16 @@ class Cache_Starred_Images extends Plugin {
Debug::log("caching media of starred articles for user " . $this->host->get_owner_uid() . "...");
$sth = $this->pdo->prepare("SELECT content, ttrss_entries.title,
$sth = $this->pdo->prepare("SELECT content, ttrss_entries.title,
ttrss_user_entries.owner_uid, link, site_url, ttrss_entries.id, plugin_data
FROM ttrss_entries, ttrss_user_entries LEFT JOIN ttrss_feeds ON
(ttrss_user_entries.feed_id = ttrss_feeds.id)
WHERE ref_id = ttrss_entries.id AND
marked = true AND
site_url != '' AND
site_url != '' AND
ttrss_user_entries.owner_uid = ? AND
plugin_data NOT LIKE '%starred_cache_images%'
ORDER BY ".sql_random_function()." LIMIT 100");
ORDER BY ".Db::sql_random_function()." LIMIT 100");
if ($sth->execute([$this->host->get_owner_uid()])) {
@ -139,7 +139,7 @@ class Cache_Starred_Images extends Plugin {
if (!$this->cache->exists($local_filename)) {
Debug::log("cache_images: downloading: $url to $local_filename", Debug::$LOG_VERBOSE);
$data = fetch_file_contents(["url" => $url, "max_size" => MAX_CACHE_FILE_SIZE]);
$data = UrlHelper::fetch(["url" => $url, "max_size" => MAX_CACHE_FILE_SIZE]);
if ($data)
return $this->cache->put($local_filename, $data);;

View File

@ -23,7 +23,7 @@ class No_Iframes extends Plugin {
$entries = $xpath->query('//iframe');
foreach ($entries as $entry) {
if (!iframe_whitelisted($entry))
if (!Sanitizer::iframe_whitelisted($entry))
$entry->parentNode->removeChild($entry);
}
@ -34,4 +34,4 @@ class No_Iframes extends Plugin {
return 2;
}
}
}

View File

@ -21,7 +21,7 @@
if (!init_plugins()) return;
login_sequence();
UserHelper::login_sequence();
header('Content-Type: text/html; charset=utf-8');
?>
@ -43,7 +43,7 @@
const __csrf_token = "<?php echo $_SESSION["csrf_token"]; ?>";
</script>
<?php print_user_stylesheet() ?>
<?php UserHelper::print_user_stylesheet() ?>
<link rel="shortcut icon" type="image/png" href="images/favicon.png"/>
<link rel="icon" type="image/png" sizes="72x72" href="images/favicon-72px.png" />

View File

@ -288,7 +288,7 @@
$new_uid = db_fetch_result($result, 0, "id");
initialize_user( $new_uid);
Pref_Users::initialize_user($new_uid);
$reg_text = "Hi!\n".
"\n".

View File

@ -14,6 +14,19 @@
require_once "db.php";
require_once "db-prefs.php";
function make_stampfile($filename) {
$fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
if (flock($fp, LOCK_EX | LOCK_NB)) {
fwrite($fp, time() . "\n");
flock($fp, LOCK_UN);
fclose($fp);
return true;
} else {
return false;
}
}
function cleanup_tags($days = 14, $limit = 1000) {
$days = (int) $days;