1 | <?php |
||
2 | class API extends Handler { |
||
3 | |||
4 | const API_LEVEL = 14; |
||
5 | |||
6 | const STATUS_OK = 0; |
||
7 | const STATUS_ERR = 1; |
||
8 | |||
9 | private $seq; |
||
10 | |||
11 | public static function param_to_bool($p) { |
||
12 | return $p && ($p !== "f" && $p !== "false"); |
||
13 | } |
||
14 | |||
15 | public function before($method) { |
||
16 | if (parent::before($method)) { |
||
17 | header("Content-Type: text/json"); |
||
18 | |||
19 | if (!$_SESSION["uid"] && $method != "login" && $method != "isloggedin") { |
||
20 | $this->wrap(self::STATUS_ERR, array("error" => 'NOT_LOGGED_IN')); |
||
21 | return false; |
||
22 | } |
||
23 | |||
24 | if ($_SESSION["uid"] && $method != "logout" && !get_pref('ENABLE_API_ACCESS')) { |
||
25 | $this->wrap(self::STATUS_ERR, array("error" => 'API_DISABLED')); |
||
26 | return false; |
||
27 | } |
||
28 | |||
29 | $this->seq = (int) clean($_REQUEST['seq']); |
||
30 | |||
31 | return true; |
||
32 | } |
||
33 | return false; |
||
34 | } |
||
35 | |||
36 | public function wrap($status, $reply) { |
||
37 | print json_encode(array("seq" => $this->seq, |
||
38 | "status" => $status, |
||
39 | "content" => $reply)); |
||
40 | } |
||
41 | |||
42 | public function getVersion() { |
||
43 | $rv = array("version" => VERSION); |
||
44 | $this->wrap(self::STATUS_OK, $rv); |
||
45 | } |
||
46 | |||
47 | public function getApiLevel() { |
||
48 | $rv = array("level" => self::API_LEVEL); |
||
49 | $this->wrap(self::STATUS_OK, $rv); |
||
50 | } |
||
51 | |||
52 | public function login() { |
||
53 | @session_destroy(); |
||
54 | @session_start(); |
||
55 | |||
56 | $login = clean($_REQUEST["user"]); |
||
57 | $password = clean($_REQUEST["password"]); |
||
58 | $password_base64 = base64_decode(clean($_REQUEST["password"])); |
||
59 | |||
60 | if (SINGLE_USER_MODE) { |
||
61 | $login = "admin"; |
||
62 | } |
||
63 | |||
64 | $sth = $this->pdo->prepare("SELECT id FROM ttrss_users WHERE login = ?"); |
||
65 | $sth->execute([$login]); |
||
66 | |||
67 | if ($row = $sth->fetch()) { |
||
68 | $uid = $row["id"]; |
||
69 | } else { |
||
70 | $uid = 0; |
||
71 | } |
||
72 | |||
73 | if (!$uid) { |
||
74 | $this->wrap(self::STATUS_ERR, array("error" => "LOGIN_ERROR")); |
||
75 | return; |
||
76 | } |
||
77 | |||
78 | if (get_pref("ENABLE_API_ACCESS", $uid)) { |
||
79 | if (authenticate_user($login, $password, false, Auth_Base::AUTH_SERVICE_API)) { // try login with normal password |
||
80 | $this->wrap(self::STATUS_OK, array("session_id" => session_id(), |
||
81 | "api_level" => self::API_LEVEL)); |
||
82 | } else if (authenticate_user($login, $password_base64, false, Auth_Base::AUTH_SERVICE_API)) { // else try with base64_decoded password |
||
83 | $this->wrap(self::STATUS_OK, array("session_id" => session_id(), |
||
84 | "api_level" => self::API_LEVEL)); |
||
85 | } else { // else we are not logged in |
||
86 | user_error("Failed login attempt for $login from {$_SERVER['REMOTE_ADDR']}", E_USER_WARNING); |
||
87 | $this->wrap(self::STATUS_ERR, array("error" => "LOGIN_ERROR")); |
||
88 | } |
||
89 | } else { |
||
90 | $this->wrap(self::STATUS_ERR, array("error" => "API_DISABLED")); |
||
91 | } |
||
92 | |||
93 | } |
||
94 | |||
95 | public function logout() { |
||
96 | logout_user(); |
||
97 | $this->wrap(self::STATUS_OK, array("status" => "OK")); |
||
98 | } |
||
99 | |||
100 | public function isLoggedIn() { |
||
101 | $this->wrap(self::STATUS_OK, array("status" => $_SESSION["uid"] != '')); |
||
102 | } |
||
103 | |||
104 | public function getUnread() { |
||
105 | $feed_id = clean($_REQUEST["feed_id"]); |
||
106 | $is_cat = clean($_REQUEST["is_cat"]); |
||
107 | |||
108 | if ($feed_id) { |
||
109 | $this->wrap(self::STATUS_OK, array("unread" => getFeedUnread($feed_id, $is_cat))); |
||
110 | } else { |
||
111 | $this->wrap(self::STATUS_OK, array("unread" => Feeds::getGlobalUnread())); |
||
112 | } |
||
113 | } |
||
114 | |||
115 | /* Method added for ttrss-reader for Android */ |
||
116 | public function getCounters() { |
||
117 | $this->wrap(self::STATUS_OK, Counters::getAllCounters()); |
||
118 | } |
||
119 | |||
120 | public function getFeeds() { |
||
121 | $cat_id = clean($_REQUEST["cat_id"]); |
||
122 | $unread_only = API::param_to_bool(clean($_REQUEST["unread_only"])); |
||
123 | $limit = (int) clean($_REQUEST["limit"]); |
||
124 | $offset = (int) clean($_REQUEST["offset"]); |
||
125 | $include_nested = API::param_to_bool(clean($_REQUEST["include_nested"])); |
||
126 | |||
127 | $feeds = $this->api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested); |
||
128 | |||
129 | $this->wrap(self::STATUS_OK, $feeds); |
||
130 | } |
||
131 | |||
132 | public function getCategories() { |
||
133 | $unread_only = API::param_to_bool(clean($_REQUEST["unread_only"])); |
||
134 | $enable_nested = API::param_to_bool(clean($_REQUEST["enable_nested"])); |
||
135 | $include_empty = API::param_to_bool(clean($_REQUEST['include_empty'])); |
||
136 | |||
137 | // TODO do not return empty categories, return Uncategorized and standard virtual cats |
||
138 | |||
139 | if ($enable_nested) { |
||
140 | $nested_qpart = "parent_cat IS NULL"; |
||
141 | } else { |
||
142 | $nested_qpart = "true"; |
||
143 | } |
||
144 | |||
145 | $sth = $this->pdo->prepare("SELECT |
||
146 | id, title, order_id, (SELECT COUNT(id) FROM |
||
147 | ttrss_feeds WHERE |
||
148 | ttrss_feed_categories.id IS NOT NULL AND cat_id = ttrss_feed_categories.id) AS num_feeds, |
||
149 | (SELECT COUNT(id) FROM |
||
150 | ttrss_feed_categories AS c2 WHERE |
||
151 | c2.parent_cat = ttrss_feed_categories.id) AS num_cats |
||
152 | FROM ttrss_feed_categories |
||
153 | WHERE $nested_qpart AND owner_uid = ?"); |
||
154 | $sth->execute([$_SESSION['uid']]); |
||
155 | |||
156 | $cats = array(); |
||
157 | |||
158 | while ($line = $sth->fetch()) { |
||
159 | if ($include_empty || $line["num_feeds"] > 0 || $line["num_cats"] > 0) { |
||
160 | $unread = getFeedUnread($line["id"], true); |
||
161 | |||
162 | if ($enable_nested) { |
||
163 | $unread += Feeds::getCategoryChildrenUnread($line["id"]); |
||
164 | } |
||
165 | |||
166 | if ($unread || !$unread_only) { |
||
167 | array_push($cats, array("id" => $line["id"], |
||
168 | "title" => $line["title"], |
||
169 | "unread" => $unread, |
||
170 | "order_id" => (int) $line["order_id"], |
||
171 | )); |
||
172 | } |
||
173 | } |
||
174 | } |
||
175 | |||
176 | foreach (array(-2, -1, 0) as $cat_id) { |
||
177 | if ($include_empty || !$this->isCategoryEmpty($cat_id)) { |
||
178 | $unread = getFeedUnread($cat_id, true); |
||
179 | |||
180 | if ($unread || !$unread_only) { |
||
181 | array_push($cats, array("id" => $cat_id, |
||
182 | "title" => Feeds::getCategoryTitle($cat_id), |
||
183 | "unread" => $unread)); |
||
184 | } |
||
185 | } |
||
186 | } |
||
187 | |||
188 | $this->wrap(self::STATUS_OK, $cats); |
||
189 | } |
||
190 | |||
191 | public function getHeadlines() { |
||
192 | $feed_id = clean($_REQUEST["feed_id"]); |
||
193 | if ($feed_id !== "") { |
||
194 | |||
195 | if (is_numeric($feed_id)) { |
||
196 | $feed_id = (int) $feed_id; |
||
197 | } |
||
198 | |||
199 | $limit = (int) clean($_REQUEST["limit"]); |
||
200 | |||
201 | if (!$limit || $limit >= 200) { |
||
202 | $limit = 200; |
||
203 | } |
||
204 | |||
205 | $offset = (int) clean($_REQUEST["skip"]); |
||
206 | $filter = clean($_REQUEST["filter"]); |
||
207 | $is_cat = API::param_to_bool(clean($_REQUEST["is_cat"])); |
||
208 | $show_excerpt = API::param_to_bool(clean($_REQUEST["show_excerpt"])); |
||
209 | $show_content = API::param_to_bool(clean($_REQUEST["show_content"])); |
||
210 | /* all_articles, unread, adaptive, marked, updated */ |
||
211 | $view_mode = clean($_REQUEST["view_mode"]); |
||
212 | $include_attachments = API::param_to_bool(clean($_REQUEST["include_attachments"])); |
||
213 | $since_id = (int) clean($_REQUEST["since_id"]); |
||
214 | $include_nested = API::param_to_bool(clean($_REQUEST["include_nested"])); |
||
215 | $sanitize_content = !isset($_REQUEST["sanitize"]) || |
||
216 | API::param_to_bool($_REQUEST["sanitize"]); |
||
217 | $force_update = API::param_to_bool(clean($_REQUEST["force_update"])); |
||
218 | $has_sandbox = API::param_to_bool(clean($_REQUEST["has_sandbox"])); |
||
219 | $excerpt_length = (int) clean($_REQUEST["excerpt_length"]); |
||
220 | $check_first_id = (int) clean($_REQUEST["check_first_id"]); |
||
221 | $include_header = API::param_to_bool(clean($_REQUEST["include_header"])); |
||
222 | |||
223 | $_SESSION['hasSandbox'] = $has_sandbox; |
||
224 | |||
225 | $skip_first_id_check = false; |
||
226 | |||
227 | $override_order = false; |
||
228 | switch (clean($_REQUEST["order_by"])) { |
||
229 | case "title": |
||
230 | $override_order = "ttrss_entries.title, date_entered, updated"; |
||
231 | break; |
||
232 | case "date_reverse": |
||
233 | $override_order = "score DESC, date_entered, updated"; |
||
234 | $skip_first_id_check = true; |
||
235 | break; |
||
236 | case "feed_dates": |
||
237 | $override_order = "updated DESC"; |
||
238 | break; |
||
239 | } |
||
240 | |||
241 | /* do not rely on params below */ |
||
242 | |||
243 | $search = clean($_REQUEST["search"]); |
||
244 | |||
245 | list($headlines, $headlines_header) = $this->api_get_headlines($feed_id, $limit, $offset, |
||
246 | $filter, $is_cat, $show_excerpt, $show_content, $view_mode, $override_order, |
||
247 | $include_attachments, $since_id, $search, |
||
248 | $include_nested, $sanitize_content, $force_update, $excerpt_length, $check_first_id, $skip_first_id_check); |
||
249 | |||
250 | if ($include_header) { |
||
251 | $this->wrap(self::STATUS_OK, array($headlines_header, $headlines)); |
||
252 | } else { |
||
253 | $this->wrap(self::STATUS_OK, $headlines); |
||
254 | } |
||
255 | } else { |
||
256 | $this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE')); |
||
257 | } |
||
258 | } |
||
259 | |||
260 | public function updateArticle() { |
||
261 | $article_ids = explode(",", clean($_REQUEST["article_ids"])); |
||
262 | $mode = (int) clean($_REQUEST["mode"]); |
||
263 | $data = clean($_REQUEST["data"]); |
||
264 | $field_raw = (int) clean($_REQUEST["field"]); |
||
265 | |||
266 | $field = ""; |
||
267 | $set_to = ""; |
||
268 | |||
269 | switch ($field_raw) { |
||
270 | case 0: |
||
271 | $field = "marked"; |
||
272 | $additional_fields = ",last_marked = NOW()"; |
||
273 | break; |
||
274 | case 1: |
||
275 | $field = "published"; |
||
276 | $additional_fields = ",last_published = NOW()"; |
||
277 | break; |
||
278 | case 2: |
||
279 | $field = "unread"; |
||
280 | $additional_fields = ",last_read = NOW()"; |
||
281 | break; |
||
282 | case 3: |
||
283 | $field = "note"; |
||
284 | }; |
||
285 | |||
286 | switch ($mode) { |
||
287 | case 1: |
||
288 | $set_to = "true"; |
||
289 | break; |
||
290 | case 0: |
||
291 | $set_to = "false"; |
||
292 | break; |
||
293 | case 2: |
||
294 | $set_to = "not $field"; |
||
295 | break; |
||
296 | } |
||
297 | |||
298 | if ($field == "note") { |
||
299 | $set_to = $this->pdo->quote($data); |
||
300 | } |
||
301 | |||
302 | if ($field && $set_to && count($article_ids) > 0) { |
||
303 | |||
304 | $article_qmarks = arr_qmarks($article_ids); |
||
305 | |||
306 | $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET |
||
307 | $field = $set_to $additional_fields |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
![]() |
|||
308 | WHERE ref_id IN ($article_qmarks) AND owner_uid = ?"); |
||
309 | $sth->execute(array_merge($article_ids, [$_SESSION['uid']])); |
||
310 | |||
311 | $num_updated = $sth->rowCount(); |
||
312 | |||
313 | if ($num_updated > 0 && $field == "unread") { |
||
314 | $sth = $this->pdo->prepare("SELECT DISTINCT feed_id FROM ttrss_user_entries |
||
315 | WHERE ref_id IN ($article_qmarks)"); |
||
316 | $sth->execute($article_ids); |
||
317 | |||
318 | while ($line = $sth->fetch()) { |
||
319 | CCache::update($line["feed_id"], $_SESSION["uid"]); |
||
320 | } |
||
321 | } |
||
322 | |||
323 | $this->wrap(self::STATUS_OK, array("status" => "OK", |
||
324 | "updated" => $num_updated)); |
||
325 | |||
326 | } else { |
||
327 | $this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE')); |
||
328 | } |
||
329 | |||
330 | } |
||
331 | |||
332 | public function getArticle() { |
||
333 | |||
334 | $article_ids = explode(",", clean($_REQUEST["article_id"])); |
||
335 | $sanitize_content = !isset($_REQUEST["sanitize"]) || |
||
336 | API::param_to_bool($_REQUEST["sanitize"]); |
||
337 | |||
338 | if ($article_ids) { |
||
339 | |||
340 | $article_qmarks = arr_qmarks($article_ids); |
||
341 | |||
342 | $sth = $this->pdo->prepare("SELECT id,guid,title,link,content,feed_id,comments,int_id, |
||
343 | marked,unread,published,score,note,lang, |
||
344 | ".SUBSTRING_FOR_DATE."(updated,1,16) as updated, |
||
345 | author,(SELECT title FROM ttrss_feeds WHERE id = feed_id) AS feed_title, |
||
346 | (SELECT site_url FROM ttrss_feeds WHERE id = feed_id) AS site_url, |
||
347 | (SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) AS hide_images |
||
348 | FROM ttrss_entries,ttrss_user_entries |
||
349 | WHERE id IN ($article_qmarks) AND ref_id = id AND owner_uid = ?"); |
||
350 | |||
351 | $sth->execute(array_merge($article_ids, [$_SESSION['uid']])); |
||
352 | |||
353 | $articles = array(); |
||
354 | |||
355 | while ($line = $sth->fetch()) { |
||
356 | |||
357 | $attachments = Article::get_article_enclosures($line['id']); |
||
358 | |||
359 | $article = array( |
||
360 | "id" => $line["id"], |
||
361 | "guid" => $line["guid"], |
||
362 | "title" => $line["title"], |
||
363 | "link" => $line["link"], |
||
364 | "labels" => Article::get_article_labels($line['id']), |
||
365 | "unread" => API::param_to_bool($line["unread"]), |
||
366 | "marked" => API::param_to_bool($line["marked"]), |
||
367 | "published" => API::param_to_bool($line["published"]), |
||
368 | "comments" => $line["comments"], |
||
369 | "author" => $line["author"], |
||
370 | "updated" => (int) strtotime($line["updated"]), |
||
371 | "feed_id" => $line["feed_id"], |
||
372 | "attachments" => $attachments, |
||
373 | "score" => (int) $line["score"], |
||
374 | "feed_title" => $line["feed_title"], |
||
375 | "note" => $line["note"], |
||
376 | "lang" => $line["lang"] |
||
377 | ); |
||
378 | |||
379 | if ($sanitize_content) { |
||
380 | $article["content"] = sanitize( |
||
381 | $line["content"], |
||
382 | API::param_to_bool($line['hide_images']), |
||
383 | false, $line["site_url"], false, $line["id"]); |
||
384 | } else { |
||
385 | $article["content"] = $line["content"]; |
||
386 | } |
||
387 | |||
388 | foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE_API) as $p) { |
||
389 | $article = $p->hook_render_article_api(array("article" => $article)); |
||
390 | } |
||
391 | |||
392 | $article['content'] = DiskCache::rewriteUrls($article['content']); |
||
393 | |||
394 | array_push($articles, $article); |
||
395 | |||
396 | } |
||
397 | |||
398 | $this->wrap(self::STATUS_OK, $articles); |
||
399 | } else { |
||
400 | $this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE')); |
||
401 | } |
||
402 | } |
||
403 | |||
404 | public function getConfig() { |
||
405 | $config = array( |
||
406 | "icons_dir" => ICONS_DIR, |
||
407 | "icons_url" => ICONS_URL); |
||
408 | |||
409 | $config["daemon_is_running"] = file_is_locked("update_daemon.lock"); |
||
410 | |||
411 | $sth = $this->pdo->prepare("SELECT COUNT(*) AS cf FROM |
||
412 | ttrss_feeds WHERE owner_uid = ?"); |
||
413 | $sth->execute([$_SESSION['uid']]); |
||
414 | $row = $sth->fetch(); |
||
415 | |||
416 | $config["num_feeds"] = $row["cf"]; |
||
417 | |||
418 | $this->wrap(self::STATUS_OK, $config); |
||
419 | } |
||
420 | |||
421 | public function updateFeed() { |
||
422 | $feed_id = (int) clean($_REQUEST["feed_id"]); |
||
423 | |||
424 | if (!ini_get("open_basedir")) { |
||
425 | RSSUtils::update_rss_feed($feed_id); |
||
426 | } |
||
427 | |||
428 | $this->wrap(self::STATUS_OK, array("status" => "OK")); |
||
429 | } |
||
430 | |||
431 | public function catchupFeed() { |
||
432 | $feed_id = clean($_REQUEST["feed_id"]); |
||
433 | $is_cat = clean($_REQUEST["is_cat"]); |
||
434 | |||
435 | Feeds::catchup_feed($feed_id, $is_cat); |
||
436 | |||
437 | $this->wrap(self::STATUS_OK, array("status" => "OK")); |
||
438 | } |
||
439 | |||
440 | public function getPref() { |
||
441 | $pref_name = clean($_REQUEST["pref_name"]); |
||
442 | |||
443 | $this->wrap(self::STATUS_OK, array("value" => get_pref($pref_name))); |
||
444 | } |
||
445 | |||
446 | public function getLabels() { |
||
447 | $article_id = (int) clean($_REQUEST['article_id']); |
||
448 | |||
449 | $rv = array(); |
||
450 | |||
451 | $sth = $this->pdo->prepare("SELECT id, caption, fg_color, bg_color |
||
452 | FROM ttrss_labels2 |
||
453 | WHERE owner_uid = ? ORDER BY caption"); |
||
454 | $sth->execute([$_SESSION['uid']]); |
||
455 | |||
456 | if ($article_id) { |
||
457 | $article_labels = Article::get_article_labels($article_id); |
||
458 | } else { |
||
459 | $article_labels = array(); |
||
460 | } |
||
461 | |||
462 | while ($line = $sth->fetch()) { |
||
463 | |||
464 | $checked = false; |
||
465 | foreach ($article_labels as $al) { |
||
466 | if (Labels::feed_to_label_id($al[0]) == $line['id']) { |
||
467 | $checked = true; |
||
468 | break; |
||
469 | } |
||
470 | } |
||
471 | |||
472 | array_push($rv, array( |
||
473 | "id" => (int) Labels::label_to_feed_id($line['id']), |
||
474 | "caption" => $line['caption'], |
||
475 | "fg_color" => $line['fg_color'], |
||
476 | "bg_color" => $line['bg_color'], |
||
477 | "checked" => $checked)); |
||
478 | } |
||
479 | |||
480 | $this->wrap(self::STATUS_OK, $rv); |
||
481 | } |
||
482 | |||
483 | public function setArticleLabel() { |
||
484 | |||
485 | $article_ids = explode(",", clean($_REQUEST["article_ids"])); |
||
486 | $label_id = (int) clean($_REQUEST['label_id']); |
||
487 | $assign = API::param_to_bool(clean($_REQUEST['assign'])); |
||
488 | |||
489 | $label = Labels::find_caption(Labels::feed_to_label_id($label_id), $_SESSION["uid"]); |
||
490 | |||
491 | $num_updated = 0; |
||
492 | |||
493 | if ($label) { |
||
494 | |||
495 | foreach ($article_ids as $id) { |
||
496 | |||
497 | if ($assign) { |
||
498 | Labels::add_article($id, $label, $_SESSION["uid"]); |
||
499 | } else { |
||
500 | Labels::remove_article($id, $label, $_SESSION["uid"]); |
||
501 | } |
||
502 | |||
503 | ++$num_updated; |
||
504 | |||
505 | } |
||
506 | } |
||
507 | |||
508 | $this->wrap(self::STATUS_OK, array("status" => "OK", |
||
509 | "updated" => $num_updated)); |
||
510 | |||
511 | } |
||
512 | |||
513 | public function index($method) { |
||
514 | $plugin = PluginHost::getInstance()->get_api_method(strtolower($method)); |
||
515 | |||
516 | if ($plugin && method_exists($plugin, $method)) { |
||
517 | $reply = $plugin->$method(); |
||
518 | |||
519 | $this->wrap($reply[0], $reply[1]); |
||
520 | |||
521 | } else { |
||
522 | $this->wrap(self::STATUS_ERR, array("error" => 'UNKNOWN_METHOD', "method" => $method)); |
||
523 | } |
||
524 | } |
||
525 | |||
526 | public function shareToPublished() { |
||
527 | $title = strip_tags(clean($_REQUEST["title"])); |
||
528 | $url = strip_tags(clean($_REQUEST["url"])); |
||
529 | $content = strip_tags(clean($_REQUEST["content"])); |
||
530 | |||
531 | if (Article::create_published_article($title, $url, $content, "", $_SESSION["uid"])) { |
||
532 | $this->wrap(self::STATUS_OK, array("status" => 'OK')); |
||
533 | } else { |
||
534 | $this->wrap(self::STATUS_ERR, array("error" => 'Publishing failed')); |
||
535 | } |
||
536 | } |
||
537 | |||
538 | public static function api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested = false) { |
||
539 | |||
540 | $feeds = array(); |
||
541 | |||
542 | $pdo = Db::pdo(); |
||
543 | |||
544 | $limit = (int) $limit; |
||
545 | $offset = (int) $offset; |
||
546 | $cat_id = (int) $cat_id; |
||
547 | |||
548 | /* Labels */ |
||
549 | |||
550 | /* API only: -4 All feeds, including virtual feeds */ |
||
551 | if ($cat_id == -4 || $cat_id == -2) { |
||
552 | $counters = Counters::getLabelCounters(true); |
||
553 | |||
554 | foreach (array_values($counters) as $cv) { |
||
555 | |||
556 | $unread = $cv["counter"]; |
||
557 | |||
558 | if ($unread || !$unread_only) { |
||
559 | |||
560 | $row = array( |
||
561 | "id" => (int) $cv["id"], |
||
562 | "title" => $cv["description"], |
||
563 | "unread" => $cv["counter"], |
||
564 | "cat_id" => -2, |
||
565 | ); |
||
566 | |||
567 | array_push($feeds, $row); |
||
568 | } |
||
569 | } |
||
570 | } |
||
571 | |||
572 | /* Virtual feeds */ |
||
573 | |||
574 | if ($cat_id == -4 || $cat_id == -1) { |
||
575 | foreach (array(-1, -2, -3, -4, -6, 0) as $i) { |
||
576 | $unread = getFeedUnread($i); |
||
577 | |||
578 | if ($unread || !$unread_only) { |
||
579 | $title = Feeds::getFeedTitle($i); |
||
580 | |||
581 | $row = array( |
||
582 | "id" => $i, |
||
583 | "title" => $title, |
||
584 | "unread" => $unread, |
||
585 | "cat_id" => -1, |
||
586 | ); |
||
587 | array_push($feeds, $row); |
||
588 | } |
||
589 | |||
590 | } |
||
591 | } |
||
592 | |||
593 | /* Child cats */ |
||
594 | |||
595 | if ($include_nested && $cat_id) { |
||
596 | $sth = $pdo->prepare("SELECT |
||
597 | id, title, order_id FROM ttrss_feed_categories |
||
598 | WHERE parent_cat = ? AND owner_uid = ? ORDER BY order_id, title"); |
||
599 | |||
600 | $sth->execute([$cat_id, $_SESSION['uid']]); |
||
601 | |||
602 | while ($line = $sth->fetch()) { |
||
603 | $unread = getFeedUnread($line["id"], true) + |
||
604 | Feeds::getCategoryChildrenUnread($line["id"]); |
||
605 | |||
606 | if ($unread || !$unread_only) { |
||
607 | $row = array( |
||
608 | "id" => (int) $line["id"], |
||
609 | "title" => $line["title"], |
||
610 | "unread" => $unread, |
||
611 | "is_cat" => true, |
||
612 | "order_id" => (int) $line["order_id"] |
||
613 | ); |
||
614 | array_push($feeds, $row); |
||
615 | } |
||
616 | } |
||
617 | } |
||
618 | |||
619 | /* Real feeds */ |
||
620 | |||
621 | if ($limit) { |
||
622 | $limit_qpart = "limit $limit OFFSET $offset"; |
||
623 | } else { |
||
624 | $limit_qpart = ""; |
||
625 | } |
||
626 | |||
627 | /* API only: -3 All feeds, excluding virtual feeds (e.g. Labels and such) */ |
||
628 | if ($cat_id == -4 || $cat_id == -3) { |
||
629 | $sth = $pdo->prepare("SELECT |
||
630 | id, feed_url, cat_id, title, order_id, ". |
||
631 | SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated |
||
632 | FROM ttrss_feeds WHERE owner_uid = ? |
||
633 | ORDER BY order_id, title " . $limit_qpart); |
||
634 | $sth->execute([$_SESSION['uid']]); |
||
635 | |||
636 | } else { |
||
637 | |||
638 | $sth = $pdo->prepare("SELECT |
||
639 | id, feed_url, cat_id, title, order_id, ". |
||
640 | SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated |
||
641 | FROM ttrss_feeds WHERE |
||
642 | (cat_id = :cat OR (:cat = 0 AND cat_id IS NULL)) |
||
643 | AND owner_uid = :uid |
||
644 | ORDER BY order_id, title " . $limit_qpart); |
||
645 | $sth->execute([":uid" => $_SESSION['uid'], ":cat" => $cat_id]); |
||
646 | } |
||
647 | |||
648 | while ($line = $sth->fetch()) { |
||
649 | |||
650 | $unread = getFeedUnread($line["id"]); |
||
651 | |||
652 | $has_icon = Feeds::feedHasIcon($line['id']); |
||
653 | |||
654 | if ($unread || !$unread_only) { |
||
655 | |||
656 | $row = array( |
||
657 | "feed_url" => $line["feed_url"], |
||
658 | "title" => $line["title"], |
||
659 | "id" => (int) $line["id"], |
||
660 | "unread" => (int) $unread, |
||
661 | "has_icon" => $has_icon, |
||
662 | "cat_id" => (int) $line["cat_id"], |
||
663 | "last_updated" => (int) strtotime($line["last_updated"]), |
||
664 | "order_id" => (int) $line["order_id"], |
||
665 | ); |
||
666 | |||
667 | array_push($feeds, $row); |
||
668 | } |
||
669 | } |
||
670 | |||
671 | return $feeds; |
||
672 | } |
||
673 | |||
674 | /** |
||
675 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
||
676 | */ |
||
677 | public static function api_get_headlines($feed_id, $limit, $offset, |
||
678 | $filter, $is_cat, $show_excerpt, $show_content, $view_mode, $order, |
||
679 | $include_attachments, $since_id, |
||
680 | $search = "", $include_nested = false, $sanitize_content = true, |
||
681 | $force_update = false, $excerpt_length = 100, $check_first_id = false, $skip_first_id_check = false) { |
||
682 | |||
683 | $pdo = Db::pdo(); |
||
684 | |||
685 | if ($force_update && $feed_id > 0 && is_numeric($feed_id)) { |
||
686 | // Update the feed if required with some basic flood control |
||
687 | |||
688 | $sth = $pdo->prepare( |
||
689 | "SELECT cache_images,".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated |
||
690 | FROM ttrss_feeds WHERE id = ?"); |
||
691 | $sth->execute([$feed_id]); |
||
692 | |||
693 | if ($row = $sth->fetch()) { |
||
694 | $last_updated = strtotime($row["last_updated"]); |
||
695 | $cache_images = API::param_to_bool($row["cache_images"]); |
||
696 | |||
697 | if (!$cache_images && time() - $last_updated > 120) { |
||
698 | RSSUtils::update_rss_feed($feed_id, true); |
||
699 | } else { |
||
700 | $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_updated = '1970-01-01', last_update_started = '1970-01-01' |
||
701 | WHERE id = ?"); |
||
702 | $sth->execute([$feed_id]); |
||
703 | } |
||
704 | } |
||
705 | } |
||
706 | |||
707 | $params = array( |
||
708 | "feed" => $feed_id, |
||
709 | "limit" => $limit, |
||
710 | "view_mode" => $view_mode, |
||
711 | "cat_view" => $is_cat, |
||
712 | "search" => $search, |
||
713 | "override_order" => $order, |
||
714 | "offset" => $offset, |
||
715 | "since_id" => $since_id, |
||
716 | "include_children" => $include_nested, |
||
717 | "check_first_id" => $check_first_id, |
||
718 | "skip_first_id_check" => $skip_first_id_check |
||
719 | ); |
||
720 | |||
721 | $qfh_ret = Feeds::queryFeedHeadlines($params); |
||
722 | |||
723 | $result = $qfh_ret[0]; |
||
724 | $feed_title = $qfh_ret[1]; |
||
725 | $first_id = $qfh_ret[6]; |
||
726 | |||
727 | $headlines = array(); |
||
728 | |||
729 | $headlines_header = array( |
||
730 | 'id' => $feed_id, |
||
731 | 'first_id' => $first_id, |
||
732 | 'is_cat' => $is_cat); |
||
733 | |||
734 | if (!is_numeric($result)) { |
||
735 | while ($line = $result->fetch()) { |
||
736 | $line["content_preview"] = truncate_string(strip_tags($line["content"]), $excerpt_length); |
||
737 | foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) { |
||
738 | $line = $p->hook_query_headlines($line, $excerpt_length, true); |
||
739 | } |
||
740 | |||
741 | $is_updated = ($line["last_read"] == "" && |
||
742 | ($line["unread"] != "t" && $line["unread"] != "1")); |
||
743 | |||
744 | $tags = explode(",", $line["tag_cache"]); |
||
745 | |||
746 | $label_cache = $line["label_cache"]; |
||
747 | $labels = false; |
||
748 | |||
749 | if ($label_cache) { |
||
750 | $label_cache = json_decode($label_cache, true); |
||
751 | |||
752 | if ($label_cache) { |
||
753 | if ($label_cache["no-labels"] == 1) { |
||
754 | $labels = array(); |
||
755 | } else { |
||
756 | $labels = $label_cache; |
||
757 | } |
||
758 | } |
||
759 | } |
||
760 | |||
761 | if (!is_array($labels)) { |
||
762 | $labels = Article::get_article_labels($line["id"]); |
||
763 | } |
||
764 | |||
765 | $headline_row = array( |
||
766 | "id" => (int) $line["id"], |
||
767 | "guid" => $line["guid"], |
||
768 | "unread" => API::param_to_bool($line["unread"]), |
||
769 | "marked" => API::param_to_bool($line["marked"]), |
||
770 | "published" => API::param_to_bool($line["published"]), |
||
771 | "updated" => (int) strtotime($line["updated"]), |
||
772 | "is_updated" => $is_updated, |
||
773 | "title" => $line["title"], |
||
774 | "link" => $line["link"], |
||
775 | "feed_id" => $line["feed_id"] ? $line['feed_id'] : 0, |
||
776 | "tags" => $tags, |
||
777 | ); |
||
778 | |||
779 | $enclosures = Article::get_article_enclosures($line['id']); |
||
780 | |||
781 | if ($include_attachments) { |
||
782 | $headline_row['attachments'] = $enclosures; |
||
783 | } |
||
784 | |||
785 | if ($show_excerpt) { |
||
786 | $headline_row["excerpt"] = $line["content_preview"]; |
||
787 | } |
||
788 | |||
789 | if ($show_content) { |
||
790 | |||
791 | if ($sanitize_content) { |
||
792 | $headline_row["content"] = sanitize( |
||
793 | $line["content"], |
||
794 | API::param_to_bool($line['hide_images']), |
||
795 | false, $line["site_url"], false, $line["id"]); |
||
796 | } else { |
||
797 | $headline_row["content"] = $line["content"]; |
||
798 | } |
||
799 | } |
||
800 | |||
801 | // unify label output to ease parsing |
||
802 | if ($labels["no-labels"] == 1) { |
||
803 | $labels = array(); |
||
804 | } |
||
805 | |||
806 | $headline_row["labels"] = $labels; |
||
807 | |||
808 | $headline_row["feed_title"] = $line["feed_title"] ? $line["feed_title"] : $feed_title; |
||
809 | |||
810 | $headline_row["comments_count"] = (int) $line["num_comments"]; |
||
811 | $headline_row["comments_link"] = $line["comments"]; |
||
812 | |||
813 | $headline_row["always_display_attachments"] = API::param_to_bool($line["always_display_enclosures"]); |
||
814 | |||
815 | $headline_row["author"] = $line["author"]; |
||
816 | |||
817 | $headline_row["score"] = (int) $line["score"]; |
||
818 | $headline_row["note"] = $line["note"]; |
||
819 | $headline_row["lang"] = $line["lang"]; |
||
820 | |||
821 | foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE_API) as $p) { |
||
822 | $headline_row = $p->hook_render_article_api(array("headline" => $headline_row)); |
||
823 | } |
||
824 | |||
825 | $headline_row["content"] = DiskCache::rewriteUrls($headline_row['content']); |
||
826 | |||
827 | list ($flavor_image, $flavor_stream) = Article::get_article_image($enclosures, $line["content"], $line["site_url"]); |
||
828 | |||
829 | $headline_row["flavor_image"] = $flavor_image; |
||
830 | $headline_row["flavor_stream"] = $flavor_stream; |
||
831 | |||
832 | array_push($headlines, $headline_row); |
||
833 | } |
||
834 | } else if (is_numeric($result) && $result == -1) { |
||
835 | $headlines_header['first_id_changed'] = true; |
||
836 | } |
||
837 | |||
838 | return array($headlines, $headlines_header); |
||
839 | } |
||
840 | |||
841 | public function unsubscribeFeed() { |
||
842 | $feed_id = (int) clean($_REQUEST["feed_id"]); |
||
843 | |||
844 | $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE |
||
845 | id = ? AND owner_uid = ?"); |
||
846 | $sth->execute([$feed_id, $_SESSION['uid']]); |
||
847 | |||
848 | if ($sth->fetch()) { |
||
849 | Pref_Feeds::remove_feed($feed_id, $_SESSION["uid"]); |
||
850 | $this->wrap(self::STATUS_OK, array("status" => "OK")); |
||
851 | } else { |
||
852 | $this->wrap(self::STATUS_ERR, array("error" => "FEED_NOT_FOUND")); |
||
853 | } |
||
854 | } |
||
855 | |||
856 | public function subscribeToFeed() { |
||
857 | $feed_url = clean($_REQUEST["feed_url"]); |
||
858 | $category_id = (int) clean($_REQUEST["category_id"]); |
||
859 | $login = clean($_REQUEST["login"]); |
||
860 | $password = clean($_REQUEST["password"]); |
||
861 | |||
862 | if ($feed_url) { |
||
863 | $rc = Feeds::subscribe_to_feed($feed_url, $category_id, $login, $password); |
||
864 | |||
865 | $this->wrap(self::STATUS_OK, array("status" => $rc)); |
||
866 | } else { |
||
867 | $this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE')); |
||
868 | } |
||
869 | } |
||
870 | |||
871 | public function getFeedTree() { |
||
872 | $include_empty = API::param_to_bool(clean($_REQUEST['include_empty'])); |
||
873 | |||
874 | $pf = new Pref_Feeds($_REQUEST); |
||
875 | |||
876 | $_REQUEST['mode'] = 2; |
||
877 | $_REQUEST['force_show_empty'] = $include_empty; |
||
878 | |||
879 | if ($pf) { |
||
880 | $data = $pf->makefeedtree(); |
||
881 | $this->wrap(self::STATUS_OK, array("categories" => $data)); |
||
882 | } else { |
||
883 | $this->wrap(self::STATUS_ERR, array("error" => |
||
884 | 'UNABLE_TO_INSTANTIATE_OBJECT')); |
||
885 | } |
||
886 | |||
887 | } |
||
888 | |||
889 | // only works for labels or uncategorized for the time being |
||
890 | private function isCategoryEmpty($id) { |
||
891 | |||
892 | if ($id == -2) { |
||
893 | $sth = $this->pdo->prepare("SELECT COUNT(id) AS count FROM ttrss_labels2 |
||
894 | WHERE owner_uid = ?"); |
||
895 | $sth->execute([$_SESSION['uid']]); |
||
896 | $row = $sth->fetch(); |
||
897 | |||
898 | return $row["count"] == 0; |
||
899 | |||
900 | } else if ($id == 0) { |
||
901 | $sth = $this->pdo->prepare("SELECT COUNT(id) AS count FROM ttrss_feeds |
||
902 | WHERE cat_id IS NULL AND owner_uid = ?"); |
||
903 | $sth->execute([$_SESSION['uid']]); |
||
904 | $row = $sth->fetch(); |
||
905 | |||
906 | return $row["count"] == 0; |
||
907 | |||
908 | } |
||
909 | |||
910 | return false; |
||
911 | } |
||
912 | |||
913 | |||
914 | } |
||
915 |