]> git.wh0rd.org - tt-rss.git/blob - include/functions2.php
b8900e78be0c9697380d9daec812afcf6ebb4ca1
[tt-rss.git] / include / functions2.php
1 <?php
2 function make_init_params() {
3 $params = array();
4
5 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
6 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
7 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
8 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
9
10 $params[strtolower($param)] = (int) get_pref($param);
11 }
12
13 $params["icons_url"] = ICONS_URL;
14 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
15 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
16 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
17 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
18 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
19 $params["label_base_index"] = (int) LABEL_BASE_INDEX;
20
21 $theme = get_pref( "USER_CSS_THEME", false, false);
22 $params["theme"] = theme_valid("$theme") ? $theme : "";
23
24 $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
25
26 $params["php_platform"] = PHP_OS;
27 $params["php_version"] = PHP_VERSION;
28
29 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
30
31 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
32 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
33
34 $max_feed_id = db_fetch_result($result, 0, "mid");
35 $num_feeds = db_fetch_result($result, 0, "nf");
36
37 $params["max_feed_id"] = (int) $max_feed_id;
38 $params["num_feeds"] = (int) $num_feeds;
39
40 $params["hotkeys"] = get_hotkeys_map();
41
42 $params["csrf_token"] = $_SESSION["csrf_token"];
43 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
44
45 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE;
46
47 $params["icon_alert"] = base64_img("images/alert.png");
48 $params["icon_information"] = base64_img("images/information.png");
49 $params["icon_cross"] = base64_img("images/cross.png");
50 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
51
52 return $params;
53 }
54
55 function get_hotkeys_info() {
56 $hotkeys = array(
57 __("Navigation") => array(
58 "next_feed" => __("Open next feed"),
59 "prev_feed" => __("Open previous feed"),
60 "next_article" => __("Open next article"),
61 "prev_article" => __("Open previous article"),
62 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
63 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
64 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
65 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
66 "search_dialog" => __("Show search dialog")),
67 __("Article") => array(
68 "toggle_mark" => __("Toggle starred"),
69 "toggle_publ" => __("Toggle published"),
70 "toggle_unread" => __("Toggle unread"),
71 "edit_tags" => __("Edit tags"),
72 "open_in_new_window" => __("Open in new window"),
73 "catchup_below" => __("Mark below as read"),
74 "catchup_above" => __("Mark above as read"),
75 "article_scroll_down" => __("Scroll down"),
76 "article_scroll_up" => __("Scroll up"),
77 "select_article_cursor" => __("Select article under cursor"),
78 "email_article" => __("Email article"),
79 "close_article" => __("Close/collapse article"),
80 "toggle_expand" => __("Toggle article expansion (combined mode)"),
81 "toggle_widescreen" => __("Toggle widescreen mode"),
82 "toggle_embed_original" => __("Toggle embed original")),
83 __("Article selection") => array(
84 "select_all" => __("Select all articles"),
85 "select_unread" => __("Select unread"),
86 "select_marked" => __("Select starred"),
87 "select_published" => __("Select published"),
88 "select_invert" => __("Invert selection"),
89 "select_none" => __("Deselect everything")),
90 __("Feed") => array(
91 "feed_refresh" => __("Refresh current feed"),
92 "feed_unhide_read" => __("Un/hide read feeds"),
93 "feed_subscribe" => __("Subscribe to feed"),
94 "feed_edit" => __("Edit feed"),
95 "feed_catchup" => __("Mark as read"),
96 "feed_reverse" => __("Reverse headlines"),
97 "feed_toggle_vgroup" => __("Toggle headline grouping"),
98 "feed_debug_update" => __("Debug feed update"),
99 "feed_debug_viewfeed" => __("Debug viewfeed()"),
100 "catchup_all" => __("Mark all feeds as read"),
101 "cat_toggle_collapse" => __("Un/collapse current category"),
102 "toggle_combined_mode" => __("Toggle combined mode"),
103 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
104 __("Go to") => array(
105 "goto_all" => __("All articles"),
106 "goto_fresh" => __("Fresh"),
107 "goto_marked" => __("Starred"),
108 "goto_published" => __("Published"),
109 "goto_tagcloud" => __("Tag cloud"),
110 "goto_prefs" => __("Preferences")),
111 __("Other") => array(
112 "create_label" => __("Create label"),
113 "create_filter" => __("Create filter"),
114 "collapse_sidebar" => __("Un/collapse sidebar"),
115 "help_dialog" => __("Show help dialog"))
116 );
117
118 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
119 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
120 }
121
122 return $hotkeys;
123 }
124
125 function get_hotkeys_map() {
126 $hotkeys = array(
127 // "navigation" => array(
128 "k" => "next_feed",
129 "j" => "prev_feed",
130 "n" => "next_article",
131 "p" => "prev_article",
132 "(38)|up" => "prev_article",
133 "(40)|down" => "next_article",
134 // "^(38)|Ctrl-up" => "prev_article_noscroll",
135 // "^(40)|Ctrl-down" => "next_article_noscroll",
136 "(191)|/" => "search_dialog",
137 // "article" => array(
138 "s" => "toggle_mark",
139 "*s" => "toggle_publ",
140 "u" => "toggle_unread",
141 "*t" => "edit_tags",
142 "o" => "open_in_new_window",
143 "c p" => "catchup_below",
144 "c n" => "catchup_above",
145 "*n" => "article_scroll_down",
146 "*p" => "article_scroll_up",
147 "*(38)|Shift+up" => "article_scroll_up",
148 "*(40)|Shift+down" => "article_scroll_down",
149 "a *w" => "toggle_widescreen",
150 "a e" => "toggle_embed_original",
151 "e" => "email_article",
152 "a q" => "close_article",
153 // "article_selection" => array(
154 "a a" => "select_all",
155 "a u" => "select_unread",
156 "a *u" => "select_marked",
157 "a p" => "select_published",
158 "a i" => "select_invert",
159 "a n" => "select_none",
160 // "feed" => array(
161 "f r" => "feed_refresh",
162 "f a" => "feed_unhide_read",
163 "f s" => "feed_subscribe",
164 "f e" => "feed_edit",
165 "f q" => "feed_catchup",
166 "f x" => "feed_reverse",
167 "f g" => "feed_toggle_vgroup",
168 "f *d" => "feed_debug_update",
169 "f *g" => "feed_debug_viewfeed",
170 "f *c" => "toggle_combined_mode",
171 "f c" => "toggle_cdm_expanded",
172 "*q" => "catchup_all",
173 "x" => "cat_toggle_collapse",
174 // "goto" => array(
175 "g a" => "goto_all",
176 "g f" => "goto_fresh",
177 "g s" => "goto_marked",
178 "g p" => "goto_published",
179 "g t" => "goto_tagcloud",
180 "g *p" => "goto_prefs",
181 // "other" => array(
182 "(9)|Tab" => "select_article_cursor", // tab
183 "c l" => "create_label",
184 "c f" => "create_filter",
185 "c s" => "collapse_sidebar",
186 "^(191)|Ctrl+/" => "help_dialog",
187 );
188
189 if (get_pref('COMBINED_DISPLAY_MODE')) {
190 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
191 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
192 }
193
194 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
195 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
196 }
197
198 $prefixes = array();
199
200 foreach (array_keys($hotkeys) as $hotkey) {
201 $pair = explode(" ", $hotkey, 2);
202
203 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
204 array_push($prefixes, $pair[0]);
205 }
206 }
207
208 return array($prefixes, $hotkeys);
209 }
210
211 function check_for_update() {
212 if (defined("GIT_VERSION_TIMESTAMP")) {
213 $content = @fetch_file_contents(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
214
215 if ($content) {
216 $content = json_decode($content, true);
217
218 if ($content && isset($content["changeset"])) {
219 if ((int)GIT_VERSION_TIMESTAMP < (int)$content["changeset"]["timestamp"] &&
220 GIT_VERSION_HEAD != $content["changeset"]["id"]) {
221
222 return $content["changeset"]["id"];
223 }
224 }
225 }
226 }
227
228 return "";
229 }
230
231 function make_runtime_info($disable_update_check = false) {
232 $data = array();
233
234 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
235 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
236
237 $max_feed_id = db_fetch_result($result, 0, "mid");
238 $num_feeds = db_fetch_result($result, 0, "nf");
239
240 $data["max_feed_id"] = (int) $max_feed_id;
241 $data["num_feeds"] = (int) $num_feeds;
242
243 $data['last_article_id'] = getLastArticleId();
244 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
245
246 $data['dep_ts'] = calculate_dep_timestamp();
247 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
248
249
250 if (CHECK_FOR_UPDATES && !$disable_update_check && $_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
251 $update_result = @check_for_update();
252
253 $data["update_result"] = $update_result;
254
255 $_SESSION["last_version_check"] = time();
256 }
257
258 if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
259
260 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
261
262 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
263
264 $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
265
266 if ($stamp) {
267 $stamp_delta = time() - $stamp;
268
269 if ($stamp_delta > 1800) {
270 $stamp_check = 0;
271 } else {
272 $stamp_check = 1;
273 $_SESSION["daemon_stamp_check"] = time();
274 }
275
276 $data['daemon_stamp_ok'] = $stamp_check;
277
278 $stamp_fmt = date("Y.m.d, G:i", $stamp);
279
280 $data['daemon_stamp'] = $stamp_fmt;
281 }
282 }
283 }
284
285 return $data;
286 }
287
288 function search_to_sql($search, $search_language) {
289
290 $keywords = str_getcsv(trim($search), " ");
291 $query_keywords = array();
292 $search_words = array();
293 $search_query_leftover = array();
294
295 if ($search_language)
296 $search_language = db_escape_string(mb_strtolower($search_language));
297 else
298 $search_language = "english";
299
300 foreach ($keywords as $k) {
301 if (strpos($k, "-") === 0) {
302 $k = substr($k, 1);
303 $not = "NOT";
304 } else {
305 $not = "";
306 }
307
308 $commandpair = explode(":", mb_strtolower($k), 2);
309
310 switch ($commandpair[0]) {
311 case "title":
312 if ($commandpair[1]) {
313 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE '%".
314 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
315 } else {
316 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
317 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
318 array_push($search_words, $k);
319 }
320 break;
321 case "author":
322 if ($commandpair[1]) {
323 array_push($query_keywords, "($not (LOWER(author) LIKE '%".
324 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
325 } else {
326 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
327 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
328 array_push($search_words, $k);
329 }
330 break;
331 case "note":
332 if ($commandpair[1]) {
333 if ($commandpair[1] == "true")
334 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
335 else if ($commandpair[1] == "false")
336 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
337 else
338 array_push($query_keywords, "($not (LOWER(note) LIKE '%".
339 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
340 } else {
341 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
342 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
343 if (!$not) array_push($search_words, $k);
344 }
345 break;
346 case "star":
347
348 if ($commandpair[1]) {
349 if ($commandpair[1] == "true")
350 array_push($query_keywords, "($not (marked = true))");
351 else
352 array_push($query_keywords, "($not (marked = false))");
353 } else {
354 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
355 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
356 if (!$not) array_push($search_words, $k);
357 }
358 break;
359 case "pub":
360 if ($commandpair[1]) {
361 if ($commandpair[1] == "true")
362 array_push($query_keywords, "($not (published = true))");
363 else
364 array_push($query_keywords, "($not (published = false))");
365
366 } else {
367 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
368 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
369 if (!$not) array_push($search_words, $k);
370 }
371 break;
372 case "unread":
373 if ($commandpair[1]) {
374 if ($commandpair[1] == "true")
375 array_push($query_keywords, "($not (unread = true))");
376 else
377 array_push($query_keywords, "($not (unread = false))");
378
379 } else {
380 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
381 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
382 if (!$not) array_push($search_words, $k);
383 }
384 break;
385 default:
386 if (strpos($k, "@") === 0) {
387
388 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
389 $orig_ts = strtotime(substr($k, 1));
390 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
391
392 //$k = date("Y-m-d", strtotime(substr($k, 1)));
393
394 array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
395 } else {
396
397 if (DB_TYPE == "pgsql") {
398 $k = mb_strtolower($k);
399 array_push($search_query_leftover, $not ? "!$k" : $k);
400 } else {
401 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
402 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
403 }
404
405 if (!$not) array_push($search_words, $k);
406 }
407 }
408 }
409
410 if (count($search_query_leftover) > 0) {
411 $search_query_leftover = db_escape_string(implode(" & ", $search_query_leftover));
412
413 if (DB_TYPE == "pgsql") {
414 array_push($query_keywords,
415 "(tsvector_combined @@ to_tsquery('$search_language', '$search_query_leftover'))");
416 }
417
418 }
419
420 $search_query_part = implode("AND", $query_keywords);
421
422 return array($search_query_part, $search_words);
423 }
424
425 function getParentCategories($cat, $owner_uid) {
426 $rv = array();
427
428 $result = db_query("SELECT parent_cat FROM ttrss_feed_categories
429 WHERE id = '$cat' AND parent_cat IS NOT NULL AND owner_uid = $owner_uid");
430
431 while ($line = db_fetch_assoc($result)) {
432 array_push($rv, $line["parent_cat"]);
433 $rv = array_merge($rv, getParentCategories($line["parent_cat"], $owner_uid));
434 }
435
436 return $rv;
437 }
438
439 function getChildCategories($cat, $owner_uid) {
440 $rv = array();
441
442 $result = db_query("SELECT id FROM ttrss_feed_categories
443 WHERE parent_cat = '$cat' AND owner_uid = $owner_uid");
444
445 while ($line = db_fetch_assoc($result)) {
446 array_push($rv, $line["id"]);
447 $rv = array_merge($rv, getChildCategories($line["id"], $owner_uid));
448 }
449
450 return $rv;
451 }
452
453 function queryFeedHeadlines($params) {
454
455 $feed = $params["feed"];
456 $limit = isset($params["limit"]) ? $params["limit"] : 30;
457 $view_mode = $params["view_mode"];
458 $cat_view = isset($params["cat_view"]) ? $params["cat_view"] : false;
459 $search = isset($params["search"]) ? $params["search"] : false;
460 $search_language = isset($params["search_language"]) ? $params["search_language"] : "";
461 $override_order = isset($params["override_order"]) ? $params["override_order"] : false;
462 $offset = isset($params["offset"]) ? $params["offset"] : 0;
463 $owner_uid = isset($params["owner_uid"]) ? $params["owner_uid"] : $_SESSION["uid"];
464 $since_id = isset($params["since_id"]) ? $params["since_id"] : 0;
465 $include_children = isset($params["include_children"]) ? $params["include_children"] : false;
466 $ignore_vfeed_group = isset($params["ignore_vfeed_group"]) ? $params["ignore_vfeed_group"] : false;
467 $override_strategy = isset($params["override_strategy"]) ? $params["override_strategy"] : false;
468 $override_vfeed = isset($params["override_vfeed"]) ? $params["override_vfeed"] : false;
469 $start_ts = isset($params["start_ts"]) ? $params["start_ts"] : false;
470 $check_first_id = isset($params["check_first_id"]) ? $params["check_first_id"] : false;
471 $skip_first_id_check = isset($params["skip_first_id_check"]) ? $params["skip_first_id_check"] : false;
472
473 $ext_tables_part = "";
474 $query_strategy_part = "";
475
476 $search_words = array();
477
478 if ($search) {
479 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH) as $plugin) {
480 list($search_query_part, $search_words) = $plugin->hook_search($search);
481 break;
482 }
483
484 // fall back in case of no plugins
485 if (!$search_query_part) {
486 list($search_query_part, $search_words) = search_to_sql($search, $search_language);
487 }
488 $search_query_part .= " AND ";
489 } else {
490 $search_query_part = "";
491 }
492
493 if ($since_id) {
494 $since_id_part = "ttrss_entries.id > $since_id AND ";
495 } else {
496 $since_id_part = "";
497 }
498
499 $view_query_part = "";
500
501 if ($view_mode == "adaptive") {
502 if ($search) {
503 $view_query_part = " ";
504 } else if ($feed != -1) {
505
506 $unread = getFeedUnread($feed, $cat_view);
507
508 if ($cat_view && $feed > 0 && $include_children)
509 $unread += getCategoryChildrenUnread($feed);
510
511 if ($unread > 0) {
512 $view_query_part = " unread = true AND ";
513 }
514 }
515 }
516
517 if ($view_mode == "marked") {
518 $view_query_part = " marked = true AND ";
519 }
520
521 if ($view_mode == "has_note") {
522 $view_query_part = " (note IS NOT NULL AND note != '') AND ";
523 }
524
525 if ($view_mode == "published") {
526 $view_query_part = " published = true AND ";
527 }
528
529 if ($view_mode == "unread" && $feed != -6) {
530 $view_query_part = " unread = true AND ";
531 }
532
533 if ($limit > 0) {
534 $limit_query_part = "LIMIT " . $limit;
535 }
536
537 $allow_archived = false;
538
539 $vfeed_query_part = "";
540
541 /* tags */
542 if (!is_numeric($feed)) {
543 $query_strategy_part = "true";
544 $vfeed_query_part = "(SELECT title FROM ttrss_feeds WHERE
545 id = feed_id) as feed_title,";
546 } else if ($feed > 0) {
547
548 if ($cat_view) {
549
550 if ($feed > 0) {
551 if ($include_children) {
552 # sub-cats
553 $subcats = getChildCategories($feed, $owner_uid);
554
555 array_push($subcats, $feed);
556 $query_strategy_part = "cat_id IN (".
557 implode(",", $subcats).")";
558
559 } else {
560 $query_strategy_part = "cat_id = '$feed'";
561 }
562
563 } else {
564 $query_strategy_part = "cat_id IS NULL";
565 }
566
567 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
568
569 } else {
570 $query_strategy_part = "feed_id = '$feed'";
571 }
572 } else if ($feed == 0 && !$cat_view) { // archive virtual feed
573 $query_strategy_part = "feed_id IS NULL";
574 $allow_archived = true;
575 } else if ($feed == 0 && $cat_view) { // uncategorized
576 $query_strategy_part = "cat_id IS NULL AND feed_id IS NOT NULL";
577 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
578 } else if ($feed == -1) { // starred virtual feed
579 $query_strategy_part = "marked = true";
580 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
581 $allow_archived = true;
582
583 if (!$override_order) {
584 $override_order = "last_marked DESC, date_entered DESC, updated DESC";
585 }
586
587 } else if ($feed == -2) { // published virtual feed OR labels category
588
589 if (!$cat_view) {
590 $query_strategy_part = "published = true";
591 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
592 $allow_archived = true;
593
594 if (!$override_order) {
595 $override_order = "last_published DESC, date_entered DESC, updated DESC";
596 }
597
598 } else {
599 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
600
601 $ext_tables_part = "ttrss_labels2,ttrss_user_labels2,";
602
603 $query_strategy_part = "ttrss_labels2.id = ttrss_user_labels2.label_id AND
604 ttrss_user_labels2.article_id = ref_id";
605
606 }
607 } else if ($feed == -6) { // recently read
608 $query_strategy_part = "unread = false AND last_read IS NOT NULL";
609
610 if (DB_TYPE == "pgsql") {
611 $query_strategy_part .= " AND last_read > NOW() - INTERVAL '1 DAY' ";
612 } else {
613 $query_strategy_part .= " AND last_read > DATE_SUB(NOW(), INTERVAL 1 DAY) ";
614 }
615
616 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
617 $allow_archived = true;
618 $ignore_vfeed_group = true;
619
620 if (!$override_order) $override_order = "last_read DESC";
621
622 } else if ($feed == -3) { // fresh virtual feed
623 $query_strategy_part = "unread = true AND score >= 0";
624
625 $intl = get_pref("FRESH_ARTICLE_MAX_AGE", $owner_uid);
626
627 if (DB_TYPE == "pgsql") {
628 $query_strategy_part .= " AND date_entered > NOW() - INTERVAL '$intl hour' ";
629 } else {
630 $query_strategy_part .= " AND date_entered > DATE_SUB(NOW(), INTERVAL $intl HOUR) ";
631 }
632
633 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
634 } else if ($feed == -4) { // all articles virtual feed
635 $allow_archived = true;
636 $query_strategy_part = "true";
637 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
638 } else if ($feed <= LABEL_BASE_INDEX) { // labels
639 $label_id = feed_to_label_id($feed);
640
641 $query_strategy_part = "label_id = '$label_id' AND
642 ttrss_labels2.id = ttrss_user_labels2.label_id AND
643 ttrss_user_labels2.article_id = ref_id";
644
645 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
646 $ext_tables_part = "ttrss_labels2,ttrss_user_labels2,";
647 $allow_archived = true;
648
649 } else {
650 $query_strategy_part = "true";
651 }
652
653 $order_by = "score DESC, date_entered DESC, updated DESC";
654
655 if ($override_order) {
656 $order_by = $override_order;
657 }
658
659 if ($override_strategy) {
660 $query_strategy_part = $override_strategy;
661 }
662
663 if ($override_vfeed) {
664 $vfeed_query_part = $override_vfeed;
665 }
666
667 $feed_title = "";
668
669 if ($search) {
670 $feed_title = T_sprintf("Search results: %s", $search);
671 } else {
672 if ($cat_view) {
673 $feed_title = getCategoryTitle($feed);
674 } else {
675 if (is_numeric($feed) && $feed > 0) {
676 $result = db_query("SELECT title,site_url,last_error,last_updated
677 FROM ttrss_feeds WHERE id = '$feed' AND owner_uid = $owner_uid");
678
679 $feed_title = db_fetch_result($result, 0, "title");
680 $feed_site_url = db_fetch_result($result, 0, "site_url");
681 $last_error = db_fetch_result($result, 0, "last_error");
682 $last_updated = db_fetch_result($result, 0, "last_updated");
683 } else {
684 $feed_title = getFeedTitle($feed);
685 }
686 }
687 }
688
689
690 $content_query_part = "content, ";
691
692 if ($limit_query_part) {
693 $offset_query_part = "OFFSET $offset";
694 } else {
695 $offset_query_part = "";
696 }
697
698 if (is_numeric($feed)) {
699 // proper override_order applied above
700 if ($vfeed_query_part && !$ignore_vfeed_group && get_pref('VFEED_GROUP_BY_FEED', $owner_uid)) {
701 if (!$override_order) {
702 $order_by = "ttrss_feeds.title, $order_by";
703 } else {
704 $order_by = "ttrss_feeds.title, $override_order";
705 }
706 }
707
708 if (!$allow_archived) {
709 $from_qpart = "${ext_tables_part}ttrss_entries LEFT JOIN ttrss_user_entries ON (ref_id = ttrss_entries.id),ttrss_feeds";
710 $feed_check_qpart = "ttrss_user_entries.feed_id = ttrss_feeds.id AND";
711
712 } else {
713 $from_qpart = "${ext_tables_part}ttrss_entries LEFT JOIN ttrss_user_entries ON (ref_id = ttrss_entries.id)
714 LEFT JOIN ttrss_feeds ON (feed_id = ttrss_feeds.id)";
715 }
716
717 if ($vfeed_query_part) $vfeed_query_part .= "favicon_avg_color,";
718
719 if ($start_ts) {
720 $start_ts_formatted = date("Y/m/d H:i:s", strtotime($start_ts));
721 $start_ts_query_part = "date_entered >= '$start_ts_formatted' AND";
722 } else {
723 $start_ts_query_part = "";
724 }
725
726 $first_id = 0;
727 $first_id_query_strategy_part = $query_strategy_part;
728
729 if ($feed == -3)
730 $first_id_query_strategy_part = "true";
731
732 if (DB_TYPE == "pgsql") {
733 $sanity_interval_qpart = "date_entered >= NOW() - INTERVAL '1 hour' AND";
734 } else {
735 $sanity_interval_qpart = "date_entered >= DATE_SUB(NOW(), INTERVAL 1 hour) AND";
736 }
737
738 if (!$search && !$skip_first_id_check) {
739 // if previous topmost article id changed that means our current pagination is no longer valid
740 $query = "SELECT DISTINCT
741 ttrss_feeds.title,
742 date_entered,
743 guid,
744 ttrss_entries.id,
745 ttrss_entries.title,
746 updated,
747 score,
748 marked,
749 published,
750 last_marked,
751 last_published,
752 last_read
753 FROM
754 $from_qpart
755 WHERE
756 $feed_check_qpart
757 ttrss_user_entries.owner_uid = '$owner_uid' AND
758 $search_query_part
759 $start_ts_query_part
760 $since_id_part
761 $sanity_interval_qpart
762 $first_id_query_strategy_part ORDER BY $order_by LIMIT 1";
763
764 if ($_REQUEST["debug"]) {
765 print $query;
766 }
767
768 $result = db_query($query);
769 if ($result && db_num_rows($result) > 0) {
770 $first_id = (int)db_fetch_result($result, 0, "id");
771
772 if ($offset > 0 && $first_id && $check_first_id && $first_id != $check_first_id) {
773 return array(-1, $feed_title, $feed_site_url, $last_error, $last_updated, $search_words, $first_id);
774 }
775 }
776 }
777
778 $query = "SELECT DISTINCT
779 date_entered,
780 guid,
781 ttrss_entries.id,ttrss_entries.title,
782 updated,
783 label_cache,
784 tag_cache,
785 always_display_enclosures,
786 site_url,
787 note,
788 num_comments,
789 comments,
790 int_id,
791 uuid,
792 lang,
793 hide_images,
794 unread,feed_id,marked,published,link,last_read,orig_feed_id,
795 last_marked, last_published,
796 $vfeed_query_part
797 $content_query_part
798 author,score
799 FROM
800 $from_qpart
801 WHERE
802 $feed_check_qpart
803 ttrss_user_entries.owner_uid = '$owner_uid' AND
804 $search_query_part
805 $start_ts_query_part
806 $view_query_part
807 $since_id_part
808 $query_strategy_part ORDER BY $order_by
809 $limit_query_part $offset_query_part";
810
811 if ($_REQUEST["debug"]) print $query;
812
813 $result = db_query($query);
814
815 } else {
816 // browsing by tag
817
818 $query = "SELECT DISTINCT
819 date_entered,
820 guid,
821 note,
822 ttrss_entries.id as id,
823 title,
824 updated,
825 unread,
826 feed_id,
827 orig_feed_id,
828 marked,
829 num_comments,
830 comments,
831 int_id,
832 tag_cache,
833 label_cache,
834 link,
835 lang,
836 uuid,
837 last_read,
838 (SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) AS hide_images,
839 last_marked, last_published,
840 $since_id_part
841 $vfeed_query_part
842 $content_query_part
843 author, score
844 FROM ttrss_entries, ttrss_user_entries, ttrss_tags
845 WHERE
846 ref_id = ttrss_entries.id AND
847 ttrss_user_entries.owner_uid = $owner_uid AND
848 post_int_id = int_id AND
849 tag_name = '$feed' AND
850 $view_query_part
851 $search_query_part
852 $query_strategy_part ORDER BY $order_by
853 $limit_query_part $offset_query_part";
854
855 if ($_REQUEST["debug"]) print $query;
856
857 $result = db_query($query);
858 }
859
860 return array($result, $feed_title, $feed_site_url, $last_error, $last_updated, $search_words, $first_id);
861
862 }
863
864 function iframe_whitelisted($entry) {
865 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
866
867 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
868
869 if ($src) {
870 foreach ($whitelist as $w) {
871 if ($src == $w || $src == "www.$w")
872 return true;
873 }
874 }
875
876 return false;
877 }
878
879 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
880 if (!$owner) $owner = $_SESSION["uid"];
881
882 $res = trim($str); if (!$res) return '';
883
884 $charset_hack = '<head>
885 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
886 </head>';
887
888 $res = trim($res); if (!$res) return '';
889
890 libxml_use_internal_errors(true);
891
892 $doc = new DOMDocument();
893 $doc->loadHTML($charset_hack . $res);
894 $xpath = new DOMXPath($doc);
895
896 $ttrss_uses_https = parse_url(get_self_url_prefix(), PHP_URL_SCHEME) === 'https';
897 $rewrite_base_url = $site_url ? $site_url : SELF_URL_PATH;
898
899 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
900
901 foreach ($entries as $entry) {
902
903 if ($entry->hasAttribute('href')) {
904 $entry->setAttribute('href',
905 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
906
907 $entry->setAttribute('rel', 'noopener noreferrer');
908 }
909
910 if ($entry->hasAttribute('src')) {
911 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
912 $cached_filename = CACHE_DIR . '/images/' . sha1($src);
913
914 if (file_exists($cached_filename)) {
915
916 // this is strictly cosmetic
917 if ($entry->tagName == 'img') {
918 $suffix = ".png";
919 } else if ($entry->parentNode && $entry->parentNode->tagName == "video") {
920 $suffix = ".mp4";
921 } else if ($entry->parentNode && $entry->parentNode->tagName == "audio") {
922 $suffix = ".ogg";
923 } else {
924 $suffix = "";
925 }
926
927 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
928
929 if ($entry->hasAttribute('srcset')) {
930 $entry->removeAttribute('srcset');
931 }
932
933 if ($entry->hasAttribute('sizes')) {
934 $entry->removeAttribute('sizes');
935 }
936 }
937
938 $entry->setAttribute('src', $src);
939 }
940
941 if ($entry->nodeName == 'img') {
942
943 if ($entry->hasAttribute('src')) {
944 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
945
946 if ($ttrss_uses_https && !$is_https_url) {
947
948 if ($entry->hasAttribute('srcset')) {
949 $entry->removeAttribute('srcset');
950 }
951
952 if ($entry->hasAttribute('sizes')) {
953 $entry->removeAttribute('sizes');
954 }
955 }
956 }
957
958 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
959 $force_remove_images || $_SESSION["bw_limit"]) {
960
961 $p = $doc->createElement('p');
962
963 $a = $doc->createElement('a');
964 $a->setAttribute('href', $entry->getAttribute('src'));
965
966 $a->appendChild(new DOMText($entry->getAttribute('src')));
967 $a->setAttribute('target', '_blank');
968 $a->setAttribute('rel', 'noopener noreferrer');
969
970 $p->appendChild($a);
971
972 $entry->parentNode->replaceChild($p, $entry);
973 }
974 }
975
976 if (strtolower($entry->nodeName) == "a") {
977 $entry->setAttribute("target", "_blank");
978 $entry->setAttribute("rel", "noopener noreferrer");
979 }
980 }
981
982 $entries = $xpath->query('//iframe');
983 foreach ($entries as $entry) {
984 if (!iframe_whitelisted($entry)) {
985 $entry->setAttribute('sandbox', 'allow-scripts');
986 } else {
987 if ($_SERVER['HTTPS'] == "on") {
988 $entry->setAttribute("src",
989 str_replace("http://", "https://",
990 $entry->getAttribute("src")));
991 }
992 }
993 }
994
995 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
996 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
997 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
998 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
999 'dt', 'em', 'footer', 'figure', 'figcaption',
1000 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'html', 'i',
1001 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1002 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1003 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1004 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1005 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1006
1007 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1008
1009 $disallowed_attributes = array('id', 'style', 'class');
1010
1011 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1012 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1013 if (is_array($retval)) {
1014 $doc = $retval[0];
1015 $allowed_elements = $retval[1];
1016 $disallowed_attributes = $retval[2];
1017 } else {
1018 $doc = $retval;
1019 }
1020 }
1021
1022 $doc->removeChild($doc->firstChild); //remove doctype
1023 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1024
1025 if ($highlight_words) {
1026 foreach ($highlight_words as $word) {
1027
1028 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1029
1030 $elements = $xpath->query("//*/text()");
1031
1032 foreach ($elements as $child) {
1033
1034 $fragment = $doc->createDocumentFragment();
1035 $text = $child->textContent;
1036
1037 while (($pos = mb_stripos($text, $word)) !== false) {
1038 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1039 $word = mb_substr($text, $pos, mb_strlen($word));
1040 $highlight = $doc->createElement('span');
1041 $highlight->appendChild(new DomText($word));
1042 $highlight->setAttribute('class', 'highlight');
1043 $fragment->appendChild($highlight);
1044 $text = mb_substr($text, $pos + mb_strlen($word));
1045 }
1046
1047 if (!empty($text)) $fragment->appendChild(new DomText($text));
1048
1049 $child->parentNode->replaceChild($fragment, $child);
1050 }
1051 }
1052 }
1053
1054 $res = $doc->saveHTML();
1055
1056 /* strip everything outside of <body>...</body> */
1057
1058 $res_frag = array();
1059 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1060 return $res_frag[1];
1061 } else {
1062 return $res;
1063 }
1064 }
1065
1066 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1067 $xpath = new DOMXPath($doc);
1068 $entries = $xpath->query('//*');
1069
1070 foreach ($entries as $entry) {
1071 if (!in_array($entry->nodeName, $allowed_elements)) {
1072 $entry->parentNode->removeChild($entry);
1073 }
1074
1075 if ($entry->hasAttributes()) {
1076 $attrs_to_remove = array();
1077
1078 foreach ($entry->attributes as $attr) {
1079
1080 if (strpos($attr->nodeName, 'on') === 0) {
1081 array_push($attrs_to_remove, $attr);
1082 }
1083
1084 if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1085 array_push($attrs_to_remove, $attr);
1086 }
1087
1088 if (in_array($attr->nodeName, $disallowed_attributes)) {
1089 array_push($attrs_to_remove, $attr);
1090 }
1091 }
1092
1093 foreach ($attrs_to_remove as $attr) {
1094 $entry->removeAttributeNode($attr);
1095 }
1096 }
1097 }
1098
1099 return $doc;
1100 }
1101
1102 function catchupArticlesById($ids, $cmode, $owner_uid = false) {
1103
1104 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
1105 if (count($ids) == 0) return;
1106
1107 $tmp_ids = array();
1108
1109 foreach ($ids as $id) {
1110 array_push($tmp_ids, "ref_id = '$id'");
1111 }
1112
1113 $ids_qpart = join(" OR ", $tmp_ids);
1114
1115 if ($cmode == 0) {
1116 db_query("UPDATE ttrss_user_entries SET
1117 unread = false,last_read = NOW()
1118 WHERE ($ids_qpart) AND owner_uid = $owner_uid");
1119 } else if ($cmode == 1) {
1120 db_query("UPDATE ttrss_user_entries SET
1121 unread = true
1122 WHERE ($ids_qpart) AND owner_uid = $owner_uid");
1123 } else {
1124 db_query("UPDATE ttrss_user_entries SET
1125 unread = NOT unread,last_read = NOW()
1126 WHERE ($ids_qpart) AND owner_uid = $owner_uid");
1127 }
1128
1129 /* update ccache */
1130
1131 $result = db_query("SELECT DISTINCT feed_id FROM ttrss_user_entries
1132 WHERE ($ids_qpart) AND owner_uid = $owner_uid");
1133
1134 while ($line = db_fetch_assoc($result)) {
1135 ccache_update($line["feed_id"], $owner_uid);
1136 }
1137 }
1138
1139 function trim_array($array) {
1140 $tmp = $array;
1141 array_walk($tmp, 'trim');
1142 return $tmp;
1143 }
1144
1145 function tag_is_valid($tag) {
1146 if ($tag == '') return false;
1147 if (is_numeric($tag)) return false;
1148 if (mb_strlen($tag) > 250) return false;
1149
1150 if (!$tag) return false;
1151
1152 return true;
1153 }
1154
1155 function render_login_form() {
1156 header('Cache-Control: public');
1157
1158 require_once "login_form.php";
1159 exit;
1160 }
1161
1162 function T_sprintf() {
1163 $args = func_get_args();
1164 return vsprintf(__(array_shift($args)), $args);
1165 }
1166
1167 function print_checkpoint($n, $s) {
1168 $ts = microtime(true);
1169 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1170 return $ts;
1171 }
1172
1173 function sanitize_tag($tag) {
1174 $tag = trim($tag);
1175
1176 $tag = mb_strtolower($tag, 'utf-8');
1177
1178 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1179
1180 if (DB_TYPE == "mysql") {
1181 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1182 }
1183
1184 return $tag;
1185 }
1186
1187 function get_self_url_prefix() {
1188 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1189 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1190 } else {
1191 return SELF_URL_PATH;
1192 }
1193 }
1194
1195 /**
1196 * Compute the Mozilla Firefox feed adding URL from server HOST and REQUEST_URI.
1197 *
1198 * @return string The Mozilla Firefox feed adding URL.
1199 */
1200 function add_feed_url() {
1201 //$url_path = ($_SERVER['HTTPS'] != "on" ? 'http://' : 'https://') . $_SERVER["HTTP_HOST"] . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
1202
1203 $url_path = get_self_url_prefix() .
1204 "/public.php?op=subscribe&feed_url=%s";
1205 return $url_path;
1206 } // function add_feed_url
1207
1208 function encrypt_password($pass, $salt = '', $mode2 = false) {
1209 if ($salt && $mode2) {
1210 return "MODE2:" . hash('sha256', $salt . $pass);
1211 } else if ($salt) {
1212 return "SHA1X:" . sha1("$salt:$pass");
1213 } else {
1214 return "SHA1:" . sha1($pass);
1215 }
1216 } // function encrypt_password
1217
1218 function load_filters($feed_id, $owner_uid) {
1219 $filters = array();
1220
1221 $cat_id = (int)getFeedCategory($feed_id);
1222
1223 if ($cat_id == 0)
1224 $null_cat_qpart = "cat_id IS NULL OR";
1225 else
1226 $null_cat_qpart = "";
1227
1228 $result = db_query("SELECT * FROM ttrss_filters2 WHERE
1229 owner_uid = $owner_uid AND enabled = true ORDER BY order_id, title");
1230
1231 $check_cats = join(",", array_merge(
1232 getParentCategories($cat_id, $owner_uid),
1233 array($cat_id)));
1234
1235 while ($line = db_fetch_assoc($result)) {
1236 $filter_id = $line["id"];
1237
1238 $result2 = db_query("SELECT
1239 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, t.name AS type_name
1240 FROM ttrss_filters2_rules AS r,
1241 ttrss_filter_types AS t
1242 WHERE
1243 ($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats)) AND
1244 (feed_id IS NULL OR feed_id = '$feed_id') AND
1245 filter_type = t.id AND filter_id = '$filter_id'");
1246
1247 $rules = array();
1248 $actions = array();
1249
1250 while ($rule_line = db_fetch_assoc($result2)) {
1251 # print_r($rule_line);
1252
1253 $rule = array();
1254 $rule["reg_exp"] = $rule_line["reg_exp"];
1255 $rule["type"] = $rule_line["type_name"];
1256 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1257
1258 array_push($rules, $rule);
1259 }
1260
1261 $result2 = db_query("SELECT a.action_param,t.name AS type_name
1262 FROM ttrss_filters2_actions AS a,
1263 ttrss_filter_actions AS t
1264 WHERE
1265 action_id = t.id AND filter_id = '$filter_id'");
1266
1267 while ($action_line = db_fetch_assoc($result2)) {
1268 # print_r($action_line);
1269
1270 $action = array();
1271 $action["type"] = $action_line["type_name"];
1272 $action["param"] = $action_line["action_param"];
1273
1274 array_push($actions, $action);
1275 }
1276
1277
1278 $filter = array();
1279 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1280 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1281 $filter["rules"] = $rules;
1282 $filter["actions"] = $actions;
1283
1284 if (count($rules) > 0 && count($actions) > 0) {
1285 array_push($filters, $filter);
1286 }
1287 }
1288
1289 return $filters;
1290 }
1291
1292 function get_score_pic($score) {
1293 if ($score > 100) {
1294 return "score_high.png";
1295 } else if ($score > 0) {
1296 return "score_half_high.png";
1297 } else if ($score < -100) {
1298 return "score_low.png";
1299 } else if ($score < 0) {
1300 return "score_half_low.png";
1301 } else {
1302 return "score_neutral.png";
1303 }
1304 }
1305
1306 function feed_has_icon($id) {
1307 return is_file(ICONS_DIR . "/$id.ico") && filesize(ICONS_DIR . "/$id.ico") > 0;
1308 }
1309
1310 function init_plugins() {
1311 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
1312
1313 return true;
1314 }
1315
1316 function add_feed_category($feed_cat, $parent_cat_id = false) {
1317
1318 if (!$feed_cat) return false;
1319
1320 db_query("BEGIN");
1321
1322 if ($parent_cat_id) {
1323 $parent_qpart = "parent_cat = '$parent_cat_id'";
1324 $parent_insert = "'$parent_cat_id'";
1325 } else {
1326 $parent_qpart = "parent_cat IS NULL";
1327 $parent_insert = "NULL";
1328 }
1329
1330 $feed_cat = mb_substr($feed_cat, 0, 250);
1331
1332 $result = db_query(
1333 "SELECT id FROM ttrss_feed_categories
1334 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]);
1335
1336 if (db_num_rows($result) == 0) {
1337
1338 $result = db_query(
1339 "INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1340 VALUES ('".$_SESSION["uid"]."', '$feed_cat', $parent_insert)");
1341
1342 db_query("COMMIT");
1343
1344 return true;
1345 }
1346
1347 return false;
1348 }
1349
1350 /**
1351 * Fixes incomplete URLs by prepending "http://".
1352 * Also replaces feed:// with http://, and
1353 * prepends a trailing slash if the url is a domain name only.
1354 *
1355 * @param string $url Possibly incomplete URL
1356 *
1357 * @return string Fixed URL.
1358 */
1359 function fix_url($url) {
1360
1361 // support schema-less urls
1362 if (strpos($url, '//') === 0) {
1363 $url = 'https:' . $url;
1364 }
1365
1366 if (strpos($url, '://') === false) {
1367 $url = 'http://' . $url;
1368 } else if (substr($url, 0, 5) == 'feed:') {
1369 $url = 'http:' . substr($url, 5);
1370 }
1371
1372 //prepend slash if the URL has no slash in it
1373 // "http://www.example" -> "http://www.example/"
1374 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
1375 $url .= '/';
1376 }
1377
1378 //convert IDNA hostname to punycode if possible
1379 if (function_exists("idn_to_ascii")) {
1380 $parts = parse_url($url);
1381 if (mb_detect_encoding($parts['host']) != 'ASCII')
1382 {
1383 $parts['host'] = idn_to_ascii($parts['host']);
1384 $url = build_url($parts);
1385 }
1386 }
1387
1388 if ($url != "http:///")
1389 return $url;
1390 else
1391 return '';
1392 }
1393
1394 function validate_feed_url($url) {
1395 $parts = parse_url($url);
1396
1397 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
1398
1399 }
1400
1401 /* function save_email_address($email) {
1402 // FIXME: implement persistent storage of emails
1403
1404 if (!$_SESSION['stored_emails'])
1405 $_SESSION['stored_emails'] = array();
1406
1407 if (!in_array($email, $_SESSION['stored_emails']))
1408 array_push($_SESSION['stored_emails'], $email);
1409 } */
1410
1411
1412 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
1413
1414 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
1415
1416 $sql_is_cat = bool_to_sql_bool($is_cat);
1417
1418 $result = db_query("SELECT access_key FROM ttrss_access_keys
1419 WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat
1420 AND owner_uid = " . $owner_uid);
1421
1422 if (db_num_rows($result) == 1) {
1423 return db_fetch_result($result, 0, "access_key");
1424 } else {
1425 $key = db_escape_string(uniqid_short());
1426
1427 $result = db_query("INSERT INTO ttrss_access_keys
1428 (access_key, feed_id, is_cat, owner_uid)
1429 VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')");
1430
1431 return $key;
1432 }
1433 return false;
1434 }
1435
1436 function get_feeds_from_html($url, $content)
1437 {
1438 $url = fix_url($url);
1439 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
1440
1441 libxml_use_internal_errors(true);
1442
1443 $doc = new DOMDocument();
1444 $doc->loadHTML($content);
1445 $xpath = new DOMXPath($doc);
1446 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
1447 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
1448 $feedUrls = array();
1449 foreach ($entries as $entry) {
1450 if ($entry->hasAttribute('href')) {
1451 $title = $entry->getAttribute('title');
1452 if ($title == '') {
1453 $title = $entry->getAttribute('type');
1454 }
1455 $feedUrl = rewrite_relative_url(
1456 $baseUrl, $entry->getAttribute('href')
1457 );
1458 $feedUrls[$feedUrl] = $title;
1459 }
1460 }
1461 return $feedUrls;
1462 }
1463
1464 function is_html($content) {
1465 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
1466 }
1467
1468 function url_is_html($url, $login = false, $pass = false) {
1469 return is_html(fetch_file_contents($url, false, $login, $pass));
1470 }
1471
1472 function getLastArticleId() {
1473 $result = db_query("SELECT ref_id AS id FROM ttrss_user_entries
1474 WHERE owner_uid = " . $_SESSION["uid"] . " ORDER BY ref_id DESC LIMIT 1");
1475
1476 if (db_num_rows($result) == 1) {
1477 return db_fetch_result($result, 0, "id");
1478 } else {
1479 return -1;
1480 }
1481 }
1482
1483 function build_url($parts) {
1484 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
1485 }
1486
1487 function cleanup_url_path($path) {
1488 $path = str_replace("/./", "/", $path);
1489 $path = str_replace("//", "/", $path);
1490
1491 return $path;
1492 }
1493
1494 /**
1495 * Converts a (possibly) relative URL to a absolute one.
1496 *
1497 * @param string $url Base URL (i.e. from where the document is)
1498 * @param string $rel_url Possibly relative URL in the document
1499 *
1500 * @return string Absolute URL
1501 */
1502 function rewrite_relative_url($url, $rel_url) {
1503 if (strpos($rel_url, "://") !== false) {
1504 return $rel_url;
1505 } else if (strpos($rel_url, "//") === 0) {
1506 # protocol-relative URL (rare but they exist)
1507 return $rel_url;
1508 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
1509 # magnet:, feed:, etc
1510 return $rel_url;
1511 } else if (strpos($rel_url, "/") === 0) {
1512 $parts = parse_url($url);
1513 $parts['path'] = $rel_url;
1514 $parts['path'] = cleanup_url_path($parts['path']);
1515
1516 return build_url($parts);
1517
1518 } else {
1519 $parts = parse_url($url);
1520 if (!isset($parts['path'])) {
1521 $parts['path'] = '/';
1522 }
1523 $dir = $parts['path'];
1524 if (substr($dir, -1) !== '/') {
1525 $dir = dirname($parts['path']);
1526 $dir !== '/' && $dir .= '/';
1527 }
1528 $parts['path'] = $dir . $rel_url;
1529 $parts['path'] = cleanup_url_path($parts['path']);
1530
1531 return build_url($parts);
1532 }
1533 }
1534
1535 function cleanup_tags($days = 14, $limit = 1000) {
1536
1537 if (DB_TYPE == "pgsql") {
1538 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
1539 } else if (DB_TYPE == "mysql") {
1540 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
1541 }
1542
1543 $tags_deleted = 0;
1544
1545 while ($limit > 0) {
1546 $limit_part = 500;
1547
1548 $query = "SELECT ttrss_tags.id AS id
1549 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
1550 WHERE post_int_id = int_id AND $interval_query AND
1551 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT $limit_part";
1552
1553 $result = db_query($query);
1554
1555 $ids = array();
1556
1557 while ($line = db_fetch_assoc($result)) {
1558 array_push($ids, $line['id']);
1559 }
1560
1561 if (count($ids) > 0) {
1562 $ids = join(",", $ids);
1563
1564 $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
1565 $tags_deleted += db_affected_rows($tmp_result);
1566 } else {
1567 break;
1568 }
1569
1570 $limit -= $limit_part;
1571 }
1572
1573 return $tags_deleted;
1574 }
1575
1576 function print_user_stylesheet() {
1577 $value = get_pref('USER_STYLESHEET');
1578
1579 if ($value) {
1580 print "<style type=\"text/css\">";
1581 print str_replace("<br/>", "\n", $value);
1582 print "</style>";
1583 }
1584
1585 }
1586
1587 function filter_to_sql($filter, $owner_uid) {
1588 $query = array();
1589
1590 if (DB_TYPE == "pgsql")
1591 $reg_qpart = "~";
1592 else
1593 $reg_qpart = "REGEXP";
1594
1595 foreach ($filter["rules"] AS $rule) {
1596 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
1597 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
1598 $rule['reg_exp']) !== FALSE;
1599
1600 if ($regexp_valid) {
1601
1602 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
1603
1604 switch ($rule["type"]) {
1605 case "title":
1606 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
1607 $rule['reg_exp'] . "')";
1608 break;
1609 case "content":
1610 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
1611 $rule['reg_exp'] . "')";
1612 break;
1613 case "both":
1614 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
1615 $rule['reg_exp'] . "') OR LOWER(" .
1616 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
1617 break;
1618 case "tag":
1619 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
1620 $rule['reg_exp'] . "')";
1621 break;
1622 case "link":
1623 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
1624 $rule['reg_exp'] . "')";
1625 break;
1626 case "author":
1627 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
1628 $rule['reg_exp'] . "')";
1629 break;
1630 }
1631
1632 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
1633
1634 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
1635 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
1636 }
1637
1638 if (isset($rule["cat_id"])) {
1639
1640 if ($rule["cat_id"] > 0) {
1641 $children = getChildCategories($rule["cat_id"], $owner_uid);
1642 array_push($children, $rule["cat_id"]);
1643
1644 $children = join(",", $children);
1645
1646 $cat_qpart = "cat_id IN ($children)";
1647 } else {
1648 $cat_qpart = "cat_id IS NULL";
1649 }
1650
1651 $qpart .= " AND $cat_qpart";
1652 }
1653
1654 $qpart .= " AND feed_id IS NOT NULL";
1655
1656 array_push($query, "($qpart)");
1657
1658 }
1659 }
1660
1661 if (count($query) > 0) {
1662 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
1663 } else {
1664 $fullquery = "(false)";
1665 }
1666
1667 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
1668
1669 return $fullquery;
1670 }
1671
1672 if (!function_exists('gzdecode')) {
1673 function gzdecode($string) { // no support for 2nd argument
1674 return file_get_contents('compress.zlib://data:who/cares;base64,'.
1675 base64_encode($string));
1676 }
1677 }
1678
1679 function get_random_bytes($length) {
1680 if (function_exists('openssl_random_pseudo_bytes')) {
1681 return openssl_random_pseudo_bytes($length);
1682 } else {
1683 $output = "";
1684
1685 for ($i = 0; $i < $length; $i++)
1686 $output .= chr(mt_rand(0, 255));
1687
1688 return $output;
1689 }
1690 }
1691
1692 function read_stdin() {
1693 $fp = fopen("php://stdin", "r");
1694
1695 if ($fp) {
1696 $line = trim(fgets($fp));
1697 fclose($fp);
1698 return $line;
1699 }
1700
1701 return null;
1702 }
1703
1704 function getFeedCategory($feed) {
1705 $result = db_query("SELECT cat_id FROM ttrss_feeds
1706 WHERE id = '$feed'");
1707
1708 if (db_num_rows($result) > 0) {
1709 return db_fetch_result($result, 0, "cat_id");
1710 } else {
1711 return false;
1712 }
1713
1714 }
1715
1716 function implements_interface($class, $interface) {
1717 return in_array($interface, class_implements($class));
1718 }
1719
1720 function get_minified_js($files) {
1721 require_once 'lib/jshrink/Minifier.php';
1722
1723 $rv = '';
1724
1725 foreach ($files as $js) {
1726 if (!isset($_GET['debug'])) {
1727 $cached_file = CACHE_DIR . "/js/".basename($js).".js";
1728
1729 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
1730
1731 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
1732
1733 if ($header && $contents) {
1734 list($htag, $hversion) = explode(":", $header);
1735
1736 if ($htag == "tt-rss" && $hversion == VERSION) {
1737 $rv .= $contents;
1738 continue;
1739 }
1740 }
1741 }
1742
1743 $minified = JShrink\Minifier::minify(file_get_contents("js/$js.js"));
1744 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
1745 $rv .= $minified;
1746
1747 } else {
1748 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
1749 }
1750 }
1751
1752 return $rv;
1753 }
1754
1755 function calculate_dep_timestamp() {
1756 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
1757
1758 $max_ts = -1;
1759
1760 foreach ($files as $file) {
1761 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
1762 }
1763
1764 return $max_ts;
1765 }
1766
1767 function T_js_decl($s1, $s2) {
1768 if ($s1 && $s2) {
1769 $s1 = preg_replace("/\n/", "", $s1);
1770 $s2 = preg_replace("/\n/", "", $s2);
1771
1772 $s1 = preg_replace("/\"/", "\\\"", $s1);
1773 $s2 = preg_replace("/\"/", "\\\"", $s2);
1774
1775 return "T_messages[\"$s1\"] = \"$s2\";\n";
1776 }
1777 }
1778
1779 function init_js_translations() {
1780
1781 print 'var T_messages = new Object();
1782
1783 function __(msg) {
1784 if (T_messages[msg]) {
1785 return T_messages[msg];
1786 } else {
1787 return msg;
1788 }
1789 }
1790
1791 function ngettext(msg1, msg2, n) {
1792 return __((parseInt(n) > 1) ? msg2 : msg1);
1793 }';
1794
1795 $l10n = _get_reader();
1796
1797 for ($i = 0; $i < $l10n->total; $i++) {
1798 $orig = $l10n->get_original_string($i);
1799 if(strpos($orig, "\000") !== FALSE) { // Plural forms
1800 $key = explode(chr(0), $orig);
1801 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
1802 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
1803 } else {
1804 $translation = __($orig);
1805 print T_js_decl($orig, $translation);
1806 }
1807 }
1808 }
1809
1810 function label_to_feed_id($label) {
1811 return LABEL_BASE_INDEX - 1 - abs($label);
1812 }
1813
1814 function feed_to_label_id($feed) {
1815 return LABEL_BASE_INDEX - 1 + abs($feed);
1816 }
1817
1818 function get_theme_path($theme) {
1819 $check = "themes/$theme";
1820 if (file_exists($check)) return $check;
1821
1822 $check = "themes.local/$theme";
1823 if (file_exists($check)) return $check;
1824 }
1825
1826 function theme_valid($theme) {
1827 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
1828
1829 if (in_array($theme, $bundled_themes)) return true;
1830
1831 $file = "themes/" . basename($theme);
1832
1833 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
1834
1835 if (file_exists($file) && is_readable($file)) {
1836 $fh = fopen($file, "r");
1837
1838 if ($fh) {
1839 $header = fgets($fh);
1840 fclose($fh);
1841
1842 return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
1843 }
1844 }
1845
1846 return false;
1847 }
1848
1849 /**
1850 * @SuppressWarnings(unused)
1851 */
1852 function error_json($code) {
1853 require_once "errors.php";
1854
1855 @$message = $ERRORS[$code];
1856
1857 return json_encode(array("error" =>
1858 array("code" => $code, "message" => $message)));
1859
1860 }
1861
1862 function abs_to_rel_path($dir) {
1863 $tmp = str_replace(dirname(__DIR__), "", $dir);
1864
1865 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
1866
1867 return $tmp;
1868 }
1869
1870 function get_upload_error_message($code) {
1871
1872 $errors = array(
1873 0 => __('There is no error, the file uploaded with success'),
1874 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
1875 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
1876 3 => __('The uploaded file was only partially uploaded'),
1877 4 => __('No file was uploaded'),
1878 6 => __('Missing a temporary folder'),
1879 7 => __('Failed to write file to disk.'),
1880 8 => __('A PHP extension stopped the file upload.'),
1881 );
1882
1883 return $errors[$code];
1884 }
1885
1886 function base64_img($filename) {
1887 if (file_exists($filename)) {
1888 $ext = pathinfo($filename, PATHINFO_EXTENSION);
1889
1890 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
1891 } else {
1892 return "";
1893 }
1894 }
1895