]> git.wh0rd.org - tt-rss.git/blob - include/functions2.php
move some common control-generating functions to controls.php
[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 get_article_tags($id, $owner_uid = 0, $tag_cache = false) {
1140
1141 $a_id = db_escape_string($id);
1142
1143 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
1144
1145 $query = "SELECT DISTINCT tag_name,
1146 owner_uid as owner FROM
1147 ttrss_tags WHERE post_int_id = (SELECT int_id FROM ttrss_user_entries WHERE
1148 ref_id = '$a_id' AND owner_uid = '$owner_uid' LIMIT 1) ORDER BY tag_name";
1149
1150 $tags = array();
1151
1152 /* check cache first */
1153
1154 if ($tag_cache === false) {
1155 $result = db_query("SELECT tag_cache FROM ttrss_user_entries
1156 WHERE ref_id = '$id' AND owner_uid = $owner_uid");
1157
1158 if (db_num_rows($result) != 0)
1159 $tag_cache = db_fetch_result($result, 0, "tag_cache");
1160 }
1161
1162 if ($tag_cache) {
1163 $tags = explode(",", $tag_cache);
1164 } else {
1165
1166 /* do it the hard way */
1167
1168 $tmp_result = db_query($query);
1169
1170 while ($tmp_line = db_fetch_assoc($tmp_result)) {
1171 array_push($tags, $tmp_line["tag_name"]);
1172 }
1173
1174 /* update the cache */
1175
1176 $tags_str = db_escape_string(join(",", $tags));
1177
1178 db_query("UPDATE ttrss_user_entries
1179 SET tag_cache = '$tags_str' WHERE ref_id = '$id'
1180 AND owner_uid = $owner_uid");
1181 }
1182
1183 return $tags;
1184 }
1185
1186 function trim_array($array) {
1187 $tmp = $array;
1188 array_walk($tmp, 'trim');
1189 return $tmp;
1190 }
1191
1192 function tag_is_valid($tag) {
1193 if ($tag == '') return false;
1194 if (is_numeric($tag)) return false;
1195 if (mb_strlen($tag) > 250) return false;
1196
1197 if (!$tag) return false;
1198
1199 return true;
1200 }
1201
1202 function render_login_form() {
1203 header('Cache-Control: public');
1204
1205 require_once "login_form.php";
1206 exit;
1207 }
1208
1209 function T_sprintf() {
1210 $args = func_get_args();
1211 return vsprintf(__(array_shift($args)), $args);
1212 }
1213
1214 function format_article($id, $mark_as_read = true, $zoom_mode = false, $owner_uid = false) {
1215 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
1216
1217 $rv = array();
1218
1219 $rv['id'] = $id;
1220
1221 /* we can figure out feed_id from article id anyway, why do we
1222 * pass feed_id here? let's ignore the argument :(*/
1223
1224 $result = db_query("SELECT feed_id FROM ttrss_user_entries
1225 WHERE ref_id = '$id'");
1226
1227 $feed_id = (int) db_fetch_result($result, 0, "feed_id");
1228
1229 $rv['feed_id'] = $feed_id;
1230
1231 //if (!$zoom_mode) { print "<article id='$id'><![CDATA["; };
1232
1233 if ($mark_as_read) {
1234 $result = db_query("UPDATE ttrss_user_entries
1235 SET unread = false,last_read = NOW()
1236 WHERE ref_id = '$id' AND owner_uid = $owner_uid");
1237
1238 ccache_update($feed_id, $owner_uid);
1239 }
1240
1241 $result = db_query("SELECT id,title,link,content,feed_id,comments,int_id,lang,
1242 ".SUBSTRING_FOR_DATE."(updated,1,16) as updated,
1243 (SELECT site_url FROM ttrss_feeds WHERE id = feed_id) as site_url,
1244 (SELECT title FROM ttrss_feeds WHERE id = feed_id) as feed_title,
1245 (SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) as hide_images,
1246 (SELECT always_display_enclosures FROM ttrss_feeds WHERE id = feed_id) as always_display_enclosures,
1247 num_comments,
1248 tag_cache,
1249 author,
1250 guid,
1251 orig_feed_id,
1252 note
1253 FROM ttrss_entries,ttrss_user_entries
1254 WHERE id = '$id' AND ref_id = id AND owner_uid = $owner_uid");
1255
1256 if ($result) {
1257
1258 $line = db_fetch_assoc($result);
1259
1260 $line["tags"] = get_article_tags($id, $owner_uid, $line["tag_cache"]);
1261 unset($line["tag_cache"]);
1262
1263 $line["content"] = sanitize($line["content"],
1264 sql_bool_to_bool($line['hide_images']),
1265 $owner_uid, $line["site_url"], false, $line["id"]);
1266
1267 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE) as $p) {
1268 $line = $p->hook_render_article($line);
1269 }
1270
1271 $num_comments = (int) $line["num_comments"];
1272 $entry_comments = "";
1273
1274 if ($num_comments > 0) {
1275 if ($line["comments"]) {
1276 $comments_url = htmlspecialchars($line["comments"]);
1277 } else {
1278 $comments_url = htmlspecialchars($line["link"]);
1279 }
1280 $entry_comments = "<a class=\"postComments\"
1281 target='_blank' rel=\"noopener noreferrer\" href=\"$comments_url\">$num_comments ".
1282 _ngettext("comment", "comments", $num_comments)."</a>";
1283
1284 } else {
1285 if ($line["comments"] && $line["link"] != $line["comments"]) {
1286 $entry_comments = "<a class=\"postComments\" target='_blank' rel=\"noopener noreferrer\" href=\"".htmlspecialchars($line["comments"])."\">".__("comments")."</a>";
1287 }
1288 }
1289
1290 if ($zoom_mode) {
1291 header("Content-Type: text/html");
1292 $rv['content'] .= "<html><head>
1293 <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>
1294 <title>Tiny Tiny RSS - ".$line["title"]."</title>".
1295 stylesheet_tag("css/tt-rss.css").
1296 stylesheet_tag("css/zoom.css").
1297 stylesheet_tag("css/dijit.css")."
1298
1299 <link rel=\"shortcut icon\" type=\"image/png\" href=\"images/favicon.png\">
1300 <link rel=\"icon\" type=\"image/png\" sizes=\"72x72\" href=\"images/favicon-72px.png\">
1301
1302 </head><body id=\"ttrssZoom\">";
1303 }
1304
1305 $rv['content'] .= "<div class=\"postReply\" id=\"POST-$id\">";
1306
1307 $rv['content'] .= "<div class=\"postHeader\" id=\"POSTHDR-$id\">";
1308
1309 $entry_author = $line["author"];
1310
1311 if ($entry_author) {
1312 $entry_author = __(" - ") . $entry_author;
1313 }
1314
1315 $parsed_updated = make_local_datetime($line["updated"], true,
1316 $owner_uid, true);
1317
1318 if (!$zoom_mode)
1319 $rv['content'] .= "<div class=\"postDate\">$parsed_updated</div>";
1320
1321 if ($line["link"]) {
1322 $rv['content'] .= "<div class='postTitle'><a target='_blank' rel='noopener noreferrer'
1323 title=\"".htmlspecialchars($line['title'])."\"
1324 href=\"" .
1325 htmlspecialchars($line["link"]) . "\">" .
1326 $line["title"] . "</a>" .
1327 "<span class='author'>$entry_author</span></div>";
1328 } else {
1329 $rv['content'] .= "<div class='postTitle'>" . $line["title"] . "$entry_author</div>";
1330 }
1331
1332 if ($zoom_mode) {
1333 $feed_title = htmlspecialchars($line["feed_title"]);
1334
1335 $rv['content'] .= "<div class=\"postFeedTitle\">$feed_title</div>";
1336
1337 $rv['content'] .= "<div class=\"postDate\">$parsed_updated</div>";
1338 }
1339
1340 $tags_str = format_tags_string($line["tags"], $id);
1341 $tags_str_full = join(", ", $line["tags"]);
1342
1343 if (!$tags_str_full) $tags_str_full = __("no tags");
1344
1345 if (!$entry_comments) $entry_comments = "&nbsp;"; # placeholder
1346
1347 $rv['content'] .= "<div class='postTags' style='float : right'>
1348 <img src='images/tag.png'
1349 class='tagsPic' alt='Tags' title='Tags'>&nbsp;";
1350
1351 if (!$zoom_mode) {
1352 $rv['content'] .= "<span id=\"ATSTR-$id\">$tags_str</span>
1353 <a title=\"".__('Edit tags for this article')."\"
1354 href=\"#\" onclick=\"editArticleTags($id, $feed_id)\">(+)</a>";
1355
1356 $rv['content'] .= "<div dojoType=\"dijit.Tooltip\"
1357 id=\"ATSTRTIP-$id\" connectId=\"ATSTR-$id\"
1358 position=\"below\">$tags_str_full</div>";
1359
1360 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_BUTTON) as $p) {
1361 $rv['content'] .= $p->hook_article_button($line);
1362 }
1363
1364 } else {
1365 $tags_str = strip_tags($tags_str);
1366 $rv['content'] .= "<span id=\"ATSTR-$id\">$tags_str</span>";
1367 }
1368 $rv['content'] .= "</div>";
1369 $rv['content'] .= "<div clear='both'>";
1370
1371 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_LEFT_BUTTON) as $p) {
1372 $rv['content'] .= $p->hook_article_left_button($line);
1373 }
1374
1375 $rv['content'] .= "$entry_comments</div>";
1376
1377 if ($line["orig_feed_id"]) {
1378
1379 $tmp_result = db_query("SELECT * FROM ttrss_archived_feeds
1380 WHERE id = ".$line["orig_feed_id"] . " AND owner_uid = " . $_SESSION["uid"]);
1381
1382 if (db_num_rows($tmp_result) != 0) {
1383
1384 $rv['content'] .= "<div clear='both'>";
1385 $rv['content'] .= __("Originally from:");
1386
1387 $rv['content'] .= "&nbsp;";
1388
1389 $tmp_line = db_fetch_assoc($tmp_result);
1390
1391 $rv['content'] .= "<a target='_blank' rel='noopener noreferrer'
1392 href=' " . htmlspecialchars($tmp_line['site_url']) . "'>" .
1393 $tmp_line['title'] . "</a>";
1394
1395 $rv['content'] .= "&nbsp;";
1396
1397 $rv['content'] .= "<a target='_blank' rel='noopener noreferrer' href='" . htmlspecialchars($tmp_line['feed_url']) . "'>";
1398 $rv['content'] .= "<img title='".__('Feed URL')."' class='tinyFeedIcon' src='images/pub_set.png'></a>";
1399
1400 $rv['content'] .= "</div>";
1401 }
1402 }
1403
1404 $rv['content'] .= "</div>";
1405
1406 $rv['content'] .= "<div id=\"POSTNOTE-$id\">";
1407 if ($line['note']) {
1408 $rv['content'] .= format_article_note($id, $line['note'], !$zoom_mode);
1409 }
1410 $rv['content'] .= "</div>";
1411
1412 if (!$line['lang']) $line['lang'] = 'en';
1413
1414 $rv['content'] .= "<div class=\"postContent\" lang=\"".$line['lang']."\">";
1415
1416 $rv['content'] .= $line["content"];
1417
1418 if (!$zoom_mode) {
1419 $rv['content'] .= format_article_enclosures($id,
1420 sql_bool_to_bool($line["always_display_enclosures"]),
1421 $line["content"],
1422 sql_bool_to_bool($line["hide_images"]));
1423 }
1424
1425 $rv['content'] .= "</div>";
1426
1427 $rv['content'] .= "</div>";
1428
1429 }
1430
1431 if ($zoom_mode) {
1432 $rv['content'] .= "
1433 <div class='footer'>
1434 <button onclick=\"return window.close()\">".
1435 __("Close this window")."</button></div>";
1436 $rv['content'] .= "</body></html>";
1437 }
1438
1439 return $rv;
1440
1441 }
1442
1443 function print_checkpoint($n, $s) {
1444 $ts = microtime(true);
1445 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1446 return $ts;
1447 }
1448
1449 function sanitize_tag($tag) {
1450 $tag = trim($tag);
1451
1452 $tag = mb_strtolower($tag, 'utf-8');
1453
1454 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1455
1456 if (DB_TYPE == "mysql") {
1457 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1458 }
1459
1460 return $tag;
1461 }
1462
1463 function get_self_url_prefix() {
1464 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1465 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1466 } else {
1467 return SELF_URL_PATH;
1468 }
1469 }
1470
1471 /**
1472 * Compute the Mozilla Firefox feed adding URL from server HOST and REQUEST_URI.
1473 *
1474 * @return string The Mozilla Firefox feed adding URL.
1475 */
1476 function add_feed_url() {
1477 //$url_path = ($_SERVER['HTTPS'] != "on" ? 'http://' : 'https://') . $_SERVER["HTTP_HOST"] . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
1478
1479 $url_path = get_self_url_prefix() .
1480 "/public.php?op=subscribe&feed_url=%s";
1481 return $url_path;
1482 } // function add_feed_url
1483
1484 function encrypt_password($pass, $salt = '', $mode2 = false) {
1485 if ($salt && $mode2) {
1486 return "MODE2:" . hash('sha256', $salt . $pass);
1487 } else if ($salt) {
1488 return "SHA1X:" . sha1("$salt:$pass");
1489 } else {
1490 return "SHA1:" . sha1($pass);
1491 }
1492 } // function encrypt_password
1493
1494 function load_filters($feed_id, $owner_uid) {
1495 $filters = array();
1496
1497 $cat_id = (int)getFeedCategory($feed_id);
1498
1499 if ($cat_id == 0)
1500 $null_cat_qpart = "cat_id IS NULL OR";
1501 else
1502 $null_cat_qpart = "";
1503
1504 $result = db_query("SELECT * FROM ttrss_filters2 WHERE
1505 owner_uid = $owner_uid AND enabled = true ORDER BY order_id, title");
1506
1507 $check_cats = join(",", array_merge(
1508 getParentCategories($cat_id, $owner_uid),
1509 array($cat_id)));
1510
1511 while ($line = db_fetch_assoc($result)) {
1512 $filter_id = $line["id"];
1513
1514 $result2 = db_query("SELECT
1515 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, t.name AS type_name
1516 FROM ttrss_filters2_rules AS r,
1517 ttrss_filter_types AS t
1518 WHERE
1519 ($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats)) AND
1520 (feed_id IS NULL OR feed_id = '$feed_id') AND
1521 filter_type = t.id AND filter_id = '$filter_id'");
1522
1523 $rules = array();
1524 $actions = array();
1525
1526 while ($rule_line = db_fetch_assoc($result2)) {
1527 # print_r($rule_line);
1528
1529 $rule = array();
1530 $rule["reg_exp"] = $rule_line["reg_exp"];
1531 $rule["type"] = $rule_line["type_name"];
1532 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1533
1534 array_push($rules, $rule);
1535 }
1536
1537 $result2 = db_query("SELECT a.action_param,t.name AS type_name
1538 FROM ttrss_filters2_actions AS a,
1539 ttrss_filter_actions AS t
1540 WHERE
1541 action_id = t.id AND filter_id = '$filter_id'");
1542
1543 while ($action_line = db_fetch_assoc($result2)) {
1544 # print_r($action_line);
1545
1546 $action = array();
1547 $action["type"] = $action_line["type_name"];
1548 $action["param"] = $action_line["action_param"];
1549
1550 array_push($actions, $action);
1551 }
1552
1553
1554 $filter = array();
1555 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1556 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1557 $filter["rules"] = $rules;
1558 $filter["actions"] = $actions;
1559
1560 if (count($rules) > 0 && count($actions) > 0) {
1561 array_push($filters, $filter);
1562 }
1563 }
1564
1565 return $filters;
1566 }
1567
1568 function get_score_pic($score) {
1569 if ($score > 100) {
1570 return "score_high.png";
1571 } else if ($score > 0) {
1572 return "score_half_high.png";
1573 } else if ($score < -100) {
1574 return "score_low.png";
1575 } else if ($score < 0) {
1576 return "score_half_low.png";
1577 } else {
1578 return "score_neutral.png";
1579 }
1580 }
1581
1582 function feed_has_icon($id) {
1583 return is_file(ICONS_DIR . "/$id.ico") && filesize(ICONS_DIR . "/$id.ico") > 0;
1584 }
1585
1586 function init_plugins() {
1587 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
1588
1589 return true;
1590 }
1591
1592 function format_tags_string($tags) {
1593 if (!is_array($tags) || count($tags) == 0) {
1594 return __("no tags");
1595 } else {
1596 $maxtags = min(5, count($tags));
1597 $tags_str = "";
1598
1599 for ($i = 0; $i < $maxtags; $i++) {
1600 $tags_str .= "<a class=\"tag\" href=\"#\" onclick=\"viewfeed({feed:'".$tags[$i]."'})\">" . $tags[$i] . "</a>, ";
1601 }
1602
1603 $tags_str = mb_substr($tags_str, 0, mb_strlen($tags_str)-2);
1604
1605 if (count($tags) > $maxtags)
1606 $tags_str .= ", &hellip;";
1607
1608 return $tags_str;
1609 }
1610 }
1611
1612 function format_article_labels($labels) {
1613
1614 if (!is_array($labels)) return '';
1615
1616 $labels_str = "";
1617
1618 foreach ($labels as $l) {
1619 $labels_str .= sprintf("<span class='hlLabelRef'
1620 style='color : %s; background-color : %s'>%s</span>",
1621 $l[2], $l[3], $l[1]);
1622 }
1623
1624 return $labels_str;
1625
1626 }
1627
1628 function format_article_note($id, $note, $allow_edit = true) {
1629
1630 $str = "<div class='articleNote' onclick=\"editArticleNote($id)\">
1631 <div class='noteEdit' onclick=\"editArticleNote($id)\">".
1632 ($allow_edit ? __('(edit note)') : "")."</div>$note</div>";
1633
1634 return $str;
1635 }
1636
1637
1638 function get_feed_category($feed_cat, $parent_cat_id = false) {
1639 if ($parent_cat_id) {
1640 $parent_qpart = "parent_cat = '$parent_cat_id'";
1641 $parent_insert = "'$parent_cat_id'";
1642 } else {
1643 $parent_qpart = "parent_cat IS NULL";
1644 $parent_insert = "NULL";
1645 }
1646
1647 $result = db_query(
1648 "SELECT id FROM ttrss_feed_categories
1649 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]);
1650
1651 if (db_num_rows($result) == 0) {
1652 return false;
1653 } else {
1654 return db_fetch_result($result, 0, "id");
1655 }
1656 }
1657
1658 function add_feed_category($feed_cat, $parent_cat_id = false) {
1659
1660 if (!$feed_cat) return false;
1661
1662 db_query("BEGIN");
1663
1664 if ($parent_cat_id) {
1665 $parent_qpart = "parent_cat = '$parent_cat_id'";
1666 $parent_insert = "'$parent_cat_id'";
1667 } else {
1668 $parent_qpart = "parent_cat IS NULL";
1669 $parent_insert = "NULL";
1670 }
1671
1672 $feed_cat = mb_substr($feed_cat, 0, 250);
1673
1674 $result = db_query(
1675 "SELECT id FROM ttrss_feed_categories
1676 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]);
1677
1678 if (db_num_rows($result) == 0) {
1679
1680 $result = db_query(
1681 "INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1682 VALUES ('".$_SESSION["uid"]."', '$feed_cat', $parent_insert)");
1683
1684 db_query("COMMIT");
1685
1686 return true;
1687 }
1688
1689 return false;
1690 }
1691
1692 function getArticleFeed($id) {
1693 $result = db_query("SELECT feed_id FROM ttrss_user_entries
1694 WHERE ref_id = '$id' AND owner_uid = " . $_SESSION["uid"]);
1695
1696 if (db_num_rows($result) != 0) {
1697 return db_fetch_result($result, 0, "feed_id");
1698 } else {
1699 return 0;
1700 }
1701 }
1702
1703 /**
1704 * Fixes incomplete URLs by prepending "http://".
1705 * Also replaces feed:// with http://, and
1706 * prepends a trailing slash if the url is a domain name only.
1707 *
1708 * @param string $url Possibly incomplete URL
1709 *
1710 * @return string Fixed URL.
1711 */
1712 function fix_url($url) {
1713
1714 // support schema-less urls
1715 if (strpos($url, '//') === 0) {
1716 $url = 'https:' . $url;
1717 }
1718
1719 if (strpos($url, '://') === false) {
1720 $url = 'http://' . $url;
1721 } else if (substr($url, 0, 5) == 'feed:') {
1722 $url = 'http:' . substr($url, 5);
1723 }
1724
1725 //prepend slash if the URL has no slash in it
1726 // "http://www.example" -> "http://www.example/"
1727 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
1728 $url .= '/';
1729 }
1730
1731 //convert IDNA hostname to punycode if possible
1732 if (function_exists("idn_to_ascii")) {
1733 $parts = parse_url($url);
1734 if (mb_detect_encoding($parts['host']) != 'ASCII')
1735 {
1736 $parts['host'] = idn_to_ascii($parts['host']);
1737 $url = build_url($parts);
1738 }
1739 }
1740
1741 if ($url != "http:///")
1742 return $url;
1743 else
1744 return '';
1745 }
1746
1747 function validate_feed_url($url) {
1748 $parts = parse_url($url);
1749
1750 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
1751
1752 }
1753
1754 function get_article_enclosures($id) {
1755
1756 $query = "SELECT * FROM ttrss_enclosures
1757 WHERE post_id = '$id' AND content_url != ''";
1758
1759 $rv = array();
1760
1761 $result = db_query($query);
1762
1763 if (db_num_rows($result) > 0) {
1764 while ($line = db_fetch_assoc($result)) {
1765
1766 if (file_exists(CACHE_DIR . '/images/' . sha1($line["content_url"]))) {
1767 $line["content_url"] = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($line["content_url"]);
1768 }
1769
1770 array_push($rv, $line);
1771 }
1772 }
1773
1774 return $rv;
1775 }
1776
1777 /* function save_email_address($email) {
1778 // FIXME: implement persistent storage of emails
1779
1780 if (!$_SESSION['stored_emails'])
1781 $_SESSION['stored_emails'] = array();
1782
1783 if (!in_array($email, $_SESSION['stored_emails']))
1784 array_push($_SESSION['stored_emails'], $email);
1785 } */
1786
1787
1788 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
1789
1790 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
1791
1792 $sql_is_cat = bool_to_sql_bool($is_cat);
1793
1794 $result = db_query("SELECT access_key FROM ttrss_access_keys
1795 WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat
1796 AND owner_uid = " . $owner_uid);
1797
1798 if (db_num_rows($result) == 1) {
1799 return db_fetch_result($result, 0, "access_key");
1800 } else {
1801 $key = db_escape_string(uniqid_short());
1802
1803 $result = db_query("INSERT INTO ttrss_access_keys
1804 (access_key, feed_id, is_cat, owner_uid)
1805 VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')");
1806
1807 return $key;
1808 }
1809 return false;
1810 }
1811
1812 function get_feeds_from_html($url, $content)
1813 {
1814 $url = fix_url($url);
1815 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
1816
1817 libxml_use_internal_errors(true);
1818
1819 $doc = new DOMDocument();
1820 $doc->loadHTML($content);
1821 $xpath = new DOMXPath($doc);
1822 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
1823 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
1824 $feedUrls = array();
1825 foreach ($entries as $entry) {
1826 if ($entry->hasAttribute('href')) {
1827 $title = $entry->getAttribute('title');
1828 if ($title == '') {
1829 $title = $entry->getAttribute('type');
1830 }
1831 $feedUrl = rewrite_relative_url(
1832 $baseUrl, $entry->getAttribute('href')
1833 );
1834 $feedUrls[$feedUrl] = $title;
1835 }
1836 }
1837 return $feedUrls;
1838 }
1839
1840 function is_html($content) {
1841 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
1842 }
1843
1844 function url_is_html($url, $login = false, $pass = false) {
1845 return is_html(fetch_file_contents($url, false, $login, $pass));
1846 }
1847
1848 function print_label_select($name, $value, $attributes = "") {
1849
1850 $result = db_query("SELECT caption FROM ttrss_labels2
1851 WHERE owner_uid = '".$_SESSION["uid"]."' ORDER BY caption");
1852
1853 print "<select default=\"$value\" name=\"" . htmlspecialchars($name) .
1854 "\" $attributes>";
1855
1856 while ($line = db_fetch_assoc($result)) {
1857
1858 $issel = ($line["caption"] == $value) ? "selected=\"1\"" : "";
1859
1860 print "<option value=\"".htmlspecialchars($line["caption"])."\"
1861 $issel>" . htmlspecialchars($line["caption"]) . "</option>";
1862
1863 }
1864
1865 # print "<option value=\"ADD_LABEL\">" .__("Add label...") . "</option>";
1866
1867 print "</select>";
1868
1869
1870 }
1871
1872 function format_article_enclosures($id, $always_display_enclosures,
1873 $article_content, $hide_images = false) {
1874
1875 $result = get_article_enclosures($id);
1876 $rv = '';
1877
1878 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FORMAT_ENCLOSURES) as $plugin) {
1879 $retval = $plugin->hook_format_enclosures($rv, $result, $id, $always_display_enclosures, $article_content, $hide_images);
1880 if (is_array($retval)) {
1881 $rv = $retval[0];
1882 $result = $retval[1];
1883 } else {
1884 $rv = $retval;
1885 }
1886 }
1887 unset($retval); // Unset to prevent breaking render if there are no HOOK_RENDER_ENCLOSURE hooks below.
1888
1889 if ($rv === '' && !empty($result)) {
1890 $entries_html = array();
1891 $entries = array();
1892 $entries_inline = array();
1893
1894 foreach ($result as $line) {
1895
1896 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ENCLOSURE_ENTRY) as $plugin) {
1897 $line = $plugin->hook_enclosure_entry($line);
1898 }
1899
1900 $url = $line["content_url"];
1901 $ctype = $line["content_type"];
1902 $title = $line["title"];
1903 $width = $line["width"];
1904 $height = $line["height"];
1905
1906 if (!$ctype) $ctype = __("unknown type");
1907
1908 //$filename = substr($url, strrpos($url, "/")+1);
1909 $filename = basename($url);
1910
1911 $player = format_inline_player($url, $ctype);
1912
1913 if ($player) array_push($entries_inline, $player);
1914
1915 # $entry .= " <a target=\"_blank\" href=\"" . htmlspecialchars($url) . "\" rel=\"noopener noreferrer\">" .
1916 # $filename . " (" . $ctype . ")" . "</a>";
1917
1918 $entry = "<div onclick=\"openUrlPopup('".htmlspecialchars($url)."')\"
1919 dojoType=\"dijit.MenuItem\">$filename ($ctype)</div>";
1920
1921 array_push($entries_html, $entry);
1922
1923 $entry = array();
1924
1925 $entry["type"] = $ctype;
1926 $entry["filename"] = $filename;
1927 $entry["url"] = $url;
1928 $entry["title"] = $title;
1929 $entry["width"] = $width;
1930 $entry["height"] = $height;
1931
1932 array_push($entries, $entry);
1933 }
1934
1935 if ($_SESSION['uid'] && !get_pref("STRIP_IMAGES") && !$_SESSION["bw_limit"]) {
1936 if ($always_display_enclosures ||
1937 !preg_match("/<img/i", $article_content)) {
1938
1939 foreach ($entries as $entry) {
1940
1941 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ENCLOSURE) as $plugin)
1942 $retval = $plugin->hook_render_enclosure($entry, $hide_images);
1943
1944
1945 if ($retval) {
1946 $rv .= $retval;
1947 } else {
1948
1949 if (preg_match("/image/", $entry["type"])) {
1950
1951 if (!$hide_images) {
1952 $encsize = '';
1953 if ($entry['height'] > 0)
1954 $encsize .= ' height="' . intval($entry['height']) . '"';
1955 if ($entry['width'] > 0)
1956 $encsize .= ' width="' . intval($entry['width']) . '"';
1957 $rv .= "<p><img
1958 alt=\"".htmlspecialchars($entry["filename"])."\"
1959 src=\"" .htmlspecialchars($entry["url"]) . "\"
1960 " . $encsize . " /></p>";
1961 } else {
1962 $rv .= "<p><a target=\"_blank\" rel=\"noopener noreferrer\"
1963 href=\"".htmlspecialchars($entry["url"])."\"
1964 >" .htmlspecialchars($entry["url"]) . "</a></p>";
1965 }
1966
1967 if ($entry['title']) {
1968 $rv.= "<div class=\"enclosure_title\">${entry['title']}</div>";
1969 }
1970 }
1971 }
1972 }
1973 }
1974 }
1975
1976 if (count($entries_inline) > 0) {
1977 $rv .= "<hr clear='both'/>";
1978 foreach ($entries_inline as $entry) { $rv .= $entry; };
1979 $rv .= "<hr clear='both'/>";
1980 }
1981
1982 $rv .= "<div class=\"attachments\" dojoType=\"dijit.form.DropDownButton\">".
1983 "<span>" . __('Attachments')."</span>";
1984
1985 $rv .= "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
1986
1987 foreach ($entries as $entry) {
1988 if ($entry["title"])
1989 $title = " &mdash; " . truncate_string($entry["title"], 30);
1990 else
1991 $title = "";
1992
1993 if ($entry["filename"])
1994 $filename = truncate_middle(htmlspecialchars($entry["filename"]), 60);
1995 else
1996 $filename = "";
1997
1998 $rv .= "<div onclick='openUrlPopup(\"".htmlspecialchars($entry["url"])."\")'
1999 dojoType=\"dijit.MenuItem\">".$filename . $title."</div>";
2000
2001 };
2002
2003 $rv .= "</div>";
2004 $rv .= "</div>";
2005 }
2006
2007 return $rv;
2008 }
2009
2010 function getLastArticleId() {
2011 $result = db_query("SELECT ref_id AS id FROM ttrss_user_entries
2012 WHERE owner_uid = " . $_SESSION["uid"] . " ORDER BY ref_id DESC LIMIT 1");
2013
2014 if (db_num_rows($result) == 1) {
2015 return db_fetch_result($result, 0, "id");
2016 } else {
2017 return -1;
2018 }
2019 }
2020
2021 function build_url($parts) {
2022 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2023 }
2024
2025 function cleanup_url_path($path) {
2026 $path = str_replace("/./", "/", $path);
2027 $path = str_replace("//", "/", $path);
2028
2029 return $path;
2030 }
2031
2032 /**
2033 * Converts a (possibly) relative URL to a absolute one.
2034 *
2035 * @param string $url Base URL (i.e. from where the document is)
2036 * @param string $rel_url Possibly relative URL in the document
2037 *
2038 * @return string Absolute URL
2039 */
2040 function rewrite_relative_url($url, $rel_url) {
2041 if (strpos($rel_url, "://") !== false) {
2042 return $rel_url;
2043 } else if (strpos($rel_url, "//") === 0) {
2044 # protocol-relative URL (rare but they exist)
2045 return $rel_url;
2046 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2047 # magnet:, feed:, etc
2048 return $rel_url;
2049 } else if (strpos($rel_url, "/") === 0) {
2050 $parts = parse_url($url);
2051 $parts['path'] = $rel_url;
2052 $parts['path'] = cleanup_url_path($parts['path']);
2053
2054 return build_url($parts);
2055
2056 } else {
2057 $parts = parse_url($url);
2058 if (!isset($parts['path'])) {
2059 $parts['path'] = '/';
2060 }
2061 $dir = $parts['path'];
2062 if (substr($dir, -1) !== '/') {
2063 $dir = dirname($parts['path']);
2064 $dir !== '/' && $dir .= '/';
2065 }
2066 $parts['path'] = $dir . $rel_url;
2067 $parts['path'] = cleanup_url_path($parts['path']);
2068
2069 return build_url($parts);
2070 }
2071 }
2072
2073 function cleanup_tags($days = 14, $limit = 1000) {
2074
2075 if (DB_TYPE == "pgsql") {
2076 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2077 } else if (DB_TYPE == "mysql") {
2078 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2079 }
2080
2081 $tags_deleted = 0;
2082
2083 while ($limit > 0) {
2084 $limit_part = 500;
2085
2086 $query = "SELECT ttrss_tags.id AS id
2087 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2088 WHERE post_int_id = int_id AND $interval_query AND
2089 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT $limit_part";
2090
2091 $result = db_query($query);
2092
2093 $ids = array();
2094
2095 while ($line = db_fetch_assoc($result)) {
2096 array_push($ids, $line['id']);
2097 }
2098
2099 if (count($ids) > 0) {
2100 $ids = join(",", $ids);
2101
2102 $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2103 $tags_deleted += db_affected_rows($tmp_result);
2104 } else {
2105 break;
2106 }
2107
2108 $limit -= $limit_part;
2109 }
2110
2111 return $tags_deleted;
2112 }
2113
2114 function print_user_stylesheet() {
2115 $value = get_pref('USER_STYLESHEET');
2116
2117 if ($value) {
2118 print "<style type=\"text/css\">";
2119 print str_replace("<br/>", "\n", $value);
2120 print "</style>";
2121 }
2122
2123 }
2124
2125 function filter_to_sql($filter, $owner_uid) {
2126 $query = array();
2127
2128 if (DB_TYPE == "pgsql")
2129 $reg_qpart = "~";
2130 else
2131 $reg_qpart = "REGEXP";
2132
2133 foreach ($filter["rules"] AS $rule) {
2134 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2135 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2136 $rule['reg_exp']) !== FALSE;
2137
2138 if ($regexp_valid) {
2139
2140 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
2141
2142 switch ($rule["type"]) {
2143 case "title":
2144 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2145 $rule['reg_exp'] . "')";
2146 break;
2147 case "content":
2148 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2149 $rule['reg_exp'] . "')";
2150 break;
2151 case "both":
2152 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2153 $rule['reg_exp'] . "') OR LOWER(" .
2154 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2155 break;
2156 case "tag":
2157 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2158 $rule['reg_exp'] . "')";
2159 break;
2160 case "link":
2161 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2162 $rule['reg_exp'] . "')";
2163 break;
2164 case "author":
2165 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2166 $rule['reg_exp'] . "')";
2167 break;
2168 }
2169
2170 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2171
2172 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2173 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
2174 }
2175
2176 if (isset($rule["cat_id"])) {
2177
2178 if ($rule["cat_id"] > 0) {
2179 $children = getChildCategories($rule["cat_id"], $owner_uid);
2180 array_push($children, $rule["cat_id"]);
2181
2182 $children = join(",", $children);
2183
2184 $cat_qpart = "cat_id IN ($children)";
2185 } else {
2186 $cat_qpart = "cat_id IS NULL";
2187 }
2188
2189 $qpart .= " AND $cat_qpart";
2190 }
2191
2192 $qpart .= " AND feed_id IS NOT NULL";
2193
2194 array_push($query, "($qpart)");
2195
2196 }
2197 }
2198
2199 if (count($query) > 0) {
2200 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2201 } else {
2202 $fullquery = "(false)";
2203 }
2204
2205 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2206
2207 return $fullquery;
2208 }
2209
2210 if (!function_exists('gzdecode')) {
2211 function gzdecode($string) { // no support for 2nd argument
2212 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2213 base64_encode($string));
2214 }
2215 }
2216
2217 function get_random_bytes($length) {
2218 if (function_exists('openssl_random_pseudo_bytes')) {
2219 return openssl_random_pseudo_bytes($length);
2220 } else {
2221 $output = "";
2222
2223 for ($i = 0; $i < $length; $i++)
2224 $output .= chr(mt_rand(0, 255));
2225
2226 return $output;
2227 }
2228 }
2229
2230 function read_stdin() {
2231 $fp = fopen("php://stdin", "r");
2232
2233 if ($fp) {
2234 $line = trim(fgets($fp));
2235 fclose($fp);
2236 return $line;
2237 }
2238
2239 return null;
2240 }
2241
2242 function getFeedCategory($feed) {
2243 $result = db_query("SELECT cat_id FROM ttrss_feeds
2244 WHERE id = '$feed'");
2245
2246 if (db_num_rows($result) > 0) {
2247 return db_fetch_result($result, 0, "cat_id");
2248 } else {
2249 return false;
2250 }
2251
2252 }
2253
2254 function implements_interface($class, $interface) {
2255 return in_array($interface, class_implements($class));
2256 }
2257
2258 function get_minified_js($files) {
2259 require_once 'lib/jshrink/Minifier.php';
2260
2261 $rv = '';
2262
2263 foreach ($files as $js) {
2264 if (!isset($_GET['debug'])) {
2265 $cached_file = CACHE_DIR . "/js/".basename($js).".js";
2266
2267 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2268
2269 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2270
2271 if ($header && $contents) {
2272 list($htag, $hversion) = explode(":", $header);
2273
2274 if ($htag == "tt-rss" && $hversion == VERSION) {
2275 $rv .= $contents;
2276 continue;
2277 }
2278 }
2279 }
2280
2281 $minified = JShrink\Minifier::minify(file_get_contents("js/$js.js"));
2282 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
2283 $rv .= $minified;
2284
2285 } else {
2286 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2287 }
2288 }
2289
2290 return $rv;
2291 }
2292
2293 function calculate_dep_timestamp() {
2294 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2295
2296 $max_ts = -1;
2297
2298 foreach ($files as $file) {
2299 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2300 }
2301
2302 return $max_ts;
2303 }
2304
2305 function T_js_decl($s1, $s2) {
2306 if ($s1 && $s2) {
2307 $s1 = preg_replace("/\n/", "", $s1);
2308 $s2 = preg_replace("/\n/", "", $s2);
2309
2310 $s1 = preg_replace("/\"/", "\\\"", $s1);
2311 $s2 = preg_replace("/\"/", "\\\"", $s2);
2312
2313 return "T_messages[\"$s1\"] = \"$s2\";\n";
2314 }
2315 }
2316
2317 function init_js_translations() {
2318
2319 print 'var T_messages = new Object();
2320
2321 function __(msg) {
2322 if (T_messages[msg]) {
2323 return T_messages[msg];
2324 } else {
2325 return msg;
2326 }
2327 }
2328
2329 function ngettext(msg1, msg2, n) {
2330 return __((parseInt(n) > 1) ? msg2 : msg1);
2331 }';
2332
2333 $l10n = _get_reader();
2334
2335 for ($i = 0; $i < $l10n->total; $i++) {
2336 $orig = $l10n->get_original_string($i);
2337 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2338 $key = explode(chr(0), $orig);
2339 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2340 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2341 } else {
2342 $translation = __($orig);
2343 print T_js_decl($orig, $translation);
2344 }
2345 }
2346 }
2347
2348 function label_to_feed_id($label) {
2349 return LABEL_BASE_INDEX - 1 - abs($label);
2350 }
2351
2352 function feed_to_label_id($feed) {
2353 return LABEL_BASE_INDEX - 1 + abs($feed);
2354 }
2355
2356 function get_theme_path($theme) {
2357 $check = "themes/$theme";
2358 if (file_exists($check)) return $check;
2359
2360 $check = "themes.local/$theme";
2361 if (file_exists($check)) return $check;
2362 }
2363
2364 function theme_valid($theme) {
2365 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2366
2367 if (in_array($theme, $bundled_themes)) return true;
2368
2369 $file = "themes/" . basename($theme);
2370
2371 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2372
2373 if (file_exists($file) && is_readable($file)) {
2374 $fh = fopen($file, "r");
2375
2376 if ($fh) {
2377 $header = fgets($fh);
2378 fclose($fh);
2379
2380 return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
2381 }
2382 }
2383
2384 return false;
2385 }
2386
2387 /**
2388 * @SuppressWarnings(unused)
2389 */
2390 function error_json($code) {
2391 require_once "errors.php";
2392
2393 @$message = $ERRORS[$code];
2394
2395 return json_encode(array("error" =>
2396 array("code" => $code, "message" => $message)));
2397
2398 }
2399
2400 function abs_to_rel_path($dir) {
2401 $tmp = str_replace(dirname(__DIR__), "", $dir);
2402
2403 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2404
2405 return $tmp;
2406 }
2407
2408 function get_upload_error_message($code) {
2409
2410 $errors = array(
2411 0 => __('There is no error, the file uploaded with success'),
2412 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2413 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2414 3 => __('The uploaded file was only partially uploaded'),
2415 4 => __('No file was uploaded'),
2416 6 => __('Missing a temporary folder'),
2417 7 => __('Failed to write file to disk.'),
2418 8 => __('A PHP extension stopped the file upload.'),
2419 );
2420
2421 return $errors[$code];
2422 }
2423
2424 function base64_img($filename) {
2425 if (file_exists($filename)) {
2426 $ext = pathinfo($filename, PATHINFO_EXTENSION);
2427
2428 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2429 } else {
2430 return "";
2431 }
2432 }
2433