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