1
0
mirror of https://git.tt-rss.org/git/tt-rss.git synced 2025-12-25 16:01:28 +00:00

wip separate handlers

This commit is contained in:
Andrew Dolgov
2021-03-02 15:16:38 +03:00
parent d6629ed188
commit 9ad4cbeeca
10 changed files with 2005 additions and 1996 deletions

884
classes/handler/api.php Normal file
View File

@@ -0,0 +1,884 @@
<?php
class Handler_API extends Handler {
const API_LEVEL = 15;
const STATUS_OK = 0;
const STATUS_ERR = 1;
const E_API_DISABLED = "API_DISABLED";
const E_NOT_LOGGED_IN = "NOT_LOGGED_IN";
const E_LOGIN_ERROR = "LOGIN_ERROR";
const E_INCORRECT_USAGE = "INCORRECT_USAGE";
const E_UNKNOWN_METHOD = "UNKNOWN_METHOD";
const E_OPERATION_FAILED = "E_OPERATION_FAILED";
private $seq;
private static function _param_to_bool($p) {
return $p && ($p !== "f" && $p !== "false");
}
private function _wrap($status, $reply) {
print json_encode([
"seq" => $this->seq,
"status" => $status,
"content" => $reply
]);
}
function before($method) {
if (parent::before($method)) {
header("Content-Type: text/json");
if (empty($_SESSION["uid"]) && $method != "login" && $method != "isloggedin") {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_NOT_LOGGED_IN));
return false;
}
if (!empty($_SESSION["uid"]) && $method != "logout" && !get_pref(Prefs::ENABLE_API_ACCESS)) {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_API_DISABLED));
return false;
}
$this->seq = (int) clean($_REQUEST['seq'] ?? 0);
return true;
}
return false;
}
function getVersion() {
$rv = array("version" => Config::get_version());
$this->_wrap(self::STATUS_OK, $rv);
}
function getApiLevel() {
$rv = array("level" => self::API_LEVEL);
$this->_wrap(self::STATUS_OK, $rv);
}
function login() {
if (session_status() == PHP_SESSION_ACTIVE) {
session_destroy();
}
session_start();
$login = clean($_REQUEST["user"]);
$password = clean($_REQUEST["password"]);
if (Config::get(Config::SINGLE_USER_MODE)) $login = "admin";
if ($uid = UserHelper::find_user_by_login($login)) {
if (get_pref(Prefs::ENABLE_API_ACCESS, $uid)) {
if (UserHelper::authenticate($login, $password, false, Auth_Base::AUTH_SERVICE_API)) {
$this->_wrap(self::STATUS_OK, array("session_id" => session_id(),
"api_level" => self::API_LEVEL));
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
}
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_API_DISABLED));
}
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
return;
}
}
function logout() {
UserHelper::logout();
$this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
function isLoggedIn() {
$this->_wrap(self::STATUS_OK, array("status" => $_SESSION["uid"] != ''));
}
function getUnread() {
$feed_id = clean($_REQUEST["feed_id"]);
$is_cat = clean($_REQUEST["is_cat"]);
if ($feed_id) {
$this->_wrap(self::STATUS_OK, array("unread" => getFeedUnread($feed_id, $is_cat)));
} else {
$this->_wrap(self::STATUS_OK, array("unread" => Feeds::_get_global_unread()));
}
}
/* Method added for ttrss-reader for Android */
function getCounters() {
$this->_wrap(self::STATUS_OK, Counters::get_all());
}
function getFeeds() {
$cat_id = clean($_REQUEST["cat_id"]);
$unread_only = self::_param_to_bool(clean($_REQUEST["unread_only"] ?? 0));
$limit = (int) clean($_REQUEST["limit"] ?? 0);
$offset = (int) clean($_REQUEST["offset"] ?? 0);
$include_nested = self::_param_to_bool(clean($_REQUEST["include_nested"] ?? false));
$feeds = $this->_api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested);
$this->_wrap(self::STATUS_OK, $feeds);
}
function getCategories() {
$unread_only = self::_param_to_bool(clean($_REQUEST["unread_only"] ?? false));
$enable_nested = self::_param_to_bool(clean($_REQUEST["enable_nested"] ?? false));
$include_empty = self::_param_to_bool(clean($_REQUEST['include_empty'] ?? false));
// TODO do not return empty categories, return Uncategorized and standard virtual cats
if ($enable_nested)
$nested_qpart = "parent_cat IS NULL";
else
$nested_qpart = "true";
$sth = $this->pdo->prepare("SELECT
id, title, order_id, (SELECT COUNT(id) FROM
ttrss_feeds WHERE
ttrss_feed_categories.id IS NOT NULL AND cat_id = ttrss_feed_categories.id) AS num_feeds,
(SELECT COUNT(id) FROM
ttrss_feed_categories AS c2 WHERE
c2.parent_cat = ttrss_feed_categories.id) AS num_cats
FROM ttrss_feed_categories
WHERE $nested_qpart AND owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$cats = array();
while ($line = $sth->fetch()) {
if ($include_empty || $line["num_feeds"] > 0 || $line["num_cats"] > 0) {
$unread = getFeedUnread($line["id"], true);
if ($enable_nested)
$unread += Feeds::_get_cat_children_unread($line["id"]);
if ($unread || !$unread_only) {
array_push($cats, array("id" => (int) $line["id"],
"title" => $line["title"],
"unread" => (int) $unread,
"order_id" => (int) $line["order_id"],
));
}
}
}
foreach (array(-2,-1,0) as $cat_id) {
if ($include_empty || !$this->_is_cat_empty($cat_id)) {
$unread = getFeedUnread($cat_id, true);
if ($unread || !$unread_only) {
array_push($cats, array("id" => $cat_id,
"title" => Feeds::_get_cat_title($cat_id),
"unread" => (int) $unread));
}
}
}
$this->_wrap(self::STATUS_OK, $cats);
}
function getHeadlines() {
$feed_id = clean($_REQUEST["feed_id"]);
if ($feed_id !== "") {
if (is_numeric($feed_id)) $feed_id = (int) $feed_id;
$limit = (int)clean($_REQUEST["limit"]);
if (!$limit || $limit >= 200) $limit = 200;
$offset = (int)clean($_REQUEST["skip"]);
$filter = clean($_REQUEST["filter"] ?? "");
$is_cat = self::_param_to_bool(clean($_REQUEST["is_cat"] ?? false));
$show_excerpt = self::_param_to_bool(clean($_REQUEST["show_excerpt"] ?? false));
$show_content = self::_param_to_bool(clean($_REQUEST["show_content"]));
/* all_articles, unread, adaptive, marked, updated */
$view_mode = clean($_REQUEST["view_mode"] ?? null);
$include_attachments = self::_param_to_bool(clean($_REQUEST["include_attachments"] ?? false));
$since_id = (int)clean($_REQUEST["since_id"] ?? 0);
$include_nested = self::_param_to_bool(clean($_REQUEST["include_nested"] ?? false));
$sanitize_content = !isset($_REQUEST["sanitize"]) ||
self::_param_to_bool($_REQUEST["sanitize"]);
$force_update = self::_param_to_bool(clean($_REQUEST["force_update"] ?? false));
$has_sandbox = self::_param_to_bool(clean($_REQUEST["has_sandbox"] ?? false));
$excerpt_length = (int)clean($_REQUEST["excerpt_length"] ?? 0);
$check_first_id = (int)clean($_REQUEST["check_first_id"] ?? 0);
$include_header = self::_param_to_bool(clean($_REQUEST["include_header"] ?? false));
$_SESSION['hasSandbox'] = $has_sandbox;
list($override_order, $skip_first_id_check) = Feeds::_order_to_override_query(clean($_REQUEST["order_by"] ?? null));
/* do not rely on params below */
$search = clean($_REQUEST["search"] ?? "");
list($headlines, $headlines_header) = $this->_api_get_headlines($feed_id, $limit, $offset,
$filter, $is_cat, $show_excerpt, $show_content, $view_mode, $override_order,
$include_attachments, $since_id, $search,
$include_nested, $sanitize_content, $force_update, $excerpt_length, $check_first_id, $skip_first_id_check);
if ($include_header) {
$this->_wrap(self::STATUS_OK, array($headlines_header, $headlines));
} else {
$this->_wrap(self::STATUS_OK, $headlines);
}
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
}
}
function updateArticle() {
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
$mode = (int) clean($_REQUEST["mode"]);
$data = clean($_REQUEST["data"] ?? "");
$field_raw = (int)clean($_REQUEST["field"]);
$field = "";
$set_to = "";
$additional_fields = "";
switch ($field_raw) {
case 0:
$field = "marked";
$additional_fields = ",last_marked = NOW()";
break;
case 1:
$field = "published";
$additional_fields = ",last_published = NOW()";
break;
case 2:
$field = "unread";
$additional_fields = ",last_read = NOW()";
break;
case 3:
$field = "note";
};
switch ($mode) {
case 1:
$set_to = "true";
break;
case 0:
$set_to = "false";
break;
case 2:
$set_to = "NOT $field";
break;
}
if ($field == "note") $set_to = $this->pdo->quote($data);
if ($field && $set_to && count($article_ids) > 0) {
$article_qmarks = arr_qmarks($article_ids);
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
$field = $set_to $additional_fields
WHERE ref_id IN ($article_qmarks) AND owner_uid = ?");
$sth->execute(array_merge($article_ids, [$_SESSION['uid']]));
$num_updated = $sth->rowCount();
$this->_wrap(self::STATUS_OK, array("status" => "OK",
"updated" => $num_updated));
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
}
}
function getArticle() {
$article_ids = explode(",", clean($_REQUEST["article_id"]));
$sanitize_content = !isset($_REQUEST["sanitize"]) ||
self::_param_to_bool($_REQUEST["sanitize"]);
if (count($article_ids) > 0) {
$article_qmarks = arr_qmarks($article_ids);
$sth = $this->pdo->prepare("SELECT id,guid,title,link,content,feed_id,comments,int_id,
marked,unread,published,score,note,lang,
".SUBSTRING_FOR_DATE."(updated,1,16) as updated,
author,(SELECT title FROM ttrss_feeds WHERE id = feed_id) AS feed_title,
(SELECT site_url FROM ttrss_feeds WHERE id = feed_id) AS site_url,
(SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) AS hide_images
FROM ttrss_entries,ttrss_user_entries
WHERE id IN ($article_qmarks) AND ref_id = id AND owner_uid = ?");
$sth->execute(array_merge($article_ids, [$_SESSION['uid']]));
$articles = array();
while ($line = $sth->fetch()) {
$article = array(
"id" => $line["id"],
"guid" => $line["guid"],
"title" => $line["title"],
"link" => $line["link"],
"labels" => Article::_get_labels($line['id']),
"unread" => self::_param_to_bool($line["unread"]),
"marked" => self::_param_to_bool($line["marked"]),
"published" => self::_param_to_bool($line["published"]),
"comments" => $line["comments"],
"author" => $line["author"],
"updated" => (int) strtotime($line["updated"]),
"feed_id" => $line["feed_id"],
"attachments" => Article::_get_enclosures($line['id']),
"score" => (int)$line["score"],
"feed_title" => $line["feed_title"],
"note" => $line["note"],
"lang" => $line["lang"]
);
if ($sanitize_content) {
$article["content"] = Sanitizer::sanitize(
$line["content"],
self::_param_to_bool($line['hide_images']),
false, $line["site_url"], false, $line["id"]);
} else {
$article["content"] = $line["content"];
}
$hook_object = ["article" => &$article];
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_API,
function ($result) use (&$article) {
$article = $result;
},
$hook_object);
$article['content'] = DiskCache::rewrite_urls($article['content']);
array_push($articles, $article);
}
$this->_wrap(self::STATUS_OK, $articles);
// @phpstan-ignore-next-line
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
}
}
function getConfig() {
$config = [
"icons_dir" => Config::get(Config::ICONS_DIR),
"icons_url" => Config::get(Config::ICONS_URL)
];
$config["daemon_is_running"] = file_is_locked("update_daemon.lock");
$sth = $this->pdo->prepare("SELECT COUNT(*) AS cf FROM
ttrss_feeds WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
$config["num_feeds"] = $row["cf"];
$this->_wrap(self::STATUS_OK, $config);
}
function updateFeed() {
$feed_id = (int) clean($_REQUEST["feed_id"]);
if (!ini_get("open_basedir")) {
RSSUtils::update_rss_feed($feed_id);
}
$this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
function catchupFeed() {
$feed_id = clean($_REQUEST["feed_id"]);
$is_cat = clean($_REQUEST["is_cat"]);
@$mode = clean($_REQUEST["mode"]);
if (!in_array($mode, ["all", "1day", "1week", "2week"]))
$mode = "all";
Feeds::_catchup($feed_id, $is_cat, $_SESSION["uid"], $mode);
$this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
function getPref() {
$pref_name = clean($_REQUEST["pref_name"]);
$this->_wrap(self::STATUS_OK, array("value" => get_pref($pref_name)));
}
function getLabels() {
$article_id = (int)clean($_REQUEST['article_id']);
$rv = array();
$sth = $this->pdo->prepare("SELECT id, caption, fg_color, bg_color
FROM ttrss_labels2
WHERE owner_uid = ? ORDER BY caption");
$sth->execute([$_SESSION['uid']]);
if ($article_id)
$article_labels = Article::_get_labels($article_id);
else
$article_labels = array();
while ($line = $sth->fetch()) {
$checked = false;
foreach ($article_labels as $al) {
if (Labels::feed_to_label_id($al[0]) == $line['id']) {
$checked = true;
break;
}
}
array_push($rv, array(
"id" => (int)Labels::label_to_feed_id($line['id']),
"caption" => $line['caption'],
"fg_color" => $line['fg_color'],
"bg_color" => $line['bg_color'],
"checked" => $checked));
}
$this->_wrap(self::STATUS_OK, $rv);
}
function setArticleLabel() {
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
$label_id = (int) clean($_REQUEST['label_id']);
$assign = self::_param_to_bool(clean($_REQUEST['assign']));
$label = Labels::find_caption(Labels::feed_to_label_id($label_id), $_SESSION["uid"]);
$num_updated = 0;
if ($label) {
foreach ($article_ids as $id) {
if ($assign)
Labels::add_article($id, $label, $_SESSION["uid"]);
else
Labels::remove_article($id, $label, $_SESSION["uid"]);
++$num_updated;
}
}
$this->_wrap(self::STATUS_OK, array("status" => "OK",
"updated" => $num_updated));
}
function index($method) {
$plugin = PluginHost::getInstance()->get_api_method(strtolower($method));
if ($plugin && method_exists($plugin, $method)) {
$reply = $plugin->$method();
$this->_wrap($reply[0], $reply[1]);
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_UNKNOWN_METHOD, "method" => $method));
}
}
function shareToPublished() {
$title = strip_tags(clean($_REQUEST["title"]));
$url = strip_tags(clean($_REQUEST["url"]));
$content = strip_tags(clean($_REQUEST["content"]));
if (Article::_create_published_article($title, $url, $content, "", $_SESSION["uid"])) {
$this->_wrap(self::STATUS_OK, array("status" => 'OK'));
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_OPERATION_FAILED));
}
}
private static function _api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested = false) {
$feeds = array();
$pdo = Db::pdo();
$limit = (int) $limit;
$offset = (int) $offset;
$cat_id = (int) $cat_id;
/* Labels */
/* API only: -4 All feeds, including virtual feeds */
if ($cat_id == -4 || $cat_id == -2) {
$counters = Counters::get_labels();
foreach (array_values($counters) as $cv) {
$unread = $cv["counter"];
if ($unread || !$unread_only) {
$row = array(
"id" => (int) $cv["id"],
"title" => $cv["description"],
"unread" => $cv["counter"],
"cat_id" => -2,
);
array_push($feeds, $row);
}
}
}
/* Virtual feeds */
if ($cat_id == -4 || $cat_id == -1) {
foreach (array(-1, -2, -3, -4, -6, 0) as $i) {
$unread = getFeedUnread($i);
if ($unread || !$unread_only) {
$title = Feeds::_get_title($i);
$row = array(
"id" => $i,
"title" => $title,
"unread" => $unread,
"cat_id" => -1,
);
array_push($feeds, $row);
}
}
}
/* Child cats */
if ($include_nested && $cat_id) {
$sth = $pdo->prepare("SELECT
id, title, order_id FROM ttrss_feed_categories
WHERE parent_cat = ? AND owner_uid = ? ORDER BY order_id, title");
$sth->execute([$cat_id, $_SESSION['uid']]);
while ($line = $sth->fetch()) {
$unread = getFeedUnread($line["id"], true) +
Feeds::_get_cat_children_unread($line["id"]);
if ($unread || !$unread_only) {
$row = array(
"id" => (int) $line["id"],
"title" => $line["title"],
"unread" => $unread,
"is_cat" => true,
"order_id" => (int) $line["order_id"]
);
array_push($feeds, $row);
}
}
}
/* Real feeds */
if ($limit) {
$limit_qpart = "LIMIT $limit OFFSET $offset";
} else {
$limit_qpart = "";
}
/* API only: -3 All feeds, excluding virtual feeds (e.g. Labels and such) */
if ($cat_id == -4 || $cat_id == -3) {
$sth = $pdo->prepare("SELECT
id, feed_url, cat_id, title, order_id, ".
SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated
FROM ttrss_feeds WHERE owner_uid = ?
ORDER BY order_id, title " . $limit_qpart);
$sth->execute([$_SESSION['uid']]);
} else {
$sth = $pdo->prepare("SELECT
id, feed_url, cat_id, title, order_id, ".
SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated
FROM ttrss_feeds WHERE
(cat_id = :cat OR (:cat = 0 AND cat_id IS NULL))
AND owner_uid = :uid
ORDER BY order_id, title " . $limit_qpart);
$sth->execute([":uid" => $_SESSION['uid'], ":cat" => $cat_id]);
}
while ($line = $sth->fetch()) {
$unread = getFeedUnread($line["id"]);
$has_icon = Feeds::_has_icon($line['id']);
if ($unread || !$unread_only) {
$row = array(
"feed_url" => $line["feed_url"],
"title" => $line["title"],
"id" => (int)$line["id"],
"unread" => (int)$unread,
"has_icon" => $has_icon,
"cat_id" => (int)$line["cat_id"],
"last_updated" => (int) strtotime($line["last_updated"]),
"order_id" => (int) $line["order_id"],
);
array_push($feeds, $row);
}
}
return $feeds;
}
private static function _api_get_headlines($feed_id, $limit, $offset,
$filter, $is_cat, $show_excerpt, $show_content, $view_mode, $order,
$include_attachments, $since_id,
$search = "", $include_nested = false, $sanitize_content = true,
$force_update = false, $excerpt_length = 100, $check_first_id = false, $skip_first_id_check = false) {
$pdo = Db::pdo();
if ($force_update && $feed_id > 0 && is_numeric($feed_id)) {
// Update the feed if required with some basic flood control
$sth = $pdo->prepare(
"SELECT cache_images,".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated
FROM ttrss_feeds WHERE id = ?");
$sth->execute([$feed_id]);
if ($row = $sth->fetch()) {
$last_updated = strtotime($row["last_updated"]);
$cache_images = self::_param_to_bool($row["cache_images"]);
if (!$cache_images && time() - $last_updated > 120) {
RSSUtils::update_rss_feed($feed_id, true);
} else {
$sth = $pdo->prepare("UPDATE ttrss_feeds SET last_updated = '1970-01-01', last_update_started = '1970-01-01'
WHERE id = ?");
$sth->execute([$feed_id]);
}
}
}
$params = array(
"feed" => $feed_id,
"limit" => $limit,
"view_mode" => $view_mode,
"cat_view" => $is_cat,
"search" => $search,
"override_order" => $order,
"offset" => $offset,
"since_id" => $since_id,
"include_children" => $include_nested,
"check_first_id" => $check_first_id,
"skip_first_id_check" => $skip_first_id_check
);
$qfh_ret = Feeds::_get_headlines($params);
$result = $qfh_ret[0];
$feed_title = $qfh_ret[1];
$first_id = $qfh_ret[6];
$headlines = array();
$headlines_header = array(
'id' => $feed_id,
'first_id' => $first_id,
'is_cat' => $is_cat);
if (!is_numeric($result)) {
while ($line = $result->fetch()) {
$line["content_preview"] = truncate_string(strip_tags($line["content"]), $excerpt_length);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
function ($result) use (&$line) {
$line = $result;
},
$line, $excerpt_length);
$is_updated = ($line["last_read"] == "" &&
($line["unread"] != "t" && $line["unread"] != "1"));
$tags = explode(",", $line["tag_cache"]);
$label_cache = $line["label_cache"];
$labels = false;
if ($label_cache) {
$label_cache = json_decode($label_cache, true);
if ($label_cache) {
if (($label_cache["no-labels"] ?? 0) == 1)
$labels = [];
else
$labels = $label_cache;
}
}
if (!is_array($labels)) $labels = Article::_get_labels($line["id"]);
$headline_row = array(
"id" => (int)$line["id"],
"guid" => $line["guid"],
"unread" => self::_param_to_bool($line["unread"]),
"marked" => self::_param_to_bool($line["marked"]),
"published" => self::_param_to_bool($line["published"]),
"updated" => (int)strtotime($line["updated"]),
"is_updated" => $is_updated,
"title" => $line["title"],
"link" => $line["link"],
"feed_id" => $line["feed_id"] ? $line['feed_id'] : 0,
"tags" => $tags,
);
$enclosures = Article::_get_enclosures($line['id']);
if ($include_attachments)
$headline_row['attachments'] = $enclosures;
if ($show_excerpt)
$headline_row["excerpt"] = $line["content_preview"];
if ($show_content) {
if ($sanitize_content) {
$headline_row["content"] = Sanitizer::sanitize(
$line["content"],
self::_param_to_bool($line['hide_images']),
false, $line["site_url"], false, $line["id"]);
} else {
$headline_row["content"] = $line["content"];
}
}
// unify label output to ease parsing
if (($labels["no-labels"] ?? 0) == 1) $labels = [];
$headline_row["labels"] = $labels;
$headline_row["feed_title"] = isset($line["feed_title"]) ? $line["feed_title"] : $feed_title;
$headline_row["comments_count"] = (int)$line["num_comments"];
$headline_row["comments_link"] = $line["comments"];
$headline_row["always_display_attachments"] = self::_param_to_bool($line["always_display_enclosures"]);
$headline_row["author"] = $line["author"];
$headline_row["score"] = (int)$line["score"];
$headline_row["note"] = $line["note"];
$headline_row["lang"] = $line["lang"];
if ($show_content) {
$hook_object = ["headline" => &$headline_row];
list ($flavor_image, $flavor_stream, $flavor_kind) = Article::_get_image($enclosures,
$line["content"], // unsanitized
$line["site_url"]);
$headline_row["flavor_image"] = $flavor_image;
$headline_row["flavor_stream"] = $flavor_stream;
/* optional */
if ($flavor_kind)
$headline_row["flavor_kind"] = $flavor_kind;
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_API,
function ($result) use (&$headline_row) {
$headline_row = $result;
},
$hook_object);
$headline_row["content"] = DiskCache::rewrite_urls($headline_row['content']);
}
array_push($headlines, $headline_row);
}
} else if (is_numeric($result) && $result == -1) {
$headlines_header['first_id_changed'] = true;
}
return array($headlines, $headlines_header);
}
function unsubscribeFeed() {
$feed_id = (int) clean($_REQUEST["feed_id"]);
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE
id = ? AND owner_uid = ?");
$sth->execute([$feed_id, $_SESSION['uid']]);
if ($row = $sth->fetch()) {
Pref_Feeds::remove_feed($feed_id, $_SESSION["uid"]);
$this->_wrap(self::STATUS_OK, array("status" => "OK"));
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_OPERATION_FAILED));
}
}
function subscribeToFeed() {
$feed_url = clean($_REQUEST["feed_url"]);
$category_id = (int) clean($_REQUEST["category_id"]);
$login = clean($_REQUEST["login"]);
$password = clean($_REQUEST["password"]);
if ($feed_url) {
$rc = Feeds::_subscribe($feed_url, $category_id, $login, $password);
$this->_wrap(self::STATUS_OK, array("status" => $rc));
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
}
}
function getFeedTree() {
$include_empty = self::_param_to_bool(clean($_REQUEST['include_empty']));
$pf = new Pref_Feeds($_REQUEST);
$_REQUEST['mode'] = 2;
$_REQUEST['force_show_empty'] = $include_empty;
$this->_wrap(self::STATUS_OK,
array("categories" => $pf->_makefeedtree()));
}
// only works for labels or uncategorized for the time being
private function _is_cat_empty($id) {
if ($id == -2) {
$sth = $this->pdo->prepare("SELECT COUNT(id) AS count FROM ttrss_labels2
WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
return $row["count"] == 0;
} else if ($id == 0) {
$sth = $this->pdo->prepare("SELECT COUNT(id) AS count FROM ttrss_feeds
WHERE cat_id IS NULL AND owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
return $row["count"] == 0;
}
return false;
}
}

166
classes/handler/article.php Normal file
View File

@@ -0,0 +1,166 @@
<?php
class Handler_Article extends Handler_Protected {
function redirect() {
$article = ORM::for_table('ttrss_entries')
->table_alias('e')
->join('ttrss_user_entries', [ 'ref_id', '=', 'e.id'], 'ue')
->where('ue.owner_uid', $_SESSION['uid'])
->find_one((int)$_REQUEST['id']);
if ($article) {
$article_url = UrlHelper::validate($article->link);
if ($article_url) {
header("Location: $article_url");
return;
}
}
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
print "Article not found or has an empty URL.";
}
function printArticleTags() {
$id = (int) clean($_REQUEST['id'] ?? 0);
print json_encode(["id" => $id,
"tags" => Article::_get_tags($id)]);
}
function setScore() {
$ids = array_map("intval", clean($_REQUEST['ids'] ?? []));
$score = (int)clean($_REQUEST['score']);
$ids_qmarks = arr_qmarks($ids);
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
score = ? WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
$sth->execute(array_merge([$score], $ids, [$_SESSION['uid']]));
print json_encode(["id" => $ids, "score" => $score]);
}
function setArticleTags() {
$id = clean($_REQUEST["id"]);
//$tags_str = clean($_REQUEST["tags_str"]);
//$tags = array_unique(array_map('trim', explode(",", $tags_str)));
$tags = FeedItem_Common::normalize_categories(explode(",", clean($_REQUEST["tags_str"])));
$this->pdo->beginTransaction();
$sth = $this->pdo->prepare("SELECT int_id FROM ttrss_user_entries WHERE
ref_id = ? AND owner_uid = ? LIMIT 1");
$sth->execute([$id, $_SESSION['uid']]);
if ($row = $sth->fetch()) {
$tags_to_cache = array();
$int_id = $row['int_id'];
$dsth = $this->pdo->prepare("DELETE FROM ttrss_tags WHERE
post_int_id = ? AND owner_uid = ?");
$dsth->execute([$int_id, $_SESSION['uid']]);
$csth = $this->pdo->prepare("SELECT post_int_id FROM ttrss_tags
WHERE post_int_id = ? AND owner_uid = ? AND tag_name = ?");
$usth = $this->pdo->prepare("INSERT INTO ttrss_tags
(post_int_id, owner_uid, tag_name)
VALUES (?, ?, ?)");
foreach ($tags as $tag) {
$csth->execute([$int_id, $_SESSION['uid'], $tag]);
if (!$csth->fetch()) {
$usth->execute([$int_id, $_SESSION['uid'], $tag]);
}
array_push($tags_to_cache, $tag);
}
/* update tag cache */
$tags_str = join(",", $tags_to_cache);
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries
SET tag_cache = ? WHERE ref_id = ? AND owner_uid = ?");
$sth->execute([$tags_str, $id, $_SESSION['uid']]);
}
$this->pdo->commit();
// get latest tags from the database, original $tags is sometimes JSON-encoded as a hash ({}) - ???
print json_encode(["id" => (int)$id, "tags" => Article::_get_tags($id)]);
}
/*function completeTags() {
$search = clean($_REQUEST["search"]);
$sth = $this->pdo->prepare("SELECT DISTINCT tag_name FROM ttrss_tags
WHERE owner_uid = ? AND
tag_name LIKE ? ORDER BY tag_name
LIMIT 10");
$sth->execute([$_SESSION['uid'], "$search%"]);
print "<ul>";
while ($line = $sth->fetch()) {
print "<li>" . $line["tag_name"] . "</li>";
}
print "</ul>";
}*/
function assigntolabel() {
return $this->_label_ops(true);
}
function removefromlabel() {
return $this->_label_ops(false);
}
private function _label_ops($assign) {
$reply = array();
$ids = explode(",", clean($_REQUEST["ids"]));
$label_id = clean($_REQUEST["lid"]);
$label = Labels::find_caption($label_id, $_SESSION["uid"]);
$reply["labels-for"] = [];
if ($label) {
foreach ($ids as $id) {
if ($assign)
Labels::add_article($id, $label, $_SESSION["uid"]);
else
Labels::remove_article($id, $label, $_SESSION["uid"]);
array_push($reply["labels-for"],
["id" => (int)$id, "labels" => Article::_get_labels($id)]);
}
}
$reply["message"] = "UPDATE_COUNTERS";
print json_encode($reply);
}
function getmetadatabyid() {
$article = ORM::for_table('ttrss_entries')
->join('ttrss_user_entries', ['ref_id', '=', 'id'], 'ue')
->where('ue.owner_uid', $_SESSION['uid'])
->find_one((int)$_REQUEST['id']);
if ($article) {
echo json_encode(["link" => $article->link, "title" => $article->title]);
} else {
echo json_encode([]);
}
}
}

305
classes/handler/feeds.php Normal file
View File

@@ -0,0 +1,305 @@
<?php
require_once "colors.php";
class Handler_Feeds extends Handler_Protected {
function csrf_ignore($method) {
$csrf_ignored = array("index");
return array_search($method, $csrf_ignored) !== false;
}
function catchupAll() {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
last_read = NOW(), unread = false WHERE unread = true AND owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
function view() {
$reply = array();
$feed = $_REQUEST["feed"];
$method = $_REQUEST["m"] ?? "";
$view_mode = $_REQUEST["view_mode"] ?? "";
$limit = 30;
$cat_view = $_REQUEST["cat"] == "true";
$next_unread_feed = $_REQUEST["nuf"] ?? 0;
$offset = $_REQUEST["skip"] ?? 0;
$order_by = $_REQUEST["order_by"] ?? "";
$check_first_id = $_REQUEST["fid"] ?? 0;
if (is_numeric($feed)) $feed = (int) $feed;
/* Feed -5 is a special case: it is used to display auxiliary information
* when there's nothing to load - e.g. no stuff in fresh feed */
if ($feed == -5) {
print json_encode($this->_generate_dashboard_feed());
return;
}
$sth = false;
if ($feed < LABEL_BASE_INDEX) {
$label_feed = Labels::feed_to_label_id($feed);
$sth = $this->pdo->prepare("SELECT id FROM ttrss_labels2 WHERE
id = ? AND owner_uid = ?");
$sth->execute([$label_feed, $_SESSION['uid']]);
} else if (!$cat_view && is_numeric($feed) && $feed > 0) {
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE
id = ? AND owner_uid = ?");
$sth->execute([$feed, $_SESSION['uid']]);
} else if ($cat_view && is_numeric($feed) && $feed > 0) {
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories WHERE
id = ? AND owner_uid = ?");
$sth->execute([$feed, $_SESSION['uid']]);
}
if ($sth && !$sth->fetch()) {
print json_encode($this->_generate_error_feed(__("Feed not found.")));
return;
}
set_pref(Prefs::_DEFAULT_VIEW_MODE, $view_mode);
set_pref(Prefs::_DEFAULT_VIEW_ORDER_BY, $order_by);
/* bump login timestamp if needed */
if (time() - $_SESSION["last_login_update"] > 3600) {
$user = ORM::for_table('ttrss_users')->find_one($_SESSION["uid"]);
$user->last_login = Db::NOW();
$user->save();
$_SESSION["last_login_update"] = time();
}
if (!$cat_view && is_numeric($feed) && $feed > 0) {
$sth = $this->pdo->prepare("UPDATE ttrss_feeds SET last_viewed = NOW()
WHERE id = ? AND owner_uid = ?");
$sth->execute([$feed, $_SESSION['uid']]);
}
$reply['headlines'] = [];
list($override_order, $skip_first_id_check) = Feeds::_order_to_override_query($order_by);
$ret = Feeds::_format_headlines_list($feed, $method,
$view_mode, $limit, $cat_view, $offset,
$override_order, true, $check_first_id, $skip_first_id_check, $order_by);
$headlines_count = $ret[1];
$disable_cache = $ret[3];
$reply['headlines'] = $ret[4];
if (!$next_unread_feed)
$reply['headlines']['id'] = $feed;
else
$reply['headlines']['id'] = $next_unread_feed;
$reply['headlines']['is_cat'] = $cat_view;
$reply['headlines-info'] = ["count" => (int) $headlines_count,
"disable_cache" => (bool) $disable_cache];
// this is parsed by handleRpcJson() on first viewfeed() to set cdm expanded, etc
$reply['runtime-info'] = RPC::_make_runtime_info();
print json_encode($reply);
}
private function _generate_dashboard_feed() {
$reply = array();
$reply['headlines']['id'] = -5;
$reply['headlines']['is_cat'] = false;
$reply['headlines']['toolbar'] = '';
$reply['headlines']['content'] = "<div class='whiteBox'>".__('No feed selected.');
$reply['headlines']['content'] .= "<p><span class=\"text-muted\">";
$sth = $this->pdo->prepare("SELECT ".SUBSTRING_FOR_DATE."(MAX(last_updated), 1, 19) AS last_updated FROM ttrss_feeds
WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
$last_updated = TimeHelper::make_local_datetime($row["last_updated"], false);
$reply['headlines']['content'] .= sprintf(__("Feeds last updated at %s"), $last_updated);
$sth = $this->pdo->prepare("SELECT COUNT(id) AS num_errors
FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
$num_errors = $row["num_errors"];
if ($num_errors > 0) {
$reply['headlines']['content'] .= "<br/>";
$reply['headlines']['content'] .= "<a class=\"text-muted\" href=\"#\" onclick=\"CommonDialogs.showFeedsWithErrors()\">".
__('Some feeds have update errors (click for details)')."</a>";
}
$reply['headlines']['content'] .= "</span></p>";
$reply['headlines-info'] = array("count" => 0,
"unread" => 0,
"disable_cache" => true);
return $reply;
}
private function _generate_error_feed($error) {
$reply = array();
$reply['headlines']['id'] = -7;
$reply['headlines']['is_cat'] = false;
$reply['headlines']['toolbar'] = '';
$reply['headlines']['content'] = "<div class='whiteBox'>". $error . "</div>";
$reply['headlines-info'] = array("count" => 0,
"unread" => 0,
"disable_cache" => true);
return $reply;
}
function subscribeToFeed() {
print json_encode([
"cat_select" => \Controls\select_feeds_cats("cat")
]);
}
function search() {
print json_encode([
"show_language" => Config::get(Config::DB_TYPE) == "pgsql",
"show_syntax_help" => count(PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH)) == 0,
"all_languages" => Pref_Feeds::get_ts_languages(),
"default_language" => get_pref(Prefs::DEFAULT_SEARCH_LANGUAGE)
]);
}
function updatedebugger() {
header("Content-type: text/html");
$xdebug = isset($_REQUEST["xdebug"]) ? (int)$_REQUEST["xdebug"] : 1;
Debug::set_enabled(true);
Debug::set_loglevel($xdebug);
$feed_id = (int)$_REQUEST["feed_id"];
$do_update = ($_REQUEST["action"] ?? "") == "do_update";
$csrf_token = $_POST["csrf_token"];
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ? AND owner_uid = ?");
$sth->execute([$feed_id, $_SESSION['uid']]);
if (!$sth->fetch()) {
print "Access denied.";
return;
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Feed Debugger</title>
<style type='text/css'>
@media (prefers-color-scheme: dark) {
body {
background : #222;
}
}
body.css_loading * {
display : none;
}
</style>
<script>
dojoConfig = {
async: true,
cacheBust: "<?= get_scripts_timestamp(); ?>",
packages: [
{ name: "fox", location: "../../js" },
]
};
</script>
<?= javascript_tag("js/utility.js") ?>
<?= javascript_tag("js/common.js") ?>
<?= javascript_tag("lib/dojo/dojo.js") ?>
<?= javascript_tag("lib/dojo/tt-rss-layer.js") ?>
</head>
<body class="flat ttrss_utility feed_debugger css_loading">
<script type="text/javascript">
require(['dojo/parser', "dojo/ready", 'dijit/form/Button','dijit/form/CheckBox', 'fox/form/Select', 'dijit/form/Form',
'dijit/form/Select','dijit/form/TextBox','dijit/form/ValidationTextBox'],function(parser, ready){
ready(function() {
parser.parse();
});
});
</script>
<div class="container">
<h1>Feed Debugger: <?= "$feed_id: " . $this->_get_title($feed_id) ?></h1>
<div class="content">
<form method="post" action="" dojoType="dijit.form.Form">
<?= \Controls\hidden_tag("op", "feeds") ?>
<?= \Controls\hidden_tag("method", "updatedebugger") ?>
<?= \Controls\hidden_tag("csrf_token", $csrf_token) ?>
<?= \Controls\hidden_tag("action", "do_update") ?>
<?= \Controls\hidden_tag("feed_id", (string)$feed_id) ?>
<fieldset>
<label>
<?= \Controls\select_hash("xdebug", $xdebug,
[Debug::$LOG_VERBOSE => "LOG_VERBOSE", Debug::$LOG_EXTENDED => "LOG_EXTENDED"]);
?></label>
</fieldset>
<fieldset>
<label class="checkbox"><?= \Controls\checkbox_tag("force_refetch", isset($_REQUEST["force_refetch"])) ?> Force refetch</label>
</fieldset>
<fieldset class="narrow">
<label class="checkbox"><?= \Controls\checkbox_tag("force_rehash", isset($_REQUEST["force_rehash"])) ?> Force rehash</label>
</fieldset>
<?= \Controls\submit_tag("Continue") ?>
</form>
<hr>
<pre><?php
if ($do_update) {
RSSUtils::update_rss_feed($feed_id, true);
}
?></pre>
</div>
</div>
</body>
</html>
<?php
}
function add() {
$feed = clean($_REQUEST['feed']);
$cat = clean($_REQUEST['cat'] ?? '');
$need_auth = isset($_REQUEST['need_auth']);
$login = $need_auth ? clean($_REQUEST['login']) : '';
$pass = $need_auth ? clean($_REQUEST['pass']) : '';
$rc = Feeds::_subscribe($feed, $cat, $login, $pass);
print json_encode(array("result" => $rc));
}
}

661
classes/handler/opml.php Normal file
View File

@@ -0,0 +1,661 @@
<?php
class Handler_OPML extends Handler_Protected {
function csrf_ignore($method) {
$csrf_ignored = array("export", "import");
return array_search($method, $csrf_ignored) !== false;
}
function export() {
$output_name = sprintf("tt-rss_%s_%s.opml", $_SESSION["name"], date("Y-m-d"));
$include_settings = $_REQUEST["include_settings"] == "1";
$owner_uid = $_SESSION["uid"];
$rc = $this->opml_export($output_name, $owner_uid, false, $include_settings);
return $rc;
}
function import() {
$owner_uid = $_SESSION["uid"];
header('Content-Type: text/html; charset=utf-8');
print "<html>
<head>
".stylesheet_tag("themes/light.css")."
<title>".__("OPML Utility")."</title>
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>
</head>
<body class='claro ttrss_utility'>
<h1>".__('OPML Utility')."</h1><div class='content'>";
Feeds::_add_cat("Imported feeds", $owner_uid);
$this->opml_notice(__("Importing OPML..."));
$this->opml_import($owner_uid);
print "<br><form method=\"GET\" action=\"prefs.php\">
<input type=\"submit\" value=\"".__("Return to preferences")."\">
</form>";
print "</div></body></html>";
}
// Export
private function opml_export_category($owner_uid, $cat_id, $hide_private_feeds = false, $include_settings = true) {
$cat_id = (int) $cat_id;
if ($hide_private_feeds)
$hide_qpart = "(private IS false AND auth_login = '' AND auth_pass = '')";
else
$hide_qpart = "true";
$out = "";
$ttrss_specific_qpart = "";
if ($cat_id) {
$sth = $this->pdo->prepare("SELECT title,order_id
FROM ttrss_feed_categories WHERE id = ?
AND owner_uid = ?");
$sth->execute([$cat_id, $owner_uid]);
$row = $sth->fetch();
$cat_title = htmlspecialchars($row['title']);
if ($include_settings) {
$order_id = (int)$row["order_id"];
$ttrss_specific_qpart = "ttrssSortOrder=\"$order_id\"";
}
} else {
$cat_title = "";
}
if ($cat_title) $out .= "<outline text=\"$cat_title\" $ttrss_specific_qpart>\n";
$sth = $this->pdo->prepare("SELECT id,title
FROM ttrss_feed_categories WHERE
(parent_cat = :cat OR (:cat = 0 AND parent_cat IS NULL)) AND
owner_uid = :uid ORDER BY order_id, title");
$sth->execute([':cat' => $cat_id, ':uid' => $owner_uid]);
while ($line = $sth->fetch()) {
$out .= $this->opml_export_category($owner_uid, $line["id"], $hide_private_feeds, $include_settings);
}
$fsth = $this->pdo->prepare("select title, feed_url, site_url, update_interval, order_id, purge_interval
FROM ttrss_feeds WHERE
(cat_id = :cat OR (:cat = 0 AND cat_id IS NULL)) AND owner_uid = :uid AND $hide_qpart
ORDER BY order_id, title");
$fsth->execute([':cat' => $cat_id, ':uid' => $owner_uid]);
while ($fline = $fsth->fetch()) {
$title = htmlspecialchars($fline["title"]);
$url = htmlspecialchars($fline["feed_url"]);
$site_url = htmlspecialchars($fline["site_url"]);
if ($include_settings) {
$update_interval = (int)$fline["update_interval"];
$order_id = (int)$fline["order_id"];
$purge_interval = (int)$fline["purge_interval"];
$ttrss_specific_qpart = "ttrssSortOrder=\"$order_id\" ttrssPurgeInterval=\"$purge_interval\" ttrssUpdateInterval=\"$update_interval\"";
} else {
$ttrss_specific_qpart = "";
}
if ($site_url) {
$html_url_qpart = "htmlUrl=\"$site_url\"";
} else {
$html_url_qpart = "";
}
$out .= "<outline type=\"rss\" text=\"$title\" xmlUrl=\"$url\" $ttrss_specific_qpart $html_url_qpart/>\n";
}
if ($cat_title) $out .= "</outline>\n";
return $out;
}
function opml_export($filename, $owner_uid, $hide_private_feeds = false, $include_settings = true, $file_output = false) {
if (!$owner_uid) return;
if (!$file_output)
if (!isset($_REQUEST["debug"])) {
header("Content-type: application/xml+opml");
header("Content-Disposition: attachment; filename=$filename");
} else {
header("Content-type: text/xml");
}
$out = "<?xml version=\"1.0\" encoding=\"utf-8\"?".">";
$out .= "<opml version=\"1.0\">";
$out .= "<head>
<dateCreated>" . date("r", time()) . "</dateCreated>
<title>Tiny Tiny RSS Feed Export</title>
</head>";
$out .= "<body>";
$out .= $this->opml_export_category($owner_uid, 0, $hide_private_feeds, $include_settings);
# export tt-rss settings
if ($include_settings) {
$out .= "<outline text=\"tt-rss-prefs\" schema-version=\"".Db_Updater::SCHEMA_VERSION."\">";
$sth = $this->pdo->prepare("SELECT pref_name, value FROM ttrss_user_prefs2 WHERE
profile IS NULL AND owner_uid = ? ORDER BY pref_name");
$sth->execute([$owner_uid]);
while ($line = $sth->fetch()) {
$name = $line["pref_name"];
$value = htmlspecialchars($line["value"]);
$out .= "<outline pref-name=\"$name\" value=\"$value\"/>";
}
$out .= "</outline>";
$out .= "<outline text=\"tt-rss-labels\" schema-version=\"".Db_Updater::SCHEMA_VERSION."\">";
$sth = $this->pdo->prepare("SELECT * FROM ttrss_labels2 WHERE
owner_uid = ?");
$sth->execute([$owner_uid]);
while ($line = $sth->fetch()) {
$name = htmlspecialchars($line['caption']);
$fg_color = htmlspecialchars($line['fg_color']);
$bg_color = htmlspecialchars($line['bg_color']);
$out .= "<outline label-name=\"$name\" label-fg-color=\"$fg_color\" label-bg-color=\"$bg_color\"/>";
}
$out .= "</outline>";
$out .= "<outline text=\"tt-rss-filters\" schema-version=\"".Db_Updater::SCHEMA_VERSION."\">";
$sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2
WHERE owner_uid = ? ORDER BY id");
$sth->execute([$owner_uid]);
while ($line = $sth->fetch()) {
$line["rules"] = array();
$line["actions"] = array();
$tmph = $this->pdo->prepare("SELECT * FROM ttrss_filters2_rules
WHERE filter_id = ?");
$tmph->execute([$line['id']]);
while ($tmp_line = $tmph->fetch(PDO::FETCH_ASSOC)) {
unset($tmp_line["id"]);
unset($tmp_line["filter_id"]);
$cat_filter = $tmp_line["cat_filter"];
if (!$tmp_line["match_on"]) {
if ($cat_filter && $tmp_line["cat_id"] || $tmp_line["feed_id"]) {
$tmp_line["feed"] = Feeds::_get_title(
$cat_filter ? $tmp_line["cat_id"] : $tmp_line["feed_id"],
$cat_filter);
} else {
$tmp_line["feed"] = "";
}
} else {
$match = [];
foreach (json_decode($tmp_line["match_on"], true) as $feed_id) {
if (strpos($feed_id, "CAT:") === 0) {
$feed_id = (int)substr($feed_id, 4);
if ($feed_id) {
array_push($match, [Feeds::_get_cat_title($feed_id), true, false]);
} else {
array_push($match, [0, true, true]);
}
} else {
if ($feed_id) {
array_push($match, [Feeds::_get_title((int)$feed_id), false, false]);
} else {
array_push($match, [0, false, true]);
}
}
}
$tmp_line["match"] = $match;
unset($tmp_line["match_on"]);
}
unset($tmp_line["feed_id"]);
unset($tmp_line["cat_id"]);
array_push($line["rules"], $tmp_line);
}
$tmph = $this->pdo->prepare("SELECT * FROM ttrss_filters2_actions
WHERE filter_id = ?");
$tmph->execute([$line['id']]);
while ($tmp_line = $tmph->fetch(PDO::FETCH_ASSOC)) {
unset($tmp_line["id"]);
unset($tmp_line["filter_id"]);
array_push($line["actions"], $tmp_line);
}
unset($line["id"]);
unset($line["owner_uid"]);
$filter = json_encode($line);
$out .= "<outline filter-type=\"2\"><![CDATA[$filter]]></outline>";
}
$out .= "</outline>";
}
$out .= "</body></opml>";
// Format output.
$doc = new DOMDocument();
$doc->formatOutput = true;
$doc->preserveWhiteSpace = false;
$doc->loadXML($out);
$xpath = new DOMXPath($doc);
$outlines = $xpath->query("//outline[@title]");
// cleanup empty categories
foreach ($outlines as $node) {
if ($node->getElementsByTagName('outline')->length == 0)
$node->parentNode->removeChild($node);
}
$res = $doc->saveXML();
/* // saveXML uses a two-space indent. Change to tabs.
$res = preg_replace_callback('/^(?: )+/mu',
create_function(
'$matches',
'return str_repeat("\t", intval(strlen($matches[0])/2));'),
$res); */
if ($file_output)
return file_put_contents($filename, $res) > 0;
else
print $res;
}
// Import
private function opml_import_feed($node, $cat_id, $owner_uid) {
$attrs = $node->attributes;
$feed_title = mb_substr($attrs->getNamedItem('text')->nodeValue, 0, 250);
if (!$feed_title) $feed_title = mb_substr($attrs->getNamedItem('title')->nodeValue, 0, 250);
$feed_url = $attrs->getNamedItem('xmlUrl')->nodeValue;
if (!$feed_url) $feed_url = $attrs->getNamedItem('xmlURL')->nodeValue;
$site_url = mb_substr($attrs->getNamedItem('htmlUrl')->nodeValue, 0, 250);
if ($feed_url) {
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE
feed_url = ? AND owner_uid = ?");
$sth->execute([$feed_url, $owner_uid]);
if (!$feed_title) $feed_title = '[Unknown]';
if (!$sth->fetch()) {
#$this->opml_notice("[FEED] [$feed_title/$feed_url] dst_CAT=$cat_id");
$this->opml_notice(T_sprintf("Adding feed: %s", $feed_title == '[Unknown]' ? $feed_url : $feed_title));
if (!$cat_id) $cat_id = null;
$update_interval = (int) $attrs->getNamedItem('ttrssUpdateInterval')->nodeValue;
if (!$update_interval) $update_interval = 0;
$order_id = (int) $attrs->getNamedItem('ttrssSortOrder')->nodeValue;
if (!$order_id) $order_id = 0;
$purge_interval = (int) $attrs->getNamedItem('ttrssPurgeInterval')->nodeValue;
if (!$purge_interval) $purge_interval = 0;
$sth = $this->pdo->prepare("INSERT INTO ttrss_feeds
(title, feed_url, owner_uid, cat_id, site_url, order_id, update_interval, purge_interval) VALUES
(?, ?, ?, ?, ?, ?, ?, ?)");
$sth->execute([$feed_title, $feed_url, $owner_uid, $cat_id, $site_url, $order_id, $update_interval, $purge_interval]);
} else {
$this->opml_notice(T_sprintf("Duplicate feed: %s", $feed_title == '[Unknown]' ? $feed_url : $feed_title));
}
}
}
private function opml_import_label($node, $owner_uid) {
$attrs = $node->attributes;
$label_name = $attrs->getNamedItem('label-name')->nodeValue;
if ($label_name) {
$fg_color = $attrs->getNamedItem('label-fg-color')->nodeValue;
$bg_color = $attrs->getNamedItem('label-bg-color')->nodeValue;
if (!Labels::find_id($label_name, $_SESSION['uid'])) {
$this->opml_notice(T_sprintf("Adding label %s", htmlspecialchars($label_name)));
Labels::create($label_name, $fg_color, $bg_color, $owner_uid);
} else {
$this->opml_notice(T_sprintf("Duplicate label: %s", htmlspecialchars($label_name)));
}
}
}
private function opml_import_preference($node) {
$attrs = $node->attributes;
$pref_name = $attrs->getNamedItem('pref-name')->nodeValue;
if ($pref_name) {
$pref_value = $attrs->getNamedItem('value')->nodeValue;
$this->opml_notice(T_sprintf("Setting preference key %s to %s",
$pref_name, $pref_value));
set_pref($pref_name, $pref_value);
}
}
private function opml_import_filter($node) {
$attrs = $node->attributes;
$filter_type = $attrs->getNamedItem('filter-type')->nodeValue;
if ($filter_type == '2') {
$filter = json_decode($node->nodeValue, true);
if ($filter) {
$match_any_rule = bool_to_sql_bool($filter["match_any_rule"]);
$enabled = bool_to_sql_bool($filter["enabled"]);
$inverse = bool_to_sql_bool($filter["inverse"]);
$title = $filter["title"];
//print "F: $title, $inverse, $enabled, $match_any_rule";
$sth = $this->pdo->prepare("INSERT INTO ttrss_filters2 (match_any_rule,enabled,inverse,title,owner_uid)
VALUES (?, ?, ?, ?, ?)");
$sth->execute([$match_any_rule, $enabled, $inverse, $title, $_SESSION['uid']]);
$sth = $this->pdo->prepare("SELECT MAX(id) AS id FROM ttrss_filters2 WHERE
owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
$filter_id = $row['id'];
if ($filter_id) {
$this->opml_notice(T_sprintf("Adding filter %s...", $title));
foreach ($filter["rules"] as $rule) {
$feed_id = null;
$cat_id = null;
if ($rule["match"]) {
$match_on = [];
foreach ($rule["match"] as $match) {
list ($name, $is_cat, $is_id) = $match;
if ($is_id) {
array_push($match_on, ($is_cat ? "CAT:" : "") . $name);
} else {
if (!$is_cat) {
$tsth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
WHERE title = ? AND owner_uid = ?");
$tsth->execute([$name, $_SESSION['uid']]);
if ($row = $tsth->fetch()) {
$match_id = $row['id'];
array_push($match_on, $match_id);
}
} else {
$tsth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories
WHERE title = ? AND owner_uid = ?");
$tsth->execute([$name, $_SESSION['uid']]);
if ($row = $tsth->fetch()) {
$match_id = $row['id'];
array_push($match_on, "CAT:$match_id");
}
}
}
}
$reg_exp = $rule["reg_exp"];
$filter_type = (int)$rule["filter_type"];
$inverse = bool_to_sql_bool($rule["inverse"]);
$match_on = json_encode($match_on);
$usth = $this->pdo->prepare("INSERT INTO ttrss_filters2_rules
(feed_id,cat_id,match_on,filter_id,filter_type,reg_exp,cat_filter,inverse)
VALUES
(NULL, NULL, ?, ?, ?, ?, false, ?)");
$usth->execute([$match_on, $filter_id, $filter_type, $reg_exp, $inverse]);
} else {
if (!$rule["cat_filter"]) {
$tsth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
WHERE title = ? AND owner_uid = ?");
$tsth->execute([$rule['feed'], $_SESSION['uid']]);
if ($row = $tsth->fetch()) {
$feed_id = $row['id'];
}
} else {
$tsth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories
WHERE title = ? AND owner_uid = ?");
$tsth->execute([$rule['feed'], $_SESSION['uid']]);
if ($row = $tsth->fetch()) {
$feed_id = $row['id'];
}
}
$cat_filter = bool_to_sql_bool($rule["cat_filter"]);
$reg_exp = $rule["reg_exp"];
$filter_type = (int)$rule["filter_type"];
$inverse = bool_to_sql_bool($rule["inverse"]);
$usth = $this->pdo->prepare("INSERT INTO ttrss_filters2_rules
(feed_id,cat_id,filter_id,filter_type,reg_exp,cat_filter,inverse)
VALUES
(?, ?, ?, ?, ?, ?, ?)");
$usth->execute([$feed_id, $cat_id, $filter_id, $filter_type, $reg_exp, $cat_filter, $inverse]);
}
}
foreach ($filter["actions"] as $action) {
$action_id = (int)$action["action_id"];
$action_param = $action["action_param"];
$usth = $this->pdo->prepare("INSERT INTO ttrss_filters2_actions
(filter_id,action_id,action_param)
VALUES
(?, ?, ?)");
$usth->execute([$filter_id, $action_id, $action_param]);
}
}
}
}
}
private function opml_import_category($doc, $root_node, $owner_uid, $parent_id) {
$default_cat_id = (int) $this->get_feed_category('Imported feeds', false);
if ($root_node) {
$cat_title = mb_substr($root_node->attributes->getNamedItem('text')->nodeValue, 0, 250);
if (!$cat_title)
$cat_title = mb_substr($root_node->attributes->getNamedItem('title')->nodeValue, 0, 250);
if (!in_array($cat_title, array("tt-rss-filters", "tt-rss-labels", "tt-rss-prefs"))) {
$cat_id = $this->get_feed_category($cat_title, $parent_id);
if ($cat_id === false) {
$order_id = (int) $root_node->attributes->getNamedItem('ttrssSortOrder')->nodeValue;
Feeds::_add_cat($cat_title, $_SESSION['uid'], $parent_id ? $parent_id : null, (int)$order_id);
$cat_id = $this->get_feed_category($cat_title, $parent_id);
}
} else {
$cat_id = 0;
}
$outlines = $root_node->childNodes;
} else {
$xpath = new DOMXPath($doc);
$outlines = $xpath->query("//opml/body/outline");
$cat_id = 0;
$cat_title = false;
}
#$this->opml_notice("[CAT] $cat_title id: $cat_id P_id: $parent_id");
$this->opml_notice(T_sprintf("Processing category: %s", $cat_title ? $cat_title : __("Uncategorized")));
foreach ($outlines as $node) {
if ($node->hasAttributes() && strtolower($node->tagName) == "outline") {
$attrs = $node->attributes;
$node_cat_title = $attrs->getNamedItem('text')->nodeValue;
if (!$node_cat_title)
$node_cat_title = $attrs->getNamedItem('title')->nodeValue;
$node_feed_url = $attrs->getNamedItem('xmlUrl')->nodeValue;
if ($node_cat_title && !$node_feed_url) {
$this->opml_import_category($doc, $node, $owner_uid, $cat_id);
} else {
if (!$cat_id) {
$dst_cat_id = $default_cat_id;
} else {
$dst_cat_id = $cat_id;
}
switch ($cat_title) {
case "tt-rss-prefs":
$this->opml_import_preference($node);
break;
case "tt-rss-labels":
$this->opml_import_label($node, $owner_uid);
break;
case "tt-rss-filters":
$this->opml_import_filter($node);
break;
default:
$this->opml_import_feed($node, $dst_cat_id, $owner_uid);
}
}
}
}
}
function opml_import($owner_uid) {
if (!$owner_uid) return;
$doc = false;
if ($_FILES['opml_file']['error'] != 0) {
print_error(T_sprintf("Upload failed with error code %d",
$_FILES['opml_file']['error']));
return;
}
if (is_uploaded_file($_FILES['opml_file']['tmp_name'])) {
$tmp_file = (string)tempnam(Config::get(Config::CACHE_DIR) . '/upload', 'opml');
$result = move_uploaded_file($_FILES['opml_file']['tmp_name'],
$tmp_file);
if (!$result) {
print_error(__("Unable to move uploaded file."));
return;
}
} else {
print_error(__('Error: please upload OPML file.'));
return;
}
$loaded = false;
if (is_file($tmp_file)) {
$doc = new DOMDocument();
libxml_disable_entity_loader(false);
$loaded = $doc->load($tmp_file);
libxml_disable_entity_loader(true);
unlink($tmp_file);
} else if (empty($doc)) {
print_error(__('Error: unable to find moved OPML file.'));
return;
}
if ($loaded) {
$this->pdo->beginTransaction();
$this->opml_import_category($doc, false, $owner_uid, false);
$this->pdo->commit();
} else {
print_error(__('Error while parsing document.'));
}
}
private function opml_notice($msg) {
print "$msg<br/>";
}
static function get_publish_url(){
return Config::get_self_url() .
"/public.php?op=publishOpml&key=" .
Feeds::_get_access_key('OPML:Publish', false, $_SESSION["uid"]);
}
function get_feed_category($feed_cat, $parent_cat_id = false) {
$parent_cat_id = (int) $parent_cat_id;
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories
WHERE title = :title
AND (parent_cat = :parent OR (:parent = 0 AND parent_cat IS NULL))
AND owner_uid = :uid");
$sth->execute([':title' => $feed_cat, ':parent' => $parent_cat_id, ':uid' => $_SESSION['uid']]);
if ($row = $sth->fetch()) {
return $row['id'];
} else {
return false;
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
class Handler_PluginHandler extends Handler_Protected {
function csrf_ignore($method) {
return true;
}
function catchall($method) {
$plugin_name = clean($_REQUEST["plugin"]);
$plugin = PluginHost::getInstance()->get_plugin($plugin_name);
$csrf_token = ($_POST["csrf_token"] ?? "");
if ($plugin) {
if (method_exists($plugin, $method)) {
if (validate_csrf($csrf_token) || $plugin->csrf_ignore($method)) {
$plugin->$method();
} else {
user_error("Rejected ${plugin_name}->${method}(): invalid CSRF token.", E_USER_WARNING);
print Errors::to_json(Errors::E_UNAUTHORIZED);
}
} else {
user_error("Rejected ${plugin_name}->${method}(): unknown method.", E_USER_WARNING);
print Errors::to_json(Errors::E_UNKNOWN_METHOD);
}
} else {
user_error("Rejected ${plugin_name}->${method}(): unknown plugin.", E_USER_WARNING);
print Errors::to_json(Errors::E_UNKNOWN_PLUGIN);
}
}
}

786
classes/handler/rpc.php Normal file
View File

@@ -0,0 +1,786 @@
<?php
class Handler_RPC extends Handler_Protected {
/*function csrf_ignore($method) {
$csrf_ignored = array("completelabels");
return array_search($method, $csrf_ignored) !== false;
}*/
private function _translations_as_array() {
global $text_domains;
$rv = [];
foreach (array_keys($text_domains) as $domain) {
/** @var gettext_reader $l10n */
$l10n = _get_reader($domain);
for ($i = 0; $i < $l10n->total; $i++) {
if (isset($l10n->table_originals[$i * 2 + 2]) && $orig = $l10n->get_original_string($i)) {
if(strpos($orig, "\000") !== false) { // Plural forms
$key = explode(chr(0), $orig);
$rv[$key[0]] = _ngettext($key[0], $key[1], 1); // Singular
$rv[$key[1]] = _ngettext($key[0], $key[1], 2); // Plural
} else {
$translation = _dgettext($domain,$orig);
$rv[$orig] = $translation;
}
}
}
}
return $rv;
}
function togglepref() {
$key = clean($_REQUEST["key"]);
set_pref($key, !get_pref($key));
$value = get_pref($key);
print json_encode(array("param" =>$key, "value" => $value));
}
function setpref() {
// set_pref escapes input, so no need to double escape it here
$key = clean($_REQUEST['key']);
$value = $_REQUEST['value'];
set_pref($key, $value, $_SESSION["uid"], $key != 'USER_STYLESHEET');
print json_encode(array("param" =>$key, "value" => $value));
}
function mark() {
$mark = clean($_REQUEST["mark"]);
$id = clean($_REQUEST["id"]);
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET marked = ?,
last_marked = NOW()
WHERE ref_id = ? AND owner_uid = ?");
$sth->execute([$mark, $id, $_SESSION['uid']]);
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
function delete() {
$ids = explode(",", clean($_REQUEST["ids"]));
$ids_qmarks = arr_qmarks($ids);
$sth = $this->pdo->prepare("DELETE FROM ttrss_user_entries
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
Article::_purge_orphans();
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
function publ() {
$pub = clean($_REQUEST["pub"]);
$id = clean($_REQUEST["id"]);
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
published = ?, last_published = NOW()
WHERE ref_id = ? AND owner_uid = ?");
$sth->execute([$pub, $id, $_SESSION['uid']]);
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
function getRuntimeInfo() {
$reply = [
'runtime-info' => $this->_make_runtime_info()
];
print json_encode($reply);
}
function getAllCounters() {
@$seq = (int) $_REQUEST['seq'];
$feed_id_count = (int)$_REQUEST["feed_id_count"];
$label_id_count = (int)$_REQUEST["label_id_count"];
// it seems impossible to distinguish empty array [] from a null - both become unset in $_REQUEST
// so, count is >= 0 means we had an array, -1 means null
// we need null because it means "return all counters"; [] would return nothing
if ($feed_id_count == -1)
$feed_ids = null;
else
$feed_ids = array_map("intval", clean($_REQUEST["feed_ids"] ?? []));
if ($label_id_count == -1)
$label_ids = null;
else
$label_ids = array_map("intval", clean($_REQUEST["label_ids"] ?? []));
$counters = is_array($feed_ids) && !get_pref(Prefs::DISABLE_CONDITIONAL_COUNTERS) ?
Counters::get_conditional($feed_ids, $label_ids) : Counters::get_all();
$reply = [
'counters' => $counters,
'seq' => $seq
];
print json_encode($reply);
}
/* GET["cmode"] = 0 - mark as read, 1 - as unread, 2 - toggle */
function catchupSelected() {
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
$cmode = (int)clean($_REQUEST["cmode"]);
if (count($ids) > 0)
Article::_catchup_by_id($ids, $cmode);
print json_encode(["message" => "UPDATE_COUNTERS",
"labels" => Article::_labels_of($ids),
"feeds" => Article::_feeds_of($ids)]);
}
function markSelected() {
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
$cmode = (int)clean($_REQUEST["cmode"]);
if (count($ids) > 0)
$this->markArticlesById($ids, $cmode);
print json_encode(["message" => "UPDATE_COUNTERS",
"labels" => Article::_labels_of($ids),
"feeds" => Article::_feeds_of($ids)]);
}
function publishSelected() {
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
$cmode = (int)clean($_REQUEST["cmode"]);
if (count($ids) > 0)
$this->publishArticlesById($ids, $cmode);
print json_encode(["message" => "UPDATE_COUNTERS",
"labels" => Article::_labels_of($ids),
"feeds" => Article::_feeds_of($ids)]);
}
function sanityCheck() {
$_SESSION["hasSandbox"] = clean($_REQUEST["hasSandbox"]) === "true";
$_SESSION["clientTzOffset"] = clean($_REQUEST["clientTzOffset"]);
$client_location = $_REQUEST["clientLocation"];
$error = Errors::E_SUCCESS;
$error_params = [];
$client_scheme = parse_url($client_location, PHP_URL_SCHEME);
$server_scheme = parse_url(Config::get_self_url(), PHP_URL_SCHEME);
if (Db_Updater::is_update_required()) {
$error = Errors::E_SCHEMA_MISMATCH;
} else if ($client_scheme != $server_scheme) {
$error = Errors::E_URL_SCHEME_MISMATCH;
$error_params["client_scheme"] = $client_scheme;
$error_params["server_scheme"] = $server_scheme;
$error_params["self_url_path"] = Config::get_self_url();
}
if ($error == Errors::E_SUCCESS) {
$reply = [];
$reply['init-params'] = $this->_make_init_params();
$reply['runtime-info'] = $this->_make_runtime_info();
$reply['translations'] = $this->_translations_as_array();
print json_encode($reply);
} else {
print Errors::to_json($error, $error_params);
}
}
/*function completeLabels() {
$search = clean($_REQUEST["search"]);
$sth = $this->pdo->prepare("SELECT DISTINCT caption FROM
ttrss_labels2
WHERE owner_uid = ? AND
LOWER(caption) LIKE LOWER(?) ORDER BY caption
LIMIT 5");
$sth->execute([$_SESSION['uid'], "%$search%"]);
print "<ul>";
while ($line = $sth->fetch()) {
print "<li>" . $line["caption"] . "</li>";
}
print "</ul>";
}*/
function catchupFeed() {
$feed_id = clean($_REQUEST['feed_id']);
$is_cat = clean($_REQUEST['is_cat']) == "true";
$mode = clean($_REQUEST['mode'] ?? '');
$search_query = clean($_REQUEST['search_query']);
$search_lang = clean($_REQUEST['search_lang']);
Feeds::_catchup($feed_id, $is_cat, false, $mode, [$search_query, $search_lang]);
// return counters here synchronously so that frontend can figure out next unread feed properly
print json_encode(['counters' => Counters::get_all()]);
//print json_encode(array("message" => "UPDATE_COUNTERS"));
}
function setWidescreen() {
$wide = (int) clean($_REQUEST["wide"]);
set_pref(Prefs::WIDESCREEN_MODE, $wide);
print json_encode(["wide" => $wide]);
}
static function updaterandomfeed_real() {
$default_interval = (int) Prefs::get_default(Prefs::DEFAULT_UPDATE_INTERVAL);
// Test if the feed need a update (update interval exceded).
if (Config::get(Config::DB_TYPE) == "pgsql") {
$update_limit_qpart = "AND ((
update_interval = 0
AND (p.value IS NULL OR p.value != '-1')
AND last_updated < NOW() - CAST((COALESCE(p.value, '$default_interval') || ' minutes') AS INTERVAL)
) OR (
update_interval > 0
AND last_updated < NOW() - CAST((update_interval || ' minutes') AS INTERVAL)
) OR (
update_interval >= 0
AND (p.value IS NULL OR p.value != '-1')
AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL)
))";
} else {
$update_limit_qpart = "AND ((
update_interval = 0
AND (p.value IS NULL OR p.value != '-1')
AND last_updated < DATE_SUB(NOW(), INTERVAL CONVERT(COALESCE(p.value, '$default_interval'), SIGNED INTEGER) MINUTE)
) OR (
update_interval > 0
AND last_updated < DATE_SUB(NOW(), INTERVAL update_interval MINUTE)
) OR (
update_interval >= 0
AND (p.value IS NULL OR p.value != '-1')
AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL)
))";
}
// Test if feed is currently being updated by another process.
if (Config::get(Config::DB_TYPE) == "pgsql") {
$updstart_thresh_qpart = "AND (last_update_started IS NULL OR last_update_started < NOW() - INTERVAL '5 minutes')";
} else {
$updstart_thresh_qpart = "AND (last_update_started IS NULL OR last_update_started < DATE_SUB(NOW(), INTERVAL 5 MINUTE))";
}
$random_qpart = Db::sql_random_function();
$pdo = Db::pdo();
// we could be invoked from public.php with no active session
if (!empty($_SESSION["uid"])) {
$owner_check_qpart = "AND f.owner_uid = ".$pdo->quote($_SESSION["uid"]);
} else {
$owner_check_qpart = "";
}
$query = "SELECT f.feed_url,f.id
FROM
ttrss_feeds f, ttrss_users u LEFT JOIN ttrss_user_prefs2 p ON
(p.owner_uid = u.id AND profile IS NULL AND pref_name = 'DEFAULT_UPDATE_INTERVAL')
WHERE
f.owner_uid = u.id
$owner_check_qpart
$update_limit_qpart
$updstart_thresh_qpart
ORDER BY $random_qpart LIMIT 30";
$res = $pdo->query($query);
$num_updated = 0;
$tstart = time();
while ($line = $res->fetch()) {
$feed_id = $line["id"];
if (time() - $tstart < ini_get("max_execution_time") * 0.7) {
RSSUtils::update_rss_feed($feed_id, true);
++$num_updated;
} else {
break;
}
}
// Purge orphans and cleanup tags
Article::_purge_orphans();
//cleanup_tags(14, 50000);
if ($num_updated > 0) {
print json_encode(array("message" => "UPDATE_COUNTERS",
"num_updated" => $num_updated));
} else {
print json_encode(array("message" => "NOTHING_TO_UPDATE"));
}
}
function updaterandomfeed() {
self::updaterandomfeed_real();
}
private function markArticlesById($ids, $cmode) {
$ids_qmarks = arr_qmarks($ids);
if ($cmode == 0) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
marked = false, last_marked = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else if ($cmode == 1) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
marked = true, last_marked = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
marked = NOT marked,last_marked = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
}
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
}
private function publishArticlesById($ids, $cmode) {
$ids_qmarks = arr_qmarks($ids);
if ($cmode == 0) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
published = false, last_published = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else if ($cmode == 1) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
published = true, last_published = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
published = NOT published,last_published = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
}
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
}
function log() {
$msg = clean($_REQUEST['msg']);
$file = basename(clean($_REQUEST['file']));
$line = (int) clean($_REQUEST['line']);
$context = clean($_REQUEST['context']);
if ($msg) {
Logger::log_error(E_USER_WARNING,
$msg, 'client-js:' . $file, $line, $context);
echo json_encode(array("message" => "HOST_ERROR_LOGGED"));
}
}
function checkforupdates() {
$rv = ["changeset" => [], "plugins" => []];
$version = Config::get_version(false);
$git_timestamp = $version["timestamp"] ?? false;
$git_commit = $version["commit"] ?? false;
if (Config::get(Config::CHECK_FOR_UPDATES) && $_SESSION["access_level"] >= 10 && $git_timestamp) {
$content = @UrlHelper::fetch(["url" => "https://tt-rss.org/version.json"]);
if ($content) {
$content = json_decode($content, true);
if ($content && isset($content["changeset"])) {
if ($git_timestamp < (int)$content["changeset"]["timestamp"] &&
$git_commit != $content["changeset"]["id"]) {
$rv["changeset"] = $content["changeset"];
}
}
}
$rv["plugins"] = Pref_Prefs::_get_updated_plugins();
}
print json_encode($rv);
}
private function _make_init_params() {
$params = array();
foreach ([Prefs::ON_CATCHUP_SHOW_NEXT_FEED, Prefs::HIDE_READ_FEEDS,
Prefs::ENABLE_FEED_CATS, Prefs::FEEDS_SORT_BY_UNREAD,
Prefs::CONFIRM_FEED_CATCHUP, Prefs::CDM_AUTO_CATCHUP,
Prefs::FRESH_ARTICLE_MAX_AGE, Prefs::HIDE_READ_SHOWS_SPECIAL,
Prefs::COMBINED_DISPLAY_MODE, Prefs::DEBUG_HEADLINE_IDS] as $param) {
$params[strtolower($param)] = (int) get_pref($param);
}
$params["safe_mode"] = !empty($_SESSION["safe_mode"]);
$params["check_for_updates"] = Config::get(Config::CHECK_FOR_UPDATES);
$params["icons_url"] = Config::get(Config::ICONS_URL);
$params["cookie_lifetime"] = Config::get(Config::SESSION_COOKIE_LIFETIME);
$params["default_view_mode"] = get_pref(Prefs::_DEFAULT_VIEW_MODE);
$params["default_view_limit"] = (int) get_pref(Prefs::_DEFAULT_VIEW_LIMIT);
$params["default_view_order_by"] = get_pref(Prefs::_DEFAULT_VIEW_ORDER_BY);
$params["bw_limit"] = (int) $_SESSION["bw_limit"];
$params["is_default_pw"] = UserHelper::is_default_password();
$params["label_base_index"] = LABEL_BASE_INDEX;
$theme = get_pref(Prefs::USER_CSS_THEME);
$params["theme"] = theme_exists($theme) ? $theme : "";
$params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
$params["php_platform"] = PHP_OS;
$params["php_version"] = PHP_VERSION;
$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"] = Config::get_self_url();
$params["max_feed_id"] = (int) $max_feed_id;
$params["num_feeds"] = (int) $num_feeds;
$params["hotkeys"] = $this->get_hotkeys_map();
$params["widescreen"] = (int) get_pref(Prefs::WIDESCREEN_MODE);
$params['simple_update'] = Config::get(Config::SIMPLE_UPDATE_MODE);
$params["icon_indicator_white"] = $this->image_to_base64("images/indicator_white.gif");
$params["labels"] = Labels::get_all($_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((string)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(Prefs::CDM_EXPANDED);
$data["labels"] = Labels::get_all($_SESSION["uid"]);
if (Config::get(Config::LOG_DESTINATION) == 'sql' && $_SESSION['access_level'] >= 10) {
if (Config::get(Config::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
errno NOT IN (".E_USER_NOTICE.", ".E_USER_DEPRECATED.") AND
$log_interval AND
errstr NOT LIKE '%imagecreatefromstring(): Data is not in a recognized format%'");
$sth->execute();
if ($row = $sth->fetch()) {
$data['recent_log_events'] = $row['cid'];
}
}
if (file_exists(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon.lock")) {
$data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
if (time() - ($_SESSION["daemon_stamp_check"] ?? 0) > 30) {
$stamp = (int) @file_get_contents(Config::get(Config::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_prefs" => __("Preferences")),
__("Other") => array(
"create_label" => __("Create label"),
"create_filter" => __("Create filter"),
"collapse_sidebar" => __("Un/collapse sidebar"),
"help_dialog" => __("Show help dialog"))
);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HOTKEY_INFO,
function ($result) use (&$hotkeys) {
$hotkeys = $result;
},
$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 P" => "goto_prefs",
"r" => "select_article_cursor",
"c l" => "create_label",
"c f" => "create_filter",
"c s" => "collapse_sidebar",
"?" => "help_dialog",
);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HOTKEY_MAP,
function ($result) use (&$hotkeys) {
$hotkeys = $result;
},
$hotkeys);
$prefixes = array();
foreach (array_keys($hotkeys) as $hotkey) {
$pair = explode(" ", (string)$hotkey, 2);
if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
array_push($prefixes, $pair[0]);
}
}
return array($prefixes, $hotkeys);
}
function hotkeyHelp() {
$info = self::get_hotkeys_info();
$imap = self::get_hotkeys_map();
$omap = array();
foreach ($imap[1] as $sequence => $action) {
if (!isset($omap[$action])) $omap[$action] = array();
array_push($omap[$action], $sequence);
}
?>
<ul class='panel panel-scrollable hotkeys-help' style='height : 300px'>
<?php
foreach ($info as $section => $hotkeys) {
?>
<li><h3><?= $section ?></h3></li>
<?php
foreach ($hotkeys as $action => $description) {
if (!empty($omap[$action])) {
foreach ($omap[$action] as $sequence) {
if (strpos($sequence, "|") !== false) {
$sequence = substr($sequence,
strpos($sequence, "|")+1,
strlen($sequence));
} else {
$keys = explode(" ", $sequence);
for ($i = 0; $i < count($keys); $i++) {
if (strlen($keys[$i]) > 1) {
$tmp = '';
foreach (str_split($keys[$i]) as $c) {
switch ($c) {
case '*':
$tmp .= __('Shift') . '+';
break;
case '^':
$tmp .= __('Ctrl') . '+';
break;
default:
$tmp .= $c;
}
}
$keys[$i] = $tmp;
}
}
$sequence = join(" ", $keys);
}
?>
<li>
<div class='hk'><code><?= $sequence ?></code></div>
<div class='desc'><?= $description ?></div>
</li>
<?php
}
}
}
}
?>
</ul>
<footer class='text-center'>
<?= \Controls\submit_tag(__('Close this window')) ?>
</footer>
<?php
}
}