api.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  1. <?php
  2. class API extends Handler {
  3. const API_LEVEL = 4;
  4. const STATUS_OK = 0;
  5. const STATUS_ERR = 1;
  6. private $seq;
  7. function before($method) {
  8. if (parent::before($method)) {
  9. header("Content-Type: text/json");
  10. if (!$_SESSION["uid"] && $method != "login" && $method != "isloggedin") {
  11. print $this->wrap(self::STATUS_ERR, array("error" => 'NOT_LOGGED_IN'));
  12. return false;
  13. }
  14. if ($_SESSION["uid"] && $method != "logout" && !get_pref($this->link, 'ENABLE_API_ACCESS')) {
  15. print $this->wrap(self::STATUS_ERR, array("error" => 'API_DISABLED'));
  16. return false;
  17. }
  18. $this->seq = (int) $_REQUEST['seq'];
  19. return true;
  20. }
  21. return false;
  22. }
  23. function wrap($status, $reply) {
  24. print json_encode(array("seq" => $this->seq,
  25. "status" => $status,
  26. "content" => $reply));
  27. }
  28. function getVersion() {
  29. $rv = array("version" => VERSION);
  30. print $this->wrap(self::STATUS_OK, $rv);
  31. }
  32. function getApiLevel() {
  33. $rv = array("level" => self::API_LEVEL);
  34. print $this->wrap(self::STATUS_OK, $rv);
  35. }
  36. function login() {
  37. $login = db_escape_string($_REQUEST["user"]);
  38. $password = $_REQUEST["password"];
  39. $password_base64 = base64_decode($_REQUEST["password"]);
  40. if (SINGLE_USER_MODE) $login = "admin";
  41. $result = db_query($this->link, "SELECT id FROM ttrss_users WHERE login = '$login'");
  42. if (db_num_rows($result) != 0) {
  43. $uid = db_fetch_result($result, 0, "id");
  44. } else {
  45. $uid = 0;
  46. }
  47. if (!$uid) {
  48. print $this->wrap(self::STATUS_ERR, array("error" => "LOGIN_ERROR"));
  49. return;
  50. }
  51. if (get_pref($this->link, "ENABLE_API_ACCESS", $uid)) {
  52. if (authenticate_user($this->link, $login, $password)) { // try login with normal password
  53. print $this->wrap(self::STATUS_OK, array("session_id" => session_id(),
  54. "api_level" => self::API_LEVEL));
  55. } else if (authenticate_user($this->link, $login, $password_base64)) { // else try with base64_decoded password
  56. print $this->wrap(self::STATUS_OK, array("session_id" => session_id(),
  57. "api_level" => self::API_LEVEL));
  58. } else { // else we are not logged in
  59. print $this->wrap(self::STATUS_ERR, array("error" => "LOGIN_ERROR"));
  60. }
  61. } else {
  62. print $this->wrap(self::STATUS_ERR, array("error" => "API_DISABLED"));
  63. }
  64. }
  65. function logout() {
  66. logout_user();
  67. print $this->wrap(self::STATUS_OK, array("status" => "OK"));
  68. }
  69. function isLoggedIn() {
  70. print $this->wrap(self::STATUS_OK, array("status" => $_SESSION["uid"] != ''));
  71. }
  72. function getUnread() {
  73. $feed_id = db_escape_string($_REQUEST["feed_id"]);
  74. $is_cat = db_escape_string($_REQUEST["is_cat"]);
  75. if ($feed_id) {
  76. print $this->wrap(self::STATUS_OK, array("unread" => getFeedUnread($this->link, $feed_id, $is_cat)));
  77. } else {
  78. print $this->wrap(self::STATUS_OK, array("unread" => getGlobalUnread($this->link)));
  79. }
  80. }
  81. /* Method added for ttrss-reader for Android */
  82. function getCounters() {
  83. print $this->wrap(self::STATUS_OK, getAllCounters($this->link));
  84. }
  85. function getFeeds() {
  86. $cat_id = db_escape_string($_REQUEST["cat_id"]);
  87. $unread_only = (bool)db_escape_string($_REQUEST["unread_only"]);
  88. $limit = (int) db_escape_string($_REQUEST["limit"]);
  89. $offset = (int) db_escape_string($_REQUEST["offset"]);
  90. $include_nested = (bool)db_escape_string($_REQUEST["include_nested"]);
  91. $feeds = $this->api_get_feeds($this->link, $cat_id, $unread_only, $limit, $offset, $include_nested);
  92. print $this->wrap(self::STATUS_OK, $feeds);
  93. }
  94. function getCategories() {
  95. $unread_only = (bool)db_escape_string($_REQUEST["unread_only"]);
  96. $enable_nested = (bool)db_escape_string($_REQUEST["enable_nested"]);
  97. // TODO do not return empty categories, return Uncategorized and standard virtual cats
  98. if ($enable_nested)
  99. $nested_qpart = "parent_cat IS NULL";
  100. else
  101. $nested_qpart = "true";
  102. $result = db_query($this->link, "SELECT
  103. id, title, order_id, (SELECT COUNT(id) FROM
  104. ttrss_feeds WHERE
  105. ttrss_feed_categories.id IS NOT NULL AND cat_id = ttrss_feed_categories.id) AS num_feeds
  106. FROM ttrss_feed_categories
  107. WHERE $nested_qpart AND owner_uid = " .
  108. $_SESSION["uid"]);
  109. $cats = array();
  110. while ($line = db_fetch_assoc($result)) {
  111. if ($line["num_feeds"] > 0) {
  112. $unread = getFeedUnread($this->link, $line["id"], true);
  113. if ($enable_nested)
  114. $unread += getCategoryChildrenUnread($this->link, $line["id"]);
  115. if ($unread || !$unread_only) {
  116. array_push($cats, array("id" => $line["id"],
  117. "title" => $line["title"],
  118. "unread" => $unread,
  119. "order_id" => (int) $line["order_id"],
  120. ));
  121. }
  122. }
  123. }
  124. foreach (array(-2,-1,0) as $cat_id) {
  125. $unread = getFeedUnread($this->link, $cat_id, true);
  126. if ($unread || !$unread_only) {
  127. array_push($cats, array("id" => $cat_id,
  128. "title" => getCategoryTitle($this->link, $cat_id),
  129. "unread" => $unread));
  130. }
  131. }
  132. print $this->wrap(self::STATUS_OK, $cats);
  133. }
  134. function getHeadlines() {
  135. $feed_id = db_escape_string($_REQUEST["feed_id"]);
  136. if ($feed_id != "") {
  137. $limit = (int)db_escape_string($_REQUEST["limit"]);
  138. if (!$limit || $limit >= 60) $limit = 60;
  139. $offset = (int)db_escape_string($_REQUEST["skip"]);
  140. $filter = db_escape_string($_REQUEST["filter"]);
  141. $is_cat = (bool)db_escape_string($_REQUEST["is_cat"]);
  142. $show_excerpt = (bool)db_escape_string($_REQUEST["show_excerpt"]);
  143. $show_content = (bool)db_escape_string($_REQUEST["show_content"]);
  144. /* all_articles, unread, adaptive, marked, updated */
  145. $view_mode = db_escape_string($_REQUEST["view_mode"]);
  146. $include_attachments = (bool)db_escape_string($_REQUEST["include_attachments"]);
  147. $since_id = (int)db_escape_string($_REQUEST["since_id"]);
  148. $include_nested = (bool)db_escape_string($_REQUEST["include_nested"]);
  149. $sanitize_content = true;
  150. /* do not rely on params below */
  151. $search = db_escape_string($_REQUEST["search"]);
  152. $search_mode = db_escape_string($_REQUEST["search_mode"]);
  153. $match_on = db_escape_string($_REQUEST["match_on"]);
  154. $headlines = $this->api_get_headlines($this->link, $feed_id, $limit, $offset,
  155. $filter, $is_cat, $show_excerpt, $show_content, $view_mode, false,
  156. $include_attachments, $since_id, $search, $search_mode, $match_on,
  157. $include_nested, $sanitize_content);
  158. print $this->wrap(self::STATUS_OK, $headlines);
  159. } else {
  160. print $this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE'));
  161. }
  162. }
  163. function updateArticle() {
  164. $article_ids = array_filter(explode(",", db_escape_string($_REQUEST["article_ids"])), is_numeric);
  165. $mode = (int) db_escape_string($_REQUEST["mode"]);
  166. $data = db_escape_string($_REQUEST["data"]);
  167. $field_raw = (int)db_escape_string($_REQUEST["field"]);
  168. $field = "";
  169. $set_to = "";
  170. switch ($field_raw) {
  171. case 0:
  172. $field = "marked";
  173. break;
  174. case 1:
  175. $field = "published";
  176. break;
  177. case 2:
  178. $field = "unread";
  179. break;
  180. case 3:
  181. $field = "note";
  182. };
  183. switch ($mode) {
  184. case 1:
  185. $set_to = "true";
  186. break;
  187. case 0:
  188. $set_to = "false";
  189. break;
  190. case 2:
  191. $set_to = "NOT $field";
  192. break;
  193. }
  194. if ($field == "note") $set_to = "'$data'";
  195. if ($field && $set_to && count($article_ids) > 0) {
  196. $article_ids = join(", ", $article_ids);
  197. if ($field == "unread") {
  198. $result = db_query($this->link, "UPDATE ttrss_user_entries SET $field = $set_to,
  199. last_read = NOW()
  200. WHERE ref_id IN ($article_ids) AND owner_uid = " . $_SESSION["uid"]);
  201. } else {
  202. $result = db_query($this->link, "UPDATE ttrss_user_entries SET $field = $set_to
  203. WHERE ref_id IN ($article_ids) AND owner_uid = " . $_SESSION["uid"]);
  204. }
  205. $num_updated = db_affected_rows($this->link, $result);
  206. if ($num_updated > 0 && $field == "unread") {
  207. $result = db_query($this->link, "SELECT DISTINCT feed_id FROM ttrss_user_entries
  208. WHERE ref_id IN ($article_ids)");
  209. while ($line = db_fetch_assoc($result)) {
  210. ccache_update($this->link, $line["feed_id"], $_SESSION["uid"]);
  211. }
  212. }
  213. print $this->wrap(self::STATUS_OK, array("status" => "OK",
  214. "updated" => $num_updated));
  215. } else {
  216. print $this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE'));
  217. }
  218. }
  219. function getArticle() {
  220. $article_id = join(",", array_filter(explode(",", db_escape_string($_REQUEST["article_id"])), is_numeric));
  221. $query = "SELECT id,title,link,content,cached_content,feed_id,comments,int_id,
  222. marked,unread,published,
  223. ".SUBSTRING_FOR_DATE."(updated,1,16) as updated,
  224. author
  225. FROM ttrss_entries,ttrss_user_entries
  226. WHERE id IN ($article_id) AND ref_id = id AND owner_uid = " .
  227. $_SESSION["uid"] ;
  228. $result = db_query($this->link, $query);
  229. $articles = array();
  230. if (db_num_rows($result) != 0) {
  231. while ($line = db_fetch_assoc($result)) {
  232. $attachments = get_article_enclosures($this->link, $line['id']);
  233. $article = array(
  234. "id" => $line["id"],
  235. "title" => $line["title"],
  236. "link" => $line["link"],
  237. "labels" => get_article_labels($this->link, $line['id']),
  238. "unread" => sql_bool_to_bool($line["unread"]),
  239. "marked" => sql_bool_to_bool($line["marked"]),
  240. "published" => sql_bool_to_bool($line["published"]),
  241. "comments" => $line["comments"],
  242. "author" => $line["author"],
  243. "updated" => strtotime($line["updated"]),
  244. "content" => $line["cached_content"] != "" ? $line["cached_content"] : $line["content"],
  245. "feed_id" => $line["feed_id"],
  246. "attachments" => $attachments
  247. );
  248. array_push($articles, $article);
  249. }
  250. }
  251. print $this->wrap(self::STATUS_OK, $articles);
  252. }
  253. function getConfig() {
  254. $config = array(
  255. "icons_dir" => ICONS_DIR,
  256. "icons_url" => ICONS_URL);
  257. $config["daemon_is_running"] = file_is_locked("update_daemon.lock");
  258. $result = db_query($this->link, "SELECT COUNT(*) AS cf FROM
  259. ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
  260. $num_feeds = db_fetch_result($result, 0, "cf");
  261. $config["num_feeds"] = (int)$num_feeds;
  262. print $this->wrap(self::STATUS_OK, $config);
  263. }
  264. function updateFeed() {
  265. $feed_id = db_escape_string($_REQUEST["feed_id"]);
  266. update_rss_feed($this->link, $feed_id, true);
  267. print $this->wrap(self::STATUS_OK, array("status" => "OK"));
  268. }
  269. function catchupFeed() {
  270. $feed_id = db_escape_string($_REQUEST["feed_id"]);
  271. $is_cat = db_escape_string($_REQUEST["is_cat"]);
  272. catchup_feed($this->link, $feed_id, $is_cat);
  273. print $this->wrap(self::STATUS_OK, array("status" => "OK"));
  274. }
  275. function getPref() {
  276. $pref_name = db_escape_string($_REQUEST["pref_name"]);
  277. print $this->wrap(self::STATUS_OK, array("value" => get_pref($this->link, $pref_name)));
  278. }
  279. function getLabels() {
  280. //$article_ids = array_filter(explode(",", db_escape_string($_REQUEST["article_ids"])), is_numeric);
  281. $article_id = (int)$_REQUEST['article_id'];
  282. $rv = array();
  283. $result = db_query($this->link, "SELECT id, caption, fg_color, bg_color
  284. FROM ttrss_labels2
  285. WHERE owner_uid = '".$_SESSION['uid']."' ORDER BY caption");
  286. if ($article_id)
  287. $article_labels = get_article_labels($this->link, $article_id);
  288. else
  289. $article_labels = array();
  290. while ($line = db_fetch_assoc($result)) {
  291. $checked = false;
  292. foreach ($article_labels as $al) {
  293. if ($al[0] == $line['id']) {
  294. $checked = true;
  295. break;
  296. }
  297. }
  298. array_push($rv, array(
  299. "id" => (int)$line['id'],
  300. "caption" => $line['caption'],
  301. "fg_color" => $line['fg_color'],
  302. "bg_color" => $line['bg_color'],
  303. "checked" => $checked));
  304. }
  305. print $this->wrap(self::STATUS_OK, $rv);
  306. }
  307. function setArticleLabel() {
  308. $article_ids = array_filter(explode(",", db_escape_string($_REQUEST["article_ids"])), is_numeric);
  309. $label_id = (int) db_escape_string($_REQUEST['label_id']);
  310. $assign = (bool) db_escape_string($_REQUEST['assign']) == "true";
  311. $label = db_escape_string(label_find_caption($this->link,
  312. $label_id, $_SESSION["uid"]));
  313. $num_updated = 0;
  314. if ($label) {
  315. foreach ($article_ids as $id) {
  316. if ($assign)
  317. label_add_article($this->link, $id, $label, $_SESSION["uid"]);
  318. else
  319. label_remove_article($this->link, $id, $label, $_SESSION["uid"]);
  320. ++$num_updated;
  321. }
  322. }
  323. print $this->wrap(self::STATUS_OK, array("status" => "OK",
  324. "updated" => $num_updated));
  325. }
  326. function index() {
  327. print $this->wrap(self::STATUS_ERR, array("error" => 'UNKNOWN_METHOD'));
  328. }
  329. function shareToPublished() {
  330. $title = db_escape_string(strip_tags($_REQUEST["title"]));
  331. $url = db_escape_string(strip_tags($_REQUEST["url"]));
  332. $content = db_escape_string(strip_tags($_REQUEST["content"]));
  333. if (Article::create_published_article($this->link, $title, $url, $content, "", $_SESSION["uid"])) {
  334. print $this->wrap(self::STATUS_OK, array("status" => 'OK'));
  335. } else {
  336. print $this->wrap(self::STATUS_ERR, array("error" => 'Publishing failed'));
  337. }
  338. }
  339. static function api_get_feeds($link, $cat_id, $unread_only, $limit, $offset, $include_nested = false) {
  340. $feeds = array();
  341. /* Labels */
  342. if ($cat_id == -4 || $cat_id == -2) {
  343. $counters = getLabelCounters($link, true);
  344. foreach (array_values($counters) as $cv) {
  345. $unread = $cv["counter"];
  346. if ($unread || !$unread_only) {
  347. $row = array(
  348. "id" => $cv["id"],
  349. "title" => $cv["description"],
  350. "unread" => $cv["counter"],
  351. "cat_id" => -2,
  352. );
  353. array_push($feeds, $row);
  354. }
  355. }
  356. }
  357. /* Virtual feeds */
  358. if ($cat_id == -4 || $cat_id == -1) {
  359. foreach (array(-1, -2, -3, -4, -6, 0) as $i) {
  360. $unread = getFeedUnread($link, $i);
  361. if ($unread || !$unread_only) {
  362. $title = getFeedTitle($link, $i);
  363. $row = array(
  364. "id" => $i,
  365. "title" => $title,
  366. "unread" => $unread,
  367. "cat_id" => -1,
  368. );
  369. array_push($feeds, $row);
  370. }
  371. }
  372. }
  373. /* Child cats */
  374. if ($include_nested && $cat_id) {
  375. $result = db_query($link, "SELECT
  376. id, title FROM ttrss_feed_categories
  377. WHERE parent_cat = '$cat_id' AND owner_uid = " . $_SESSION["uid"] .
  378. " ORDER BY id, title");
  379. while ($line = db_fetch_assoc($result)) {
  380. $unread = getFeedUnread($link, $line["id"], true) +
  381. getCategoryChildrenUnread($link, $line["id"]);
  382. if ($unread || !$unread_only) {
  383. $row = array(
  384. "id" => $line["id"],
  385. "title" => $line["title"],
  386. "unread" => $unread,
  387. "is_cat" => true,
  388. );
  389. array_push($feeds, $row);
  390. }
  391. }
  392. }
  393. /* Real feeds */
  394. if ($limit) {
  395. $limit_qpart = "LIMIT $limit OFFSET $offset";
  396. } else {
  397. $limit_qpart = "";
  398. }
  399. if ($cat_id == -4 || $cat_id == -3) {
  400. $result = db_query($link, "SELECT
  401. id, feed_url, cat_id, title, order_id, ".
  402. SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated
  403. FROM ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"] .
  404. " ORDER BY cat_id, title " . $limit_qpart);
  405. } else {
  406. if ($cat_id)
  407. $cat_qpart = "cat_id = '$cat_id'";
  408. else
  409. $cat_qpart = "cat_id IS NULL";
  410. $result = db_query($link, "SELECT
  411. id, feed_url, cat_id, title, order_id, ".
  412. SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated
  413. FROM ttrss_feeds WHERE
  414. $cat_qpart AND owner_uid = " . $_SESSION["uid"] .
  415. " ORDER BY cat_id, title " . $limit_qpart);
  416. }
  417. while ($line = db_fetch_assoc($result)) {
  418. $unread = getFeedUnread($link, $line["id"]);
  419. $has_icon = feed_has_icon($line['id']);
  420. if ($unread || !$unread_only) {
  421. $row = array(
  422. "feed_url" => $line["feed_url"],
  423. "title" => $line["title"],
  424. "id" => (int)$line["id"],
  425. "unread" => (int)$unread,
  426. "has_icon" => $has_icon,
  427. "cat_id" => (int)$line["cat_id"],
  428. "last_updated" => strtotime($line["last_updated"]),
  429. "order_id" => (int) $line["order_id"],
  430. );
  431. array_push($feeds, $row);
  432. }
  433. }
  434. return $feeds;
  435. }
  436. static function api_get_headlines($link, $feed_id, $limit, $offset,
  437. $filter, $is_cat, $show_excerpt, $show_content, $view_mode, $order,
  438. $include_attachments, $since_id,
  439. $search = "", $search_mode = "", $match_on = "",
  440. $include_nested = false, $sanitize_content = true) {
  441. $qfh_ret = queryFeedHeadlines($link, $feed_id, $limit,
  442. $view_mode, $is_cat, $search, $search_mode, $match_on,
  443. $order, $offset, 0, false, $since_id, $include_nested);
  444. $result = $qfh_ret[0];
  445. $feed_title = $qfh_ret[1];
  446. $headlines = array();
  447. while ($line = db_fetch_assoc($result)) {
  448. $is_updated = ($line["last_read"] == "" &&
  449. ($line["unread"] != "t" && $line["unread"] != "1"));
  450. $tags = explode(",", $line["tag_cache"]);
  451. $labels = json_decode($line["label_cache"], true);
  452. //if (!$tags) $tags = get_article_tags($link, $line["id"]);
  453. //if (!$labels) $labels = get_article_labels($link, $line["id"]);
  454. $headline_row = array(
  455. "id" => (int)$line["id"],
  456. "unread" => sql_bool_to_bool($line["unread"]),
  457. "marked" => sql_bool_to_bool($line["marked"]),
  458. "published" => sql_bool_to_bool($line["published"]),
  459. "updated" => strtotime($line["updated"]),
  460. "is_updated" => $is_updated,
  461. "title" => $line["title"],
  462. "link" => $line["link"],
  463. "feed_id" => $line["feed_id"],
  464. "tags" => $tags,
  465. );
  466. if ($include_attachments)
  467. $headline_row['attachments'] = get_article_enclosures($link,
  468. $line['id']);
  469. if ($show_excerpt) {
  470. $excerpt = truncate_string(strip_tags($line["content_preview"]), 100);
  471. $headline_row["excerpt"] = $excerpt;
  472. }
  473. if ($show_content) {
  474. if ($line["cached_content"] != "") {
  475. $line["content_preview"] =& $line["cached_content"];
  476. }
  477. if ($sanitize_content) {
  478. $headline_row["content"] = sanitize($link,
  479. $line["content_preview"], false, false, $line["site_url"]);
  480. } else {
  481. $headline_row["content"] = $line["content_preview"];
  482. }
  483. }
  484. // unify label output to ease parsing
  485. if ($labels["no-labels"] == 1) $labels = array();
  486. $headline_row["labels"] = $labels;
  487. $headline_row["feed_title"] = $line["feed_title"];
  488. $headline_row["comments_count"] = (int)$line["num_comments"];
  489. $headline_row["comments_link"] = $line["comments"];
  490. $headline_row["always_display_attachments"] = sql_bool_to_bool($line["always_display_enclosures"]);
  491. array_push($headlines, $headline_row);
  492. }
  493. return $headlines;
  494. }
  495. }
  496. ?>