diff options
author | Andrew Dolgov <[email protected]> | 2020-09-22 09:04:33 +0300 |
---|---|---|
committer | Andrew Dolgov <[email protected]> | 2020-09-22 09:04:33 +0300 |
commit | 74568df4ff7b7788991636f6fb2ed62012f85c3b (patch) | |
tree | 673bcb01157b38e4b38f8f4c8227012e8a750e18 /classes | |
parent | d04ac399ff284e9747e3fb55e87d05e0a5b8d85f (diff) |
remove a lot of stuff from global context (functions.php), add a few helper classes instead
Diffstat (limited to 'classes')
-rwxr-xr-x | classes/api.php | 10 | ||||
-rw-r--r-- | classes/backend.php | 4 | ||||
-rwxr-xr-x | classes/db.php | 9 | ||||
-rw-r--r-- | classes/diskcache.php | 54 | ||||
-rwxr-xr-x | classes/feeds.php | 10 | ||||
-rwxr-xr-x | classes/handler/public.php | 32 | ||||
-rwxr-xr-x | classes/pref/feeds.php | 2 | ||||
-rw-r--r-- | classes/pref/prefs.php | 59 | ||||
-rw-r--r-- | classes/pref/users.php | 23 | ||||
-rwxr-xr-x | classes/rpc.php | 300 | ||||
-rwxr-xr-x | classes/rssutils.php | 16 | ||||
-rw-r--r-- | classes/sanitizer.php | 217 | ||||
-rw-r--r-- | classes/urlhelper.php | 474 | ||||
-rw-r--r-- | classes/userhelper.php | 141 |
14 files changed, 1306 insertions, 45 deletions
diff --git a/classes/api.php b/classes/api.php index 7b0c58a98..ce046e66a 100755 --- a/classes/api.php +++ b/classes/api.php @@ -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"]); diff --git a/classes/backend.php b/classes/backend.php index 27abfbf5f..16c20660a 100644 --- a/classes/backend.php +++ b/classes/backend.php @@ -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) { diff --git a/classes/db.php b/classes/db.php index ac493f6d6..70b97d0ee 100755 --- a/classes/db.php +++ b/classes/db.php @@ -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()"; + } + } + } diff --git a/classes/diskcache.php b/classes/diskcache.php index da7722bf0..24e45ea8b 100644 --- a/classes/diskcache.php +++ b/classes/diskcache.php @@ -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; + } + } } diff --git a/classes/feeds.php b/classes/feeds.php index 4714e4c9a..b428252d8 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -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 = []; diff --git a/classes/handler/public.php b/classes/handler/public.php index 822ad1fd9..9671b18b2 100755 --- a/classes/handler/public.php +++ b/classes/handler/public.php @@ -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; + } + } ?> diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php index a6a543b76..4ff6ac9c2 100755 --- a/classes/pref/feeds.php +++ b/classes/pref/feeds.php @@ -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(); diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php index ac2684683..f825454dd 100644 --- a/classes/pref/prefs.php +++ b/classes/pref/prefs.php @@ -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(); + + } + } diff --git a/classes/pref/users.php b/classes/pref/users.php index 851d4fa9e..aeabc4502 100644 --- a/classes/pref/users.php +++ b/classes/pref/users.php @@ -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(); + } + } diff --git a/classes/rpc.php b/classes/rpc.php index 55651cc27..a7123566c 100755 --- a/classes/rpc.php +++ b/classes/rpc.php @@ -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); + } + } diff --git a/classes/rssutils.php b/classes/rssutils.php index 7cfbca6d3..14a0a18a7 100755 --- a/classes/rssutils.php +++ b/classes/rssutils.php @@ -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)) { diff --git a/classes/sanitizer.php b/classes/sanitizer.php new file mode 100644 index 000000000..8e008ef85 --- /dev/null +++ b/classes/sanitizer.php @@ -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; + } + } + +} diff --git a/classes/urlhelper.php b/classes/urlhelper.php new file mode 100644 index 000000000..c57d0b9d9 --- /dev/null +++ b/classes/urlhelper.php @@ -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; + } + } + +} diff --git a/classes/userhelper.php b/classes/userhelper.php new file mode 100644 index 000000000..2ae1f7b83 --- /dev/null +++ b/classes/userhelper.php @@ -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>"; + } + + } + +} |