]> git.wh0rd.org - tt-rss.git/blob - include/functions2.php
add compact theme with smaller font
[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 return $params;
48 }
49
50 function get_hotkeys_info() {
51 $hotkeys = array(
52 __("Navigation") => array(
53 "next_feed" => __("Open next feed"),
54 "prev_feed" => __("Open previous feed"),
55 "next_article" => __("Open next article"),
56 "prev_article" => __("Open previous article"),
57 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
58 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
59 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
60 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
61 "search_dialog" => __("Show search dialog")),
62 __("Article") => array(
63 "toggle_mark" => __("Toggle starred"),
64 "toggle_publ" => __("Toggle published"),
65 "toggle_unread" => __("Toggle unread"),
66 "edit_tags" => __("Edit tags"),
67 "open_in_new_window" => __("Open in new window"),
68 "catchup_below" => __("Mark below as read"),
69 "catchup_above" => __("Mark above as read"),
70 "article_scroll_down" => __("Scroll down"),
71 "article_scroll_up" => __("Scroll up"),
72 "select_article_cursor" => __("Select article under cursor"),
73 "email_article" => __("Email article"),
74 "close_article" => __("Close/collapse article"),
75 "toggle_expand" => __("Toggle article expansion (combined mode)"),
76 "toggle_widescreen" => __("Toggle widescreen mode"),
77 "toggle_embed_original" => __("Toggle embed original")),
78 __("Article selection") => array(
79 "select_all" => __("Select all articles"),
80 "select_unread" => __("Select unread"),
81 "select_marked" => __("Select starred"),
82 "select_published" => __("Select published"),
83 "select_invert" => __("Invert selection"),
84 "select_none" => __("Deselect everything")),
85 __("Feed") => array(
86 "feed_refresh" => __("Refresh current feed"),
87 "feed_unhide_read" => __("Un/hide read feeds"),
88 "feed_subscribe" => __("Subscribe to feed"),
89 "feed_edit" => __("Edit feed"),
90 "feed_catchup" => __("Mark as read"),
91 "feed_reverse" => __("Reverse headlines"),
92 "feed_toggle_vgroup" => __("Toggle headline grouping"),
93 "feed_debug_update" => __("Debug feed update"),
94 "feed_debug_viewfeed" => __("Debug viewfeed()"),
95 "catchup_all" => __("Mark all feeds as read"),
96 "cat_toggle_collapse" => __("Un/collapse current category"),
97 "toggle_combined_mode" => __("Toggle combined mode"),
98 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
99 __("Go to") => array(
100 "goto_all" => __("All articles"),
101 "goto_fresh" => __("Fresh"),
102 "goto_marked" => __("Starred"),
103 "goto_published" => __("Published"),
104 "goto_tagcloud" => __("Tag cloud"),
105 "goto_prefs" => __("Preferences")),
106 __("Other") => array(
107 "create_label" => __("Create label"),
108 "create_filter" => __("Create filter"),
109 "collapse_sidebar" => __("Un/collapse sidebar"),
110 "help_dialog" => __("Show help dialog"))
111 );
112
113 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
114 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
115 }
116
117 return $hotkeys;
118 }
119
120 function get_hotkeys_map() {
121 $hotkeys = array(
122 // "navigation" => array(
123 "k" => "next_feed",
124 "j" => "prev_feed",
125 "n" => "next_article",
126 "p" => "prev_article",
127 "(38)|up" => "prev_article",
128 "(40)|down" => "next_article",
129 // "^(38)|Ctrl-up" => "prev_article_noscroll",
130 // "^(40)|Ctrl-down" => "next_article_noscroll",
131 "(191)|/" => "search_dialog",
132 // "article" => array(
133 "s" => "toggle_mark",
134 "*s" => "toggle_publ",
135 "u" => "toggle_unread",
136 "*t" => "edit_tags",
137 "o" => "open_in_new_window",
138 "c p" => "catchup_below",
139 "c n" => "catchup_above",
140 "*n" => "article_scroll_down",
141 "*p" => "article_scroll_up",
142 "*(38)|Shift+up" => "article_scroll_up",
143 "*(40)|Shift+down" => "article_scroll_down",
144 "a *w" => "toggle_widescreen",
145 "a e" => "toggle_embed_original",
146 "e" => "email_article",
147 "a q" => "close_article",
148 // "article_selection" => array(
149 "a a" => "select_all",
150 "a u" => "select_unread",
151 "a *u" => "select_marked",
152 "a p" => "select_published",
153 "a i" => "select_invert",
154 "a n" => "select_none",
155 // "feed" => array(
156 "f r" => "feed_refresh",
157 "f a" => "feed_unhide_read",
158 "f s" => "feed_subscribe",
159 "f e" => "feed_edit",
160 "f q" => "feed_catchup",
161 "f x" => "feed_reverse",
162 "f g" => "feed_toggle_vgroup",
163 "f *d" => "feed_debug_update",
164 "f *g" => "feed_debug_viewfeed",
165 "f *c" => "toggle_combined_mode",
166 "f c" => "toggle_cdm_expanded",
167 "*q" => "catchup_all",
168 "x" => "cat_toggle_collapse",
169 // "goto" => array(
170 "g a" => "goto_all",
171 "g f" => "goto_fresh",
172 "g s" => "goto_marked",
173 "g p" => "goto_published",
174 "g t" => "goto_tagcloud",
175 "g *p" => "goto_prefs",
176 // "other" => array(
177 "(9)|Tab" => "select_article_cursor", // tab
178 "c l" => "create_label",
179 "c f" => "create_filter",
180 "c s" => "collapse_sidebar",
181 "^(191)|Ctrl+/" => "help_dialog",
182 );
183
184 if (get_pref('COMBINED_DISPLAY_MODE')) {
185 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
186 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
187 }
188
189 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
190 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
191 }
192
193 $prefixes = array();
194
195 foreach (array_keys($hotkeys) as $hotkey) {
196 $pair = explode(" ", $hotkey, 2);
197
198 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
199 array_push($prefixes, $pair[0]);
200 }
201 }
202
203 return array($prefixes, $hotkeys);
204 }
205
206 function check_for_update() {
207 if (defined("GIT_VERSION_TIMESTAMP")) {
208 $content = @fetch_file_contents(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
209
210 if ($content) {
211 $content = json_decode($content, true);
212
213 if ($content && isset($content["changeset"])) {
214 if ((int)GIT_VERSION_TIMESTAMP < (int)$content["changeset"]["timestamp"] &&
215 GIT_VERSION_HEAD != $content["changeset"]["id"]) {
216
217 return $content["changeset"]["id"];
218 }
219 }
220 }
221 }
222
223 return "";
224 }
225
226 function make_runtime_info($disable_update_check = false) {
227 $data = array();
228
229 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
230 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
231
232 $max_feed_id = db_fetch_result($result, 0, "mid");
233 $num_feeds = db_fetch_result($result, 0, "nf");
234
235 $data["max_feed_id"] = (int) $max_feed_id;
236 $data["num_feeds"] = (int) $num_feeds;
237
238 $data['last_article_id'] = getLastArticleId();
239 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
240
241 $data['dep_ts'] = calculate_dep_timestamp();
242 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
243
244
245 if (CHECK_FOR_UPDATES && !$disable_update_check && $_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
246 $update_result = @check_for_update();
247
248 $data["update_result"] = $update_result;
249
250 $_SESSION["last_version_check"] = time();
251 }
252
253 if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
254
255 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
256
257 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
258
259 $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
260
261 if ($stamp) {
262 $stamp_delta = time() - $stamp;
263
264 if ($stamp_delta > 1800) {
265 $stamp_check = 0;
266 } else {
267 $stamp_check = 1;
268 $_SESSION["daemon_stamp_check"] = time();
269 }
270
271 $data['daemon_stamp_ok'] = $stamp_check;
272
273 $stamp_fmt = date("Y.m.d, G:i", $stamp);
274
275 $data['daemon_stamp'] = $stamp_fmt;
276 }
277 }
278 }
279
280 return $data;
281 }
282
283 function search_to_sql($search, $search_language) {
284
285 $keywords = str_getcsv(trim($search), " ");
286 $query_keywords = array();
287 $search_words = array();
288 $search_query_leftover = array();
289
290 if ($search_language)
291 $search_language = db_escape_string(mb_strtolower($search_language));
292 else
293 $search_language = "english";
294
295 foreach ($keywords as $k) {
296 if (strpos($k, "-") === 0) {
297 $k = substr($k, 1);
298 $not = "NOT";
299 } else {
300 $not = "";
301 }
302
303 $commandpair = explode(":", mb_strtolower($k), 2);
304
305 switch ($commandpair[0]) {
306 case "title":
307 if ($commandpair[1]) {
308 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE '%".
309 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
310 } else {
311 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
312 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
313 array_push($search_words, $k);
314 }
315 break;
316 case "author":
317 if ($commandpair[1]) {
318 array_push($query_keywords, "($not (LOWER(author) LIKE '%".
319 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
320 } else {
321 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
322 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
323 array_push($search_words, $k);
324 }
325 break;
326 case "note":
327 if ($commandpair[1]) {
328 if ($commandpair[1] == "true")
329 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
330 else if ($commandpair[1] == "false")
331 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
332 else
333 array_push($query_keywords, "($not (LOWER(note) LIKE '%".
334 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
335 } else {
336 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
337 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
338 if (!$not) array_push($search_words, $k);
339 }
340 break;
341 case "star":
342
343 if ($commandpair[1]) {
344 if ($commandpair[1] == "true")
345 array_push($query_keywords, "($not (marked = true))");
346 else
347 array_push($query_keywords, "($not (marked = false))");
348 } else {
349 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
350 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
351 if (!$not) array_push($search_words, $k);
352 }
353 break;
354 case "pub":
355 if ($commandpair[1]) {
356 if ($commandpair[1] == "true")
357 array_push($query_keywords, "($not (published = true))");
358 else
359 array_push($query_keywords, "($not (published = false))");
360
361 } else {
362 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
363 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
364 if (!$not) array_push($search_words, $k);
365 }
366 break;
367 case "unread":
368 if ($commandpair[1]) {
369 if ($commandpair[1] == "true")
370 array_push($query_keywords, "($not (unread = true))");
371 else
372 array_push($query_keywords, "($not (unread = false))");
373
374 } else {
375 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
376 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
377 if (!$not) array_push($search_words, $k);
378 }
379 break;
380 default:
381 if (strpos($k, "@") === 0) {
382
383 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
384 $orig_ts = strtotime(substr($k, 1));
385 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
386
387 //$k = date("Y-m-d", strtotime(substr($k, 1)));
388
389 array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
390 } else {
391
392 if (DB_TYPE == "pgsql") {
393 $k = mb_strtolower($k);
394 array_push($search_query_leftover, $not ? "!$k" : $k);
395 } else {
396 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
397 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
398 }
399
400 if (!$not) array_push($search_words, $k);
401 }
402 }
403 }
404
405 if (count($search_query_leftover) > 0) {
406 $search_query_leftover = db_escape_string(implode(" & ", $search_query_leftover));
407
408 if (DB_TYPE == "pgsql") {
409 array_push($query_keywords,
410 "(tsvector_combined @@ to_tsquery('$search_language', '$search_query_leftover'))");
411 }
412
413 }
414
415 $search_query_part = implode("AND", $query_keywords);
416
417 return array($search_query_part, $search_words);
418 }
419
420 function getParentCategories($cat, $owner_uid) {
421 $rv = array();
422
423 $result = db_query("SELECT parent_cat FROM ttrss_feed_categories
424 WHERE id = '$cat' AND parent_cat IS NOT NULL AND owner_uid = $owner_uid");
425
426 while ($line = db_fetch_assoc($result)) {
427 array_push($rv, $line["parent_cat"]);
428 $rv = array_merge($rv, getParentCategories($line["parent_cat"], $owner_uid));
429 }
430
431 return $rv;
432 }
433
434 function getChildCategories($cat, $owner_uid) {
435 $rv = array();
436
437 $result = db_query("SELECT id FROM ttrss_feed_categories
438 WHERE parent_cat = '$cat' AND owner_uid = $owner_uid");
439
440 while ($line = db_fetch_assoc($result)) {
441 array_push($rv, $line["id"]);
442 $rv = array_merge($rv, getChildCategories($line["id"], $owner_uid));
443 }
444
445 return $rv;
446 }
447
448 function queryFeedHeadlines($params) {
449
450 $feed = $params["feed"];
451 $limit = isset($params["limit"]) ? $params["limit"] : 30;
452 $view_mode = $params["view_mode"];
453 $cat_view = isset($params["cat_view"]) ? $params["cat_view"] : false;
454 $search = isset($params["search"]) ? $params["search"] : false;
455 $search_language = isset($params["search_language"]) ? $params["search_language"] : "";
456 $override_order = isset($params["override_order"]) ? $params["override_order"] : false;
457 $offset = isset($params["offset"]) ? $params["offset"] : 0;
458 $owner_uid = isset($params["owner_uid"]) ? $params["owner_uid"] : $_SESSION["uid"];
459 $since_id = isset($params["since_id"]) ? $params["since_id"] : 0;
460 $include_children = isset($params["include_children"]) ? $params["include_children"] : false;
461 $ignore_vfeed_group = isset($params["ignore_vfeed_group"]) ? $params["ignore_vfeed_group"] : false;
462 $override_strategy = isset($params["override_strategy"]) ? $params["override_strategy"] : false;
463 $override_vfeed = isset($params["override_vfeed"]) ? $params["override_vfeed"] : false;
464 $start_ts = isset($params["start_ts"]) ? $params["start_ts"] : false;
465 $check_first_id = isset($params["check_first_id"]) ? $params["check_first_id"] : false;
466 $skip_first_id_check = isset($params["skip_first_id_check"]) ? $params["skip_first_id_check"] : false;
467
468 $ext_tables_part = "";
469 $query_strategy_part = "";
470
471 $search_words = array();
472
473 if ($search) {
474 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH) as $plugin) {
475 list($search_query_part, $search_words) = $plugin->hook_search($search);
476 break;
477 }
478
479 // fall back in case of no plugins
480 if (!$search_query_part) {
481 list($search_query_part, $search_words) = search_to_sql($search, $search_language);
482 }
483 $search_query_part .= " AND ";
484 } else {
485 $search_query_part = "";
486 }
487
488 if ($since_id) {
489 $since_id_part = "ttrss_entries.id > $since_id AND ";
490 } else {
491 $since_id_part = "";
492 }
493
494 $view_query_part = "";
495
496 if ($view_mode == "adaptive") {
497 if ($search) {
498 $view_query_part = " ";
499 } else if ($feed != -1) {
500
501 $unread = getFeedUnread($feed, $cat_view);
502
503 if ($cat_view && $feed > 0 && $include_children)
504 $unread += getCategoryChildrenUnread($feed);
505
506 if ($unread > 0) {
507 $view_query_part = " unread = true AND ";
508 }
509 }
510 }
511
512 if ($view_mode == "marked") {
513 $view_query_part = " marked = true AND ";
514 }
515
516 if ($view_mode == "has_note") {
517 $view_query_part = " (note IS NOT NULL AND note != '') AND ";
518 }
519
520 if ($view_mode == "published") {
521 $view_query_part = " published = true AND ";
522 }
523
524 if ($view_mode == "unread" && $feed != -6) {
525 $view_query_part = " unread = true AND ";
526 }
527
528 if ($limit > 0) {
529 $limit_query_part = "LIMIT " . $limit;
530 }
531
532 $allow_archived = false;
533
534 $vfeed_query_part = "";
535
536 /* tags */
537 if (!is_numeric($feed)) {
538 $query_strategy_part = "true";
539 $vfeed_query_part = "(SELECT title FROM ttrss_feeds WHERE
540 id = feed_id) as feed_title,";
541 } else if ($feed > 0) {
542
543 if ($cat_view) {
544
545 if ($feed > 0) {
546 if ($include_children) {
547 # sub-cats
548 $subcats = getChildCategories($feed, $owner_uid);
549
550 array_push($subcats, $feed);
551 $query_strategy_part = "cat_id IN (".
552 implode(",", $subcats).")";
553
554 } else {
555 $query_strategy_part = "cat_id = '$feed'";
556 }
557
558 } else {
559 $query_strategy_part = "cat_id IS NULL";
560 }
561
562 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
563
564 } else {
565 $query_strategy_part = "feed_id = '$feed'";
566 }
567 } else if ($feed == 0 && !$cat_view) { // archive virtual feed
568 $query_strategy_part = "feed_id IS NULL";
569 $allow_archived = true;
570 } else if ($feed == 0 && $cat_view) { // uncategorized
571 $query_strategy_part = "cat_id IS NULL AND feed_id IS NOT NULL";
572 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
573 } else if ($feed == -1) { // starred virtual feed
574 $query_strategy_part = "marked = true";
575 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
576 $allow_archived = true;
577
578 if (!$override_order) {
579 $override_order = "last_marked DESC, date_entered DESC, updated DESC";
580 }
581
582 } else if ($feed == -2) { // published virtual feed OR labels category
583
584 if (!$cat_view) {
585 $query_strategy_part = "published = true";
586 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
587 $allow_archived = true;
588
589 if (!$override_order) {
590 $override_order = "last_published DESC, date_entered DESC, updated DESC";
591 }
592
593 } else {
594 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
595
596 $ext_tables_part = "ttrss_labels2,ttrss_user_labels2,";
597
598 $query_strategy_part = "ttrss_labels2.id = ttrss_user_labels2.label_id AND
599 ttrss_user_labels2.article_id = ref_id";
600
601 }
602 } else if ($feed == -6) { // recently read
603 $query_strategy_part = "unread = false AND last_read IS NOT NULL";
604
605 if (DB_TYPE == "pgsql") {
606 $query_strategy_part .= " AND last_read > NOW() - INTERVAL '1 DAY' ";
607 } else {
608 $query_strategy_part .= " AND last_read > DATE_SUB(NOW(), INTERVAL 1 DAY) ";
609 }
610
611 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
612 $allow_archived = true;
613 $ignore_vfeed_group = true;
614
615 if (!$override_order) $override_order = "last_read DESC";
616
617 } else if ($feed == -3) { // fresh virtual feed
618 $query_strategy_part = "unread = true AND score >= 0";
619
620 $intl = get_pref("FRESH_ARTICLE_MAX_AGE", $owner_uid);
621
622 if (DB_TYPE == "pgsql") {
623 $query_strategy_part .= " AND date_entered > NOW() - INTERVAL '$intl hour' ";
624 } else {
625 $query_strategy_part .= " AND date_entered > DATE_SUB(NOW(), INTERVAL $intl HOUR) ";
626 }
627
628 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
629 } else if ($feed == -4) { // all articles virtual feed
630 $allow_archived = true;
631 $query_strategy_part = "true";
632 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
633 } else if ($feed <= LABEL_BASE_INDEX) { // labels
634 $label_id = feed_to_label_id($feed);
635
636 $query_strategy_part = "label_id = '$label_id' AND
637 ttrss_labels2.id = ttrss_user_labels2.label_id AND
638 ttrss_user_labels2.article_id = ref_id";
639
640 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
641 $ext_tables_part = "ttrss_labels2,ttrss_user_labels2,";
642 $allow_archived = true;
643
644 } else {
645 $query_strategy_part = "true";
646 }
647
648 $order_by = "score DESC, date_entered DESC, updated DESC";
649
650 if ($override_order) {
651 $order_by = $override_order;
652 }
653
654 if ($override_strategy) {
655 $query_strategy_part = $override_strategy;
656 }
657
658 if ($override_vfeed) {
659 $vfeed_query_part = $override_vfeed;
660 }
661
662 $feed_title = "";
663
664 if ($search) {
665 $feed_title = T_sprintf("Search results: %s", $search);
666 } else {
667 if ($cat_view) {
668 $feed_title = getCategoryTitle($feed);
669 } else {
670 if (is_numeric($feed) && $feed > 0) {
671 $result = db_query("SELECT title,site_url,last_error,last_updated
672 FROM ttrss_feeds WHERE id = '$feed' AND owner_uid = $owner_uid");
673
674 $feed_title = db_fetch_result($result, 0, "title");
675 $feed_site_url = db_fetch_result($result, 0, "site_url");
676 $last_error = db_fetch_result($result, 0, "last_error");
677 $last_updated = db_fetch_result($result, 0, "last_updated");
678 } else {
679 $feed_title = getFeedTitle($feed);
680 }
681 }
682 }
683
684
685 $content_query_part = "content, ";
686
687 if ($limit_query_part) {
688 $offset_query_part = "OFFSET $offset";
689 } else {
690 $offset_query_part = "";
691 }
692
693 if (is_numeric($feed)) {
694 // proper override_order applied above
695 if ($vfeed_query_part && !$ignore_vfeed_group && get_pref('VFEED_GROUP_BY_FEED', $owner_uid)) {
696 if (!$override_order) {
697 $order_by = "ttrss_feeds.title, $order_by";
698 } else {
699 $order_by = "ttrss_feeds.title, $override_order";
700 }
701 }
702
703 if (!$allow_archived) {
704 $from_qpart = "${ext_tables_part}ttrss_entries LEFT JOIN ttrss_user_entries ON (ref_id = ttrss_entries.id),ttrss_feeds";
705 $feed_check_qpart = "ttrss_user_entries.feed_id = ttrss_feeds.id AND";
706
707 } else {
708 $from_qpart = "${ext_tables_part}ttrss_entries LEFT JOIN ttrss_user_entries ON (ref_id = ttrss_entries.id)
709 LEFT JOIN ttrss_feeds ON (feed_id = ttrss_feeds.id)";
710 }
711
712 if ($vfeed_query_part) $vfeed_query_part .= "favicon_avg_color,";
713
714 if ($start_ts) {
715 $start_ts_formatted = date("Y/m/d H:i:s", strtotime($start_ts));
716 $start_ts_query_part = "date_entered >= '$start_ts_formatted' AND";
717 } else {
718 $start_ts_query_part = "";
719 }
720
721 $first_id = 0;
722 $first_id_query_strategy_part = $query_strategy_part;
723
724 if ($feed == -3)
725 $first_id_query_strategy_part = "true";
726
727 if (DB_TYPE == "pgsql") {
728 $sanity_interval_qpart = "date_entered >= NOW() - INTERVAL '1 hour' AND";
729 } else {
730 $sanity_interval_qpart = "date_entered >= DATE_SUB(NOW(), INTERVAL 1 hour) AND";
731 }
732
733 if (!$search && !$skip_first_id_check) {
734 // if previous topmost article id changed that means our current pagination is no longer valid
735 $query = "SELECT DISTINCT
736 ttrss_feeds.title,
737 date_entered,
738 guid,
739 ttrss_entries.id,
740 ttrss_entries.title,
741 updated,
742 score,
743 marked,
744 published,
745 last_marked,
746 last_published,
747 last_read
748 FROM
749 $from_qpart
750 WHERE
751 $feed_check_qpart
752 ttrss_user_entries.owner_uid = '$owner_uid' AND
753 $search_query_part
754 $start_ts_query_part
755 $since_id_part
756 $sanity_interval_qpart
757 $first_id_query_strategy_part ORDER BY $order_by LIMIT 1";
758
759 if ($_REQUEST["debug"]) {
760 print $query;
761 }
762
763 $result = db_query($query);
764 if ($result && db_num_rows($result) > 0) {
765 $first_id = (int)db_fetch_result($result, 0, "id");
766
767 if ($offset > 0 && $first_id && $check_first_id && $first_id != $check_first_id) {
768 return array(-1, $feed_title, $feed_site_url, $last_error, $last_updated, $search_words, $first_id);
769 }
770 }
771 }
772
773 $query = "SELECT DISTINCT
774 date_entered,
775 guid,
776 ttrss_entries.id,ttrss_entries.title,
777 updated,
778 label_cache,
779 tag_cache,
780 always_display_enclosures,
781 site_url,
782 note,
783 num_comments,
784 comments,
785 int_id,
786 uuid,
787 lang,
788 hide_images,
789 unread,feed_id,marked,published,link,last_read,orig_feed_id,
790 last_marked, last_published,
791 $vfeed_query_part
792 $content_query_part
793 author,score
794 FROM
795 $from_qpart
796 WHERE
797 $feed_check_qpart
798 ttrss_user_entries.owner_uid = '$owner_uid' AND
799 $search_query_part
800 $start_ts_query_part
801 $view_query_part
802 $since_id_part
803 $query_strategy_part ORDER BY $order_by
804 $limit_query_part $offset_query_part";
805
806 if ($_REQUEST["debug"]) print $query;
807
808 $result = db_query($query);
809
810 } else {
811 // browsing by tag
812
813 $query = "SELECT DISTINCT
814 date_entered,
815 guid,
816 note,
817 ttrss_entries.id as id,
818 title,
819 updated,
820 unread,
821 feed_id,
822 orig_feed_id,
823 marked,
824 num_comments,
825 comments,
826 int_id,
827 tag_cache,
828 label_cache,
829 link,
830 lang,
831 uuid,
832 last_read,
833 (SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) AS hide_images,
834 last_marked, last_published,
835 $since_id_part
836 $vfeed_query_part
837 $content_query_part
838 author, score
839 FROM ttrss_entries, ttrss_user_entries, ttrss_tags
840 WHERE
841 ref_id = ttrss_entries.id AND
842 ttrss_user_entries.owner_uid = $owner_uid AND
843 post_int_id = int_id AND
844 tag_name = '$feed' AND
845 $view_query_part
846 $search_query_part
847 $query_strategy_part ORDER BY $order_by
848 $limit_query_part $offset_query_part";
849
850 if ($_REQUEST["debug"]) print $query;
851
852 $result = db_query($query);
853 }
854
855 return array($result, $feed_title, $feed_site_url, $last_error, $last_updated, $search_words, $first_id);
856
857 }
858
859 function iframe_whitelisted($entry) {
860 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
861
862 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
863
864 if ($src) {
865 foreach ($whitelist as $w) {
866 if ($src == $w || $src == "www.$w")
867 return true;
868 }
869 }
870
871 return false;
872 }
873
874 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
875 if (!$owner) $owner = $_SESSION["uid"];
876
877 $res = trim($str); if (!$res) return '';
878
879 $charset_hack = '<head>
880 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
881 </head>';
882
883 $res = trim($res); if (!$res) return '';
884
885 libxml_use_internal_errors(true);
886
887 $doc = new DOMDocument();
888 $doc->loadHTML($charset_hack . $res);
889 $xpath = new DOMXPath($doc);
890
891 $entries = $xpath->query('(//a[@href]|//img[@src])');
892
893 $ttrss_uses_https = parse_url(get_self_url_prefix(), PHP_URL_SCHEME) === 'https';
894
895 foreach ($entries as $entry) {
896
897 if ($site_url) {
898
899 if ($entry->hasAttribute('href')) {
900 $entry->setAttribute('href',
901 rewrite_relative_url($site_url, $entry->getAttribute('href')));
902
903 $entry->setAttribute('rel', 'noopener noreferrer');
904 }
905
906 if ($entry->hasAttribute('src')) {
907 $src = rewrite_relative_url($site_url, $entry->getAttribute('src'));
908
909 $cached_filename = CACHE_DIR . '/images/' . sha1($src) . '.png';
910
911 if (file_exists($cached_filename)) {
912 $src = SELF_URL_PATH . '/public.php?op=cached_image&hash=' . sha1($src);
913
914 if ($entry->hasAttribute('srcset')) {
915 $entry->removeAttribute('srcset');
916 }
917
918 if ($entry->hasAttribute('sizes')) {
919 $entry->removeAttribute('sizes');
920 }
921 }
922
923 $entry->setAttribute('src', $src);
924 }
925
926 if ($entry->nodeName == 'img') {
927 if ($entry->hasAttribute('src')) {
928 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
929
930 if ($ttrss_uses_https && !$is_https_url) {
931
932 if ($entry->hasAttribute('srcset')) {
933 $entry->removeAttribute('srcset');
934 }
935
936 if ($entry->hasAttribute('sizes')) {
937 $entry->removeAttribute('sizes');
938 }
939 }
940 }
941
942 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
943 $force_remove_images || $_SESSION["bw_limit"]) {
944
945 $p = $doc->createElement('p');
946
947 $a = $doc->createElement('a');
948 $a->setAttribute('href', $entry->getAttribute('src'));
949
950 $a->appendChild(new DOMText($entry->getAttribute('src')));
951 $a->setAttribute('target', '_blank');
952
953 $p->appendChild($a);
954
955 $entry->parentNode->replaceChild($p, $entry);
956 }
957 }
958 }
959
960 if (strtolower($entry->nodeName) == "a") {
961 $entry->setAttribute("target", "_blank");
962 }
963 }
964
965 $entries = $xpath->query('//iframe');
966 foreach ($entries as $entry) {
967 if (!iframe_whitelisted($entry)) {
968 $entry->setAttribute('sandbox', 'allow-scripts');
969 } else {
970 if ($_SERVER['HTTPS'] == "on") {
971 $entry->setAttribute("src",
972 str_replace("http://", "https://",
973 $entry->getAttribute("src")));
974 }
975 }
976 }
977
978 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
979 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
980 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
981 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
982 'dt', 'em', 'footer', 'figure', 'figcaption',
983 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'html', 'i',
984 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
985 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
986 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
987 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
988 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
989
990 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
991
992 $disallowed_attributes = array('id', 'style', 'class');
993
994 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
995 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
996 if (is_array($retval)) {
997 $doc = $retval[0];
998 $allowed_elements = $retval[1];
999 $disallowed_attributes = $retval[2];
1000 } else {
1001 $doc = $retval;
1002 }
1003 }
1004
1005 $doc->removeChild($doc->firstChild); //remove doctype
1006 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1007
1008 if ($highlight_words) {
1009 foreach ($highlight_words as $word) {
1010
1011 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1012
1013 $elements = $xpath->query("//*/text()");
1014
1015 foreach ($elements as $child) {
1016
1017 $fragment = $doc->createDocumentFragment();
1018 $text = $child->textContent;
1019
1020 while (($pos = mb_stripos($text, $word)) !== false) {
1021 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1022 $word = mb_substr($text, $pos, mb_strlen($word));
1023 $highlight = $doc->createElement('span');
1024 $highlight->appendChild(new DomText($word));
1025 $highlight->setAttribute('class', 'highlight');
1026 $fragment->appendChild($highlight);
1027 $text = mb_substr($text, $pos + mb_strlen($word));
1028 }
1029
1030 if (!empty($text)) $fragment->appendChild(new DomText($text));
1031
1032 $child->parentNode->replaceChild($fragment, $child);
1033 }
1034 }
1035 }
1036
1037 $res = $doc->saveHTML();
1038
1039 /* strip everything outside of <body>...</body> */
1040
1041 $res_frag = array();
1042 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1043 return $res_frag[1];
1044 } else {
1045 return $res;
1046 }
1047 }
1048
1049 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1050 $xpath = new DOMXPath($doc);
1051 $entries = $xpath->query('//*');
1052
1053 foreach ($entries as $entry) {
1054 if (!in_array($entry->nodeName, $allowed_elements)) {
1055 $entry->parentNode->removeChild($entry);
1056 }
1057
1058 if ($entry->hasAttributes()) {
1059 $attrs_to_remove = array();
1060
1061 foreach ($entry->attributes as $attr) {
1062
1063 if (strpos($attr->nodeName, 'on') === 0) {
1064 array_push($attrs_to_remove, $attr);
1065 }
1066
1067 if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1068 array_push($attrs_to_remove, $attr);
1069 }
1070
1071 if (in_array($attr->nodeName, $disallowed_attributes)) {
1072 array_push($attrs_to_remove, $attr);
1073 }
1074 }
1075
1076 foreach ($attrs_to_remove as $attr) {
1077 $entry->removeAttributeNode($attr);
1078 }
1079 }
1080 }
1081
1082 return $doc;
1083 }
1084
1085 function catchupArticlesById($ids, $cmode, $owner_uid = false) {
1086
1087 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
1088 if (count($ids) == 0) return;
1089
1090 $tmp_ids = array();
1091
1092 foreach ($ids as $id) {
1093 array_push($tmp_ids, "ref_id = '$id'");
1094 }
1095
1096 $ids_qpart = join(" OR ", $tmp_ids);
1097
1098 if ($cmode == 0) {
1099 db_query("UPDATE ttrss_user_entries SET
1100 unread = false,last_read = NOW()
1101 WHERE ($ids_qpart) AND owner_uid = $owner_uid");
1102 } else if ($cmode == 1) {
1103 db_query("UPDATE ttrss_user_entries SET
1104 unread = true
1105 WHERE ($ids_qpart) AND owner_uid = $owner_uid");
1106 } else {
1107 db_query("UPDATE ttrss_user_entries SET
1108 unread = NOT unread,last_read = NOW()
1109 WHERE ($ids_qpart) AND owner_uid = $owner_uid");
1110 }
1111
1112 /* update ccache */
1113
1114 $result = db_query("SELECT DISTINCT feed_id FROM ttrss_user_entries
1115 WHERE ($ids_qpart) AND owner_uid = $owner_uid");
1116
1117 while ($line = db_fetch_assoc($result)) {
1118 ccache_update($line["feed_id"], $owner_uid);
1119 }
1120 }
1121
1122 function get_article_tags($id, $owner_uid = 0, $tag_cache = false) {
1123
1124 $a_id = db_escape_string($id);
1125
1126 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
1127
1128 $query = "SELECT DISTINCT tag_name,
1129 owner_uid as owner FROM
1130 ttrss_tags WHERE post_int_id = (SELECT int_id FROM ttrss_user_entries WHERE
1131 ref_id = '$a_id' AND owner_uid = '$owner_uid' LIMIT 1) ORDER BY tag_name";
1132
1133 $tags = array();
1134
1135 /* check cache first */
1136
1137 if ($tag_cache === false) {
1138 $result = db_query("SELECT tag_cache FROM ttrss_user_entries
1139 WHERE ref_id = '$id' AND owner_uid = $owner_uid");
1140
1141 if (db_num_rows($result) != 0)
1142 $tag_cache = db_fetch_result($result, 0, "tag_cache");
1143 }
1144
1145 if ($tag_cache) {
1146 $tags = explode(",", $tag_cache);
1147 } else {
1148
1149 /* do it the hard way */
1150
1151 $tmp_result = db_query($query);
1152
1153 while ($tmp_line = db_fetch_assoc($tmp_result)) {
1154 array_push($tags, $tmp_line["tag_name"]);
1155 }
1156
1157 /* update the cache */
1158
1159 $tags_str = db_escape_string(join(",", $tags));
1160
1161 db_query("UPDATE ttrss_user_entries
1162 SET tag_cache = '$tags_str' WHERE ref_id = '$id'
1163 AND owner_uid = $owner_uid");
1164 }
1165
1166 return $tags;
1167 }
1168
1169 function trim_array($array) {
1170 $tmp = $array;
1171 array_walk($tmp, 'trim');
1172 return $tmp;
1173 }
1174
1175 function tag_is_valid($tag) {
1176 if ($tag == '') return false;
1177 if (is_numeric($tag)) return false;
1178 if (mb_strlen($tag) > 250) return false;
1179
1180 if (!$tag) return false;
1181
1182 return true;
1183 }
1184
1185 function render_login_form() {
1186 header('Cache-Control: public');
1187
1188 require_once "login_form.php";
1189 exit;
1190 }
1191
1192 function format_warning($msg, $id = "") {
1193 return "<div class=\"alert\" id=\"$id\">$msg</div>";
1194 }
1195
1196 function format_notice($msg, $id = "") {
1197 return "<div class=\"alert alert-info\" id=\"$id\">$msg</div>";
1198 }
1199
1200 function format_error($msg, $id = "") {
1201 return "<div class=\"alert alert-danger\" id=\"$id\">$msg</div>";
1202 }
1203
1204 function print_notice($msg) {
1205 return print format_notice($msg);
1206 }
1207
1208 function print_warning($msg) {
1209 return print format_warning($msg);
1210 }
1211
1212 function print_error($msg) {
1213 return print format_error($msg);
1214 }
1215
1216
1217 function T_sprintf() {
1218 $args = func_get_args();
1219 return vsprintf(__(array_shift($args)), $args);
1220 }
1221
1222 function format_inline_player($url, $ctype) {
1223
1224 $entry = "";
1225
1226 $url = htmlspecialchars($url);
1227
1228 if (strpos($ctype, "audio/") === 0) {
1229
1230 if ($_SESSION["hasAudio"] && (strpos($ctype, "ogg") !== false ||
1231 $_SESSION["hasMp3"])) {
1232
1233 $entry .= "<audio preload=\"none\" controls>
1234 <source type=\"$ctype\" src=\"$url\"/>
1235 </audio>";
1236
1237 } else {
1238
1239 $entry .= "<object type=\"application/x-shockwave-flash\"
1240 data=\"lib/button/musicplayer.swf?song_url=$url\"
1241 width=\"17\" height=\"17\" style='float : left; margin-right : 5px;'>
1242 <param name=\"movie\"
1243 value=\"lib/button/musicplayer.swf?song_url=$url\" />
1244 </object>";
1245 }
1246
1247 if ($entry) $entry .= "&nbsp; <a target=\"_blank\"
1248 href=\"$url\">" . basename($url) . "</a>";
1249
1250 return $entry;
1251
1252 }
1253
1254 return "";
1255
1256 /* $filename = substr($url, strrpos($url, "/")+1);
1257
1258 $entry .= " <a target=\"_blank\" href=\"" . htmlspecialchars($url) . "\">" .
1259 $filename . " (" . $ctype . ")" . "</a>"; */
1260
1261 }
1262
1263 function format_article($id, $mark_as_read = true, $zoom_mode = false, $owner_uid = false) {
1264 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
1265
1266 $rv = array();
1267
1268 $rv['id'] = $id;
1269
1270 /* we can figure out feed_id from article id anyway, why do we
1271 * pass feed_id here? let's ignore the argument :(*/
1272
1273 $result = db_query("SELECT feed_id FROM ttrss_user_entries
1274 WHERE ref_id = '$id'");
1275
1276 $feed_id = (int) db_fetch_result($result, 0, "feed_id");
1277
1278 $rv['feed_id'] = $feed_id;
1279
1280 //if (!$zoom_mode) { print "<article id='$id'><![CDATA["; };
1281
1282 if ($mark_as_read) {
1283 $result = db_query("UPDATE ttrss_user_entries
1284 SET unread = false,last_read = NOW()
1285 WHERE ref_id = '$id' AND owner_uid = $owner_uid");
1286
1287 ccache_update($feed_id, $owner_uid);
1288 }
1289
1290 $result = db_query("SELECT id,title,link,content,feed_id,comments,int_id,lang,
1291 ".SUBSTRING_FOR_DATE."(updated,1,16) as updated,
1292 (SELECT site_url FROM ttrss_feeds WHERE id = feed_id) as site_url,
1293 (SELECT title FROM ttrss_feeds WHERE id = feed_id) as feed_title,
1294 (SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) as hide_images,
1295 (SELECT always_display_enclosures FROM ttrss_feeds WHERE id = feed_id) as always_display_enclosures,
1296 num_comments,
1297 tag_cache,
1298 author,
1299 guid,
1300 orig_feed_id,
1301 note
1302 FROM ttrss_entries,ttrss_user_entries
1303 WHERE id = '$id' AND ref_id = id AND owner_uid = $owner_uid");
1304
1305 if ($result) {
1306
1307 $line = db_fetch_assoc($result);
1308
1309 $line["tags"] = get_article_tags($id, $owner_uid, $line["tag_cache"]);
1310 unset($line["tag_cache"]);
1311
1312 $line["content"] = sanitize($line["content"],
1313 sql_bool_to_bool($line['hide_images']),
1314 $owner_uid, $line["site_url"], false, $line["id"]);
1315
1316 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE) as $p) {
1317 $line = $p->hook_render_article($line);
1318 }
1319
1320 $num_comments = (int) $line["num_comments"];
1321 $entry_comments = "";
1322
1323 if ($num_comments > 0) {
1324 if ($line["comments"]) {
1325 $comments_url = htmlspecialchars($line["comments"]);
1326 } else {
1327 $comments_url = htmlspecialchars($line["link"]);
1328 }
1329 $entry_comments = "<a class=\"postComments\"
1330 target='_blank' href=\"$comments_url\">$num_comments ".
1331 _ngettext("comment", "comments", $num_comments)."</a>";
1332
1333 } else {
1334 if ($line["comments"] && $line["link"] != $line["comments"]) {
1335 $entry_comments = "<a class=\"postComments\" target='_blank' href=\"".htmlspecialchars($line["comments"])."\">".__("comments")."</a>";
1336 }
1337 }
1338
1339 if ($zoom_mode) {
1340 header("Content-Type: text/html");
1341 $rv['content'] .= "<html><head>
1342 <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>
1343 <title>Tiny Tiny RSS - ".$line["title"]."</title>".
1344 stylesheet_tag("css/tt-rss.css").
1345 stylesheet_tag("css/zoom.css").
1346 stylesheet_tag("css/dijit.css")."
1347
1348 <link rel=\"shortcut icon\" type=\"image/png\" href=\"images/favicon.png\">
1349 <link rel=\"icon\" type=\"image/png\" sizes=\"72x72\" href=\"images/favicon-72px.png\">
1350
1351 </head><body id=\"ttrssZoom\">";
1352 }
1353
1354 $rv['content'] .= "<div class=\"postReply\" id=\"POST-$id\">";
1355
1356 $rv['content'] .= "<div class=\"postHeader\" id=\"POSTHDR-$id\">";
1357
1358 $entry_author = $line["author"];
1359
1360 if ($entry_author) {
1361 $entry_author = __(" - ") . $entry_author;
1362 }
1363
1364 $parsed_updated = make_local_datetime($line["updated"], true,
1365 $owner_uid, true);
1366
1367 if (!$zoom_mode)
1368 $rv['content'] .= "<div class=\"postDate\">$parsed_updated</div>";
1369
1370 if ($line["link"]) {
1371 $rv['content'] .= "<div class='postTitle'><a target='_blank'
1372 title=\"".htmlspecialchars($line['title'])."\"
1373 href=\"" .
1374 htmlspecialchars($line["link"]) . "\">" .
1375 $line["title"] . "</a>" .
1376 "<span class='author'>$entry_author</span></div>";
1377 } else {
1378 $rv['content'] .= "<div class='postTitle'>" . $line["title"] . "$entry_author</div>";
1379 }
1380
1381 if ($zoom_mode) {
1382 $feed_title = htmlspecialchars($line["feed_title"]);
1383
1384 $rv['content'] .= "<div class=\"postFeedTitle\">$feed_title</div>";
1385
1386 $rv['content'] .= "<div class=\"postDate\">$parsed_updated</div>";
1387 }
1388
1389 $tags_str = format_tags_string($line["tags"], $id);
1390 $tags_str_full = join(", ", $line["tags"]);
1391
1392 if (!$tags_str_full) $tags_str_full = __("no tags");
1393
1394 if (!$entry_comments) $entry_comments = "&nbsp;"; # placeholder
1395
1396 $rv['content'] .= "<div class='postTags' style='float : right'>
1397 <img src='images/tag.png'
1398 class='tagsPic' alt='Tags' title='Tags'>&nbsp;";
1399
1400 if (!$zoom_mode) {
1401 $rv['content'] .= "<span id=\"ATSTR-$id\">$tags_str</span>
1402 <a title=\"".__('Edit tags for this article')."\"
1403 href=\"#\" onclick=\"editArticleTags($id, $feed_id)\">(+)</a>";
1404
1405 $rv['content'] .= "<div dojoType=\"dijit.Tooltip\"
1406 id=\"ATSTRTIP-$id\" connectId=\"ATSTR-$id\"
1407 position=\"below\">$tags_str_full</div>";
1408
1409 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_BUTTON) as $p) {
1410 $rv['content'] .= $p->hook_article_button($line);
1411 }
1412
1413 } else {
1414 $tags_str = strip_tags($tags_str);
1415 $rv['content'] .= "<span id=\"ATSTR-$id\">$tags_str</span>";
1416 }
1417 $rv['content'] .= "</div>";
1418 $rv['content'] .= "<div clear='both'>";
1419
1420 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_LEFT_BUTTON) as $p) {
1421 $rv['content'] .= $p->hook_article_left_button($line);
1422 }
1423
1424 $rv['content'] .= "$entry_comments</div>";
1425
1426 if ($line["orig_feed_id"]) {
1427
1428 $tmp_result = db_query("SELECT * FROM ttrss_archived_feeds
1429 WHERE id = ".$line["orig_feed_id"] . " AND owner_uid = " . $_SESSION["uid"]);
1430
1431 if (db_num_rows($tmp_result) != 0) {
1432
1433 $rv['content'] .= "<div clear='both'>";
1434 $rv['content'] .= __("Originally from:");
1435
1436 $rv['content'] .= "&nbsp;";
1437
1438 $tmp_line = db_fetch_assoc($tmp_result);
1439
1440 $rv['content'] .= "<a target='_blank'
1441 href=' " . htmlspecialchars($tmp_line['site_url']) . "'>" .
1442 $tmp_line['title'] . "</a>";
1443
1444 $rv['content'] .= "&nbsp;";
1445
1446 $rv['content'] .= "<a target='_blank' href='" . htmlspecialchars($tmp_line['feed_url']) . "'>";
1447 $rv['content'] .= "<img title='".__('Feed URL')."' class='tinyFeedIcon' src='images/pub_set.png'></a>";
1448
1449 $rv['content'] .= "</div>";
1450 }
1451 }
1452
1453 $rv['content'] .= "</div>";
1454
1455 $rv['content'] .= "<div id=\"POSTNOTE-$id\">";
1456 if ($line['note']) {
1457 $rv['content'] .= format_article_note($id, $line['note'], !$zoom_mode);
1458 }
1459 $rv['content'] .= "</div>";
1460
1461 if (!$line['lang']) $line['lang'] = 'en';
1462
1463 $rv['content'] .= "<div class=\"postContent\" lang=\"".$line['lang']."\">";
1464
1465 $rv['content'] .= $line["content"];
1466
1467 if (!$zoom_mode) {
1468 $rv['content'] .= format_article_enclosures($id,
1469 sql_bool_to_bool($line["always_display_enclosures"]),
1470 $line["content"],
1471 sql_bool_to_bool($line["hide_images"]));
1472 }
1473
1474 $rv['content'] .= "</div>";
1475
1476 $rv['content'] .= "</div>";
1477
1478 }
1479
1480 if ($zoom_mode) {
1481 $rv['content'] .= "
1482 <div class='footer'>
1483 <button onclick=\"return window.close()\">".
1484 __("Close this window")."</button></div>";
1485 $rv['content'] .= "</body></html>";
1486 }
1487
1488 return $rv;
1489
1490 }
1491
1492 function print_checkpoint($n, $s) {
1493 $ts = microtime(true);
1494 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1495 return $ts;
1496 }
1497
1498 function sanitize_tag($tag) {
1499 $tag = trim($tag);
1500
1501 $tag = mb_strtolower($tag, 'utf-8');
1502
1503 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1504
1505 if (DB_TYPE == "mysql") {
1506 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1507 }
1508
1509 return $tag;
1510 }
1511
1512 function get_self_url_prefix() {
1513 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1514 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1515 } else {
1516 return SELF_URL_PATH;
1517 }
1518 }
1519
1520 /**
1521 * Compute the Mozilla Firefox feed adding URL from server HOST and REQUEST_URI.
1522 *
1523 * @return string The Mozilla Firefox feed adding URL.
1524 */
1525 function add_feed_url() {
1526 //$url_path = ($_SERVER['HTTPS'] != "on" ? 'http://' : 'https://') . $_SERVER["HTTP_HOST"] . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
1527
1528 $url_path = get_self_url_prefix() .
1529 "/public.php?op=subscribe&feed_url=%s";
1530 return $url_path;
1531 } // function add_feed_url
1532
1533 function encrypt_password($pass, $salt = '', $mode2 = false) {
1534 if ($salt && $mode2) {
1535 return "MODE2:" . hash('sha256', $salt . $pass);
1536 } else if ($salt) {
1537 return "SHA1X:" . sha1("$salt:$pass");
1538 } else {
1539 return "SHA1:" . sha1($pass);
1540 }
1541 } // function encrypt_password
1542
1543 function load_filters($feed_id, $owner_uid, $action_id = false) {
1544 $filters = array();
1545
1546 $cat_id = (int)getFeedCategory($feed_id);
1547
1548 if ($cat_id == 0)
1549 $null_cat_qpart = "cat_id IS NULL OR";
1550 else
1551 $null_cat_qpart = "";
1552
1553 $result = db_query("SELECT * FROM ttrss_filters2 WHERE
1554 owner_uid = $owner_uid AND enabled = true ORDER BY order_id, title");
1555
1556 $check_cats = join(",", array_merge(
1557 getParentCategories($cat_id, $owner_uid),
1558 array($cat_id)));
1559
1560 while ($line = db_fetch_assoc($result)) {
1561 $filter_id = $line["id"];
1562
1563 $result2 = db_query("SELECT
1564 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, t.name AS type_name
1565 FROM ttrss_filters2_rules AS r,
1566 ttrss_filter_types AS t
1567 WHERE
1568 ($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats)) AND
1569 (feed_id IS NULL OR feed_id = '$feed_id') AND
1570 filter_type = t.id AND filter_id = '$filter_id'");
1571
1572 $rules = array();
1573 $actions = array();
1574
1575 while ($rule_line = db_fetch_assoc($result2)) {
1576 # print_r($rule_line);
1577
1578 $rule = array();
1579 $rule["reg_exp"] = $rule_line["reg_exp"];
1580 $rule["type"] = $rule_line["type_name"];
1581 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1582
1583 array_push($rules, $rule);
1584 }
1585
1586 $result2 = db_query("SELECT a.action_param,t.name AS type_name
1587 FROM ttrss_filters2_actions AS a,
1588 ttrss_filter_actions AS t
1589 WHERE
1590 action_id = t.id AND filter_id = '$filter_id'");
1591
1592 while ($action_line = db_fetch_assoc($result2)) {
1593 # print_r($action_line);
1594
1595 $action = array();
1596 $action["type"] = $action_line["type_name"];
1597 $action["param"] = $action_line["action_param"];
1598
1599 array_push($actions, $action);
1600 }
1601
1602
1603 $filter = array();
1604 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1605 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1606 $filter["rules"] = $rules;
1607 $filter["actions"] = $actions;
1608
1609 if (count($rules) > 0 && count($actions) > 0) {
1610 array_push($filters, $filter);
1611 }
1612 }
1613
1614 return $filters;
1615 }
1616
1617 function get_score_pic($score) {
1618 if ($score > 100) {
1619 return "score_high.png";
1620 } else if ($score > 0) {
1621 return "score_half_high.png";
1622 } else if ($score < -100) {
1623 return "score_low.png";
1624 } else if ($score < 0) {
1625 return "score_half_low.png";
1626 } else {
1627 return "score_neutral.png";
1628 }
1629 }
1630
1631 function feed_has_icon($id) {
1632 return is_file(ICONS_DIR . "/$id.ico") && filesize(ICONS_DIR . "/$id.ico") > 0;
1633 }
1634
1635 function init_plugins() {
1636 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
1637
1638 return true;
1639 }
1640
1641 function format_tags_string($tags, $id) {
1642 if (!is_array($tags) || count($tags) == 0) {
1643 return __("no tags");
1644 } else {
1645 $maxtags = min(5, count($tags));
1646 $tags_str = "";
1647
1648 for ($i = 0; $i < $maxtags; $i++) {
1649 $tags_str .= "<a class=\"tag\" href=\"#\" onclick=\"viewfeed({feed:'".$tags[$i]."'})\">" . $tags[$i] . "</a>, ";
1650 }
1651
1652 $tags_str = mb_substr($tags_str, 0, mb_strlen($tags_str)-2);
1653
1654 if (count($tags) > $maxtags)
1655 $tags_str .= ", &hellip;";
1656
1657 return $tags_str;
1658 }
1659 }
1660
1661 function format_article_labels($labels, $id) {
1662
1663 if (!is_array($labels)) return '';
1664
1665 $labels_str = "";
1666
1667 foreach ($labels as $l) {
1668 $labels_str .= sprintf("<span class='hlLabelRef'
1669 style='color : %s; background-color : %s'>%s</span>",
1670 $l[2], $l[3], $l[1]);
1671 }
1672
1673 return $labels_str;
1674
1675 }
1676
1677 function format_article_note($id, $note, $allow_edit = true) {
1678
1679 $str = "<div class='articleNote' onclick=\"editArticleNote($id)\">
1680 <div class='noteEdit' onclick=\"editArticleNote($id)\">".
1681 ($allow_edit ? __('(edit note)') : "")."</div>$note</div>";
1682
1683 return $str;
1684 }
1685
1686
1687 function get_feed_category($feed_cat, $parent_cat_id = false) {
1688 if ($parent_cat_id) {
1689 $parent_qpart = "parent_cat = '$parent_cat_id'";
1690 $parent_insert = "'$parent_cat_id'";
1691 } else {
1692 $parent_qpart = "parent_cat IS NULL";
1693 $parent_insert = "NULL";
1694 }
1695
1696 $result = db_query(
1697 "SELECT id FROM ttrss_feed_categories
1698 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]);
1699
1700 if (db_num_rows($result) == 0) {
1701 return false;
1702 } else {
1703 return db_fetch_result($result, 0, "id");
1704 }
1705 }
1706
1707 function add_feed_category($feed_cat, $parent_cat_id = false) {
1708
1709 if (!$feed_cat) return false;
1710
1711 db_query("BEGIN");
1712
1713 if ($parent_cat_id) {
1714 $parent_qpart = "parent_cat = '$parent_cat_id'";
1715 $parent_insert = "'$parent_cat_id'";
1716 } else {
1717 $parent_qpart = "parent_cat IS NULL";
1718 $parent_insert = "NULL";
1719 }
1720
1721 $feed_cat = mb_substr($feed_cat, 0, 250);
1722
1723 $result = db_query(
1724 "SELECT id FROM ttrss_feed_categories
1725 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]);
1726
1727 if (db_num_rows($result) == 0) {
1728
1729 $result = db_query(
1730 "INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1731 VALUES ('".$_SESSION["uid"]."', '$feed_cat', $parent_insert)");
1732
1733 db_query("COMMIT");
1734
1735 return true;
1736 }
1737
1738 return false;
1739 }
1740
1741 function getArticleFeed($id) {
1742 $result = db_query("SELECT feed_id FROM ttrss_user_entries
1743 WHERE ref_id = '$id' AND owner_uid = " . $_SESSION["uid"]);
1744
1745 if (db_num_rows($result) != 0) {
1746 return db_fetch_result($result, 0, "feed_id");
1747 } else {
1748 return 0;
1749 }
1750 }
1751
1752 /**
1753 * Fixes incomplete URLs by prepending "http://".
1754 * Also replaces feed:// with http://, and
1755 * prepends a trailing slash if the url is a domain name only.
1756 *
1757 * @param string $url Possibly incomplete URL
1758 *
1759 * @return string Fixed URL.
1760 */
1761 function fix_url($url) {
1762
1763 // support schema-less urls
1764 if (strpos($url, '//') === 0) {
1765 $url = 'https:' . $url;
1766 }
1767
1768 if (strpos($url, '://') === false) {
1769 $url = 'http://' . $url;
1770 } else if (substr($url, 0, 5) == 'feed:') {
1771 $url = 'http:' . substr($url, 5);
1772 }
1773
1774 //prepend slash if the URL has no slash in it
1775 // "http://www.example" -> "http://www.example/"
1776 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
1777 $url .= '/';
1778 }
1779
1780 //convert IDNA hostname to punycode if possible
1781 if (function_exists("idn_to_ascii")) {
1782 $parts = parse_url($url);
1783 if (mb_detect_encoding($parts['host']) != 'ASCII')
1784 {
1785 $parts['host'] = idn_to_ascii($parts['host']);
1786 $url = build_url($parts);
1787 }
1788 }
1789
1790 if ($url != "http:///")
1791 return $url;
1792 else
1793 return '';
1794 }
1795
1796 function validate_feed_url($url) {
1797 $parts = parse_url($url);
1798
1799 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
1800
1801 }
1802
1803 function get_article_enclosures($id) {
1804
1805 $query = "SELECT * FROM ttrss_enclosures
1806 WHERE post_id = '$id' AND content_url != ''";
1807
1808 $rv = array();
1809
1810 $result = db_query($query);
1811
1812 if (db_num_rows($result) > 0) {
1813 while ($line = db_fetch_assoc($result)) {
1814 array_push($rv, $line);
1815 }
1816 }
1817
1818 return $rv;
1819 }
1820
1821 /* function save_email_address($email) {
1822 // FIXME: implement persistent storage of emails
1823
1824 if (!$_SESSION['stored_emails'])
1825 $_SESSION['stored_emails'] = array();
1826
1827 if (!in_array($email, $_SESSION['stored_emails']))
1828 array_push($_SESSION['stored_emails'], $email);
1829 } */
1830
1831
1832 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
1833
1834 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
1835
1836 $sql_is_cat = bool_to_sql_bool($is_cat);
1837
1838 $result = db_query("SELECT access_key FROM ttrss_access_keys
1839 WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat
1840 AND owner_uid = " . $owner_uid);
1841
1842 if (db_num_rows($result) == 1) {
1843 return db_fetch_result($result, 0, "access_key");
1844 } else {
1845 $key = db_escape_string(uniqid_short());
1846
1847 $result = db_query("INSERT INTO ttrss_access_keys
1848 (access_key, feed_id, is_cat, owner_uid)
1849 VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')");
1850
1851 return $key;
1852 }
1853 return false;
1854 }
1855
1856 function get_feeds_from_html($url, $content)
1857 {
1858 $url = fix_url($url);
1859 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
1860
1861 libxml_use_internal_errors(true);
1862
1863 $doc = new DOMDocument();
1864 $doc->loadHTML($content);
1865 $xpath = new DOMXPath($doc);
1866 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
1867 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
1868 $feedUrls = array();
1869 foreach ($entries as $entry) {
1870 if ($entry->hasAttribute('href')) {
1871 $title = $entry->getAttribute('title');
1872 if ($title == '') {
1873 $title = $entry->getAttribute('type');
1874 }
1875 $feedUrl = rewrite_relative_url(
1876 $baseUrl, $entry->getAttribute('href')
1877 );
1878 $feedUrls[$feedUrl] = $title;
1879 }
1880 }
1881 return $feedUrls;
1882 }
1883
1884 function is_html($content) {
1885 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
1886 }
1887
1888 function url_is_html($url, $login = false, $pass = false) {
1889 return is_html(fetch_file_contents($url, false, $login, $pass));
1890 }
1891
1892 function print_label_select($name, $value, $attributes = "") {
1893
1894 $result = db_query("SELECT caption FROM ttrss_labels2
1895 WHERE owner_uid = '".$_SESSION["uid"]."' ORDER BY caption");
1896
1897 print "<select default=\"$value\" name=\"" . htmlspecialchars($name) .
1898 "\" $attributes onchange=\"labelSelectOnChange(this)\" >";
1899
1900 while ($line = db_fetch_assoc($result)) {
1901
1902 $issel = ($line["caption"] == $value) ? "selected=\"1\"" : "";
1903
1904 print "<option value=\"".htmlspecialchars($line["caption"])."\"
1905 $issel>" . htmlspecialchars($line["caption"]) . "</option>";
1906
1907 }
1908
1909 # print "<option value=\"ADD_LABEL\">" .__("Add label...") . "</option>";
1910
1911 print "</select>";
1912
1913
1914 }
1915
1916 function format_article_enclosures($id, $always_display_enclosures,
1917 $article_content, $hide_images = false) {
1918
1919 $result = get_article_enclosures($id);
1920 $rv = '';
1921
1922 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FORMAT_ENCLOSURES) as $plugin) {
1923 $retval = $plugin->hook_format_enclosures($rv, $result, $id, $always_display_enclosures, $article_content, $hide_images);
1924 if (is_array($retval)) {
1925 $rv = $retval[0];
1926 $result = $retval[1];
1927 } else {
1928 $rv = $retval;
1929 }
1930 }
1931 unset($retval); // Unset to prevent breaking render if there are no HOOK_RENDER_ENCLOSURE hooks below.
1932
1933 if ($rv === '' && !empty($result)) {
1934 $entries_html = array();
1935 $entries = array();
1936 $entries_inline = array();
1937
1938 foreach ($result as $line) {
1939
1940 $url = $line["content_url"];
1941 $ctype = $line["content_type"];
1942 $title = $line["title"];
1943 $width = $line["width"];
1944 $height = $line["height"];
1945
1946 if (!$ctype) $ctype = __("unknown type");
1947
1948 //$filename = substr($url, strrpos($url, "/")+1);
1949 $filename = basename($url);
1950
1951 $player = format_inline_player($url, $ctype);
1952
1953 if ($player) array_push($entries_inline, $player);
1954
1955 # $entry .= " <a target=\"_blank\" href=\"" . htmlspecialchars($url) . "\">" .
1956 # $filename . " (" . $ctype . ")" . "</a>";
1957
1958 $entry = "<div onclick=\"window.open('".htmlspecialchars($url)."')\"
1959 dojoType=\"dijit.MenuItem\">$filename ($ctype)</div>";
1960
1961 array_push($entries_html, $entry);
1962
1963 $entry = array();
1964
1965 $entry["type"] = $ctype;
1966 $entry["filename"] = $filename;
1967 $entry["url"] = $url;
1968 $entry["title"] = $title;
1969 $entry["width"] = $width;
1970 $entry["height"] = $height;
1971
1972 array_push($entries, $entry);
1973 }
1974
1975 if ($_SESSION['uid'] && !get_pref("STRIP_IMAGES") && !$_SESSION["bw_limit"]) {
1976 if ($always_display_enclosures ||
1977 !preg_match("/<img/i", $article_content)) {
1978
1979 foreach ($entries as $entry) {
1980
1981 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ENCLOSURE) as $plugin)
1982 $retval = $plugin->hook_render_enclosure($entry, $hide_images);
1983
1984
1985 if ($retval) {
1986 $rv .= $retval;
1987 } else {
1988
1989 if (preg_match("/image/", $entry["type"]) ||
1990 preg_match("/\.(jpg|png|gif|bmp)/i", $entry["filename"])) {
1991
1992 if (!$hide_images) {
1993 $encsize = '';
1994 if ($entry['height'] > 0)
1995 $encsize .= ' height="' . intval($entry['height']) . '"';
1996 if ($entry['width'] > 0)
1997 $encsize .= ' width="' . intval($entry['width']) . '"';
1998 $rv .= "<p><img
1999 alt=\"".htmlspecialchars($entry["filename"])."\"
2000 src=\"" .htmlspecialchars($entry["url"]) . "\"
2001 " . $encsize . " /></p>";
2002 } else {
2003 $rv .= "<p><a target=\"_blank\"
2004 href=\"".htmlspecialchars($entry["url"])."\"
2005 >" .htmlspecialchars($entry["url"]) . "</a></p>";
2006 }
2007
2008 if ($entry['title']) {
2009 $rv.= "<div class=\"enclosure_title\">${entry['title']}</div>";
2010 }
2011 }
2012 }
2013 }
2014 }
2015 }
2016
2017 if (count($entries_inline) > 0) {
2018 $rv .= "<hr clear='both'/>";
2019 foreach ($entries_inline as $entry) { $rv .= $entry; };
2020 $rv .= "<hr clear='both'/>";
2021 }
2022
2023 $rv .= "<div class=\"attachments\" dojoType=\"dijit.form.DropDownButton\">".
2024 "<span>" . __('Attachments')."</span>";
2025
2026 $rv .= "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
2027
2028 foreach ($entries as $entry) {
2029 if ($entry["title"])
2030 $title = " &mdash; " . truncate_string($entry["title"], 30);
2031 else
2032 $title = "";
2033
2034 if ($entry["filename"])
2035 $filename = truncate_middle(htmlspecialchars($entry["filename"]), 60);
2036 else
2037 $filename = "";
2038
2039 $rv .= "<div onclick='window.open(\"".htmlspecialchars($entry["url"])."\")'
2040 dojoType=\"dijit.MenuItem\">".$filename . $title."</div>";
2041
2042 };
2043
2044 $rv .= "</div>";
2045 $rv .= "</div>";
2046 }
2047
2048 return $rv;
2049 }
2050
2051 function getLastArticleId() {
2052 $result = db_query("SELECT ref_id AS id FROM ttrss_user_entries
2053 WHERE owner_uid = " . $_SESSION["uid"] . " ORDER BY ref_id DESC LIMIT 1");
2054
2055 if (db_num_rows($result) == 1) {
2056 return db_fetch_result($result, 0, "id");
2057 } else {
2058 return -1;
2059 }
2060 }
2061
2062 function build_url($parts) {
2063 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2064 }
2065
2066 /**
2067 * Converts a (possibly) relative URL to a absolute one.
2068 *
2069 * @param string $url Base URL (i.e. from where the document is)
2070 * @param string $rel_url Possibly relative URL in the document
2071 *
2072 * @return string Absolute URL
2073 */
2074 function rewrite_relative_url($url, $rel_url) {
2075 if (strpos($rel_url, "://") !== false) {
2076 return $rel_url;
2077 } else if (strpos($rel_url, "//") === 0) {
2078 # protocol-relative URL (rare but they exist)
2079 return $rel_url;
2080 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2081 # magnet:, feed:, etc
2082 return $rel_url;
2083 } else if (strpos($rel_url, "/") === 0) {
2084 $parts = parse_url($url);
2085 $parts['path'] = $rel_url;
2086
2087 return build_url($parts);
2088
2089 } else {
2090 $parts = parse_url($url);
2091 if (!isset($parts['path'])) {
2092 $parts['path'] = '/';
2093 }
2094 $dir = $parts['path'];
2095 if (substr($dir, -1) !== '/') {
2096 $dir = dirname($parts['path']);
2097 $dir !== '/' && $dir .= '/';
2098 }
2099 $parts['path'] = $dir . $rel_url;
2100
2101 return build_url($parts);
2102 }
2103 }
2104
2105 function cleanup_tags($days = 14, $limit = 1000) {
2106
2107 if (DB_TYPE == "pgsql") {
2108 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2109 } else if (DB_TYPE == "mysql") {
2110 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2111 }
2112
2113 $tags_deleted = 0;
2114
2115 while ($limit > 0) {
2116 $limit_part = 500;
2117
2118 $query = "SELECT ttrss_tags.id AS id
2119 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2120 WHERE post_int_id = int_id AND $interval_query AND
2121 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT $limit_part";
2122
2123 $result = db_query($query);
2124
2125 $ids = array();
2126
2127 while ($line = db_fetch_assoc($result)) {
2128 array_push($ids, $line['id']);
2129 }
2130
2131 if (count($ids) > 0) {
2132 $ids = join(",", $ids);
2133
2134 $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2135 $tags_deleted += db_affected_rows($tmp_result);
2136 } else {
2137 break;
2138 }
2139
2140 $limit -= $limit_part;
2141 }
2142
2143 return $tags_deleted;
2144 }
2145
2146 function print_user_stylesheet() {
2147 $value = get_pref('USER_STYLESHEET');
2148
2149 if ($value) {
2150 print "<style type=\"text/css\">";
2151 print str_replace("<br/>", "\n", $value);
2152 print "</style>";
2153 }
2154
2155 }
2156
2157 function filter_to_sql($filter, $owner_uid) {
2158 $query = array();
2159
2160 if (DB_TYPE == "pgsql")
2161 $reg_qpart = "~";
2162 else
2163 $reg_qpart = "REGEXP";
2164
2165 foreach ($filter["rules"] AS $rule) {
2166 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2167 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2168 $rule['reg_exp']) !== FALSE;
2169
2170 if ($regexp_valid) {
2171
2172 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
2173
2174 switch ($rule["type"]) {
2175 case "title":
2176 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2177 $rule['reg_exp'] . "')";
2178 break;
2179 case "content":
2180 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2181 $rule['reg_exp'] . "')";
2182 break;
2183 case "both":
2184 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2185 $rule['reg_exp'] . "') OR LOWER(" .
2186 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2187 break;
2188 case "tag":
2189 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2190 $rule['reg_exp'] . "')";
2191 break;
2192 case "link":
2193 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2194 $rule['reg_exp'] . "')";
2195 break;
2196 case "author":
2197 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2198 $rule['reg_exp'] . "')";
2199 break;
2200 }
2201
2202 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2203
2204 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2205 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
2206 }
2207
2208 if (isset($rule["cat_id"])) {
2209
2210 if ($rule["cat_id"] > 0) {
2211 $children = getChildCategories($rule["cat_id"], $owner_uid);
2212 array_push($children, $rule["cat_id"]);
2213
2214 $children = join(",", $children);
2215
2216 $cat_qpart = "cat_id IN ($children)";
2217 } else {
2218 $cat_qpart = "cat_id IS NULL";
2219 }
2220
2221 $qpart .= " AND $cat_qpart";
2222 }
2223
2224 $qpart .= " AND feed_id IS NOT NULL";
2225
2226 array_push($query, "($qpart)");
2227
2228 }
2229 }
2230
2231 if (count($query) > 0) {
2232 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2233 } else {
2234 $fullquery = "(false)";
2235 }
2236
2237 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2238
2239 return $fullquery;
2240 }
2241
2242 if (!function_exists('gzdecode')) {
2243 function gzdecode($string) { // no support for 2nd argument
2244 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2245 base64_encode($string));
2246 }
2247 }
2248
2249 function get_random_bytes($length) {
2250 if (function_exists('openssl_random_pseudo_bytes')) {
2251 return openssl_random_pseudo_bytes($length);
2252 } else {
2253 $output = "";
2254
2255 for ($i = 0; $i < $length; $i++)
2256 $output .= chr(mt_rand(0, 255));
2257
2258 return $output;
2259 }
2260 }
2261
2262 function read_stdin() {
2263 $fp = fopen("php://stdin", "r");
2264
2265 if ($fp) {
2266 $line = trim(fgets($fp));
2267 fclose($fp);
2268 return $line;
2269 }
2270
2271 return null;
2272 }
2273
2274 function tmpdirname($path, $prefix) {
2275 // Use PHP's tmpfile function to create a temporary
2276 // directory name. Delete the file and keep the name.
2277 $tempname = tempnam($path,$prefix);
2278 if (!$tempname)
2279 return false;
2280
2281 if (!unlink($tempname))
2282 return false;
2283
2284 return $tempname;
2285 }
2286
2287 function getFeedCategory($feed) {
2288 $result = db_query("SELECT cat_id FROM ttrss_feeds
2289 WHERE id = '$feed'");
2290
2291 if (db_num_rows($result) > 0) {
2292 return db_fetch_result($result, 0, "cat_id");
2293 } else {
2294 return false;
2295 }
2296
2297 }
2298
2299 function implements_interface($class, $interface) {
2300 return in_array($interface, class_implements($class));
2301 }
2302
2303 function get_minified_js($files) {
2304 require_once 'lib/jshrink/Minifier.php';
2305
2306 $rv = '';
2307
2308 foreach ($files as $js) {
2309 if (!isset($_GET['debug'])) {
2310 $cached_file = CACHE_DIR . "/js/".basename($js).".js";
2311
2312 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2313
2314 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2315
2316 if ($header && $contents) {
2317 list($htag, $hversion) = explode(":", $header);
2318
2319 if ($htag == "tt-rss" && $hversion == VERSION) {
2320 $rv .= $contents;
2321 continue;
2322 }
2323 }
2324 }
2325
2326 $minified = JShrink\Minifier::minify(file_get_contents("js/$js.js"));
2327 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
2328 $rv .= $minified;
2329
2330 } else {
2331 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2332 }
2333 }
2334
2335 return $rv;
2336 }
2337
2338 function stylesheet_tag($filename) {
2339 $timestamp = filemtime($filename);
2340
2341 return "<link rel=\"stylesheet\" type=\"text/css\" href=\"$filename?$timestamp\"/>\n";
2342 }
2343
2344 function javascript_tag($filename) {
2345 $query = "";
2346
2347 if (!(strpos($filename, "?") === FALSE)) {
2348 $query = substr($filename, strpos($filename, "?")+1);
2349 $filename = substr($filename, 0, strpos($filename, "?"));
2350 }
2351
2352 $timestamp = filemtime($filename);
2353
2354 if ($query) $timestamp .= "&$query";
2355
2356 return "<script type=\"text/javascript\" charset=\"utf-8\" src=\"$filename?$timestamp\"></script>\n";
2357 }
2358
2359 function calculate_dep_timestamp() {
2360 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2361
2362 $max_ts = -1;
2363
2364 foreach ($files as $file) {
2365 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2366 }
2367
2368 return $max_ts;
2369 }
2370
2371 function T_js_decl($s1, $s2) {
2372 if ($s1 && $s2) {
2373 $s1 = preg_replace("/\n/", "", $s1);
2374 $s2 = preg_replace("/\n/", "", $s2);
2375
2376 $s1 = preg_replace("/\"/", "\\\"", $s1);
2377 $s2 = preg_replace("/\"/", "\\\"", $s2);
2378
2379 return "T_messages[\"$s1\"] = \"$s2\";\n";
2380 }
2381 }
2382
2383 function init_js_translations() {
2384
2385 print 'var T_messages = new Object();
2386
2387 function __(msg) {
2388 if (T_messages[msg]) {
2389 return T_messages[msg];
2390 } else {
2391 return msg;
2392 }
2393 }
2394
2395 function ngettext(msg1, msg2, n) {
2396 return __((parseInt(n) > 1) ? msg2 : msg1);
2397 }';
2398
2399 $l10n = _get_reader();
2400
2401 for ($i = 0; $i < $l10n->total; $i++) {
2402 $orig = $l10n->get_original_string($i);
2403 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2404 $key = explode(chr(0), $orig);
2405 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2406 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2407 } else {
2408 $translation = __($orig);
2409 print T_js_decl($orig, $translation);
2410 }
2411 }
2412 }
2413
2414 function label_to_feed_id($label) {
2415 return LABEL_BASE_INDEX - 1 - abs($label);
2416 }
2417
2418 function feed_to_label_id($feed) {
2419 return LABEL_BASE_INDEX - 1 + abs($feed);
2420 }
2421
2422 function get_theme_path($theme) {
2423 $check = "themes/$theme";
2424 if (file_exists($check)) return $check;
2425
2426 $check = "themes.local/$theme";
2427 if (file_exists($check)) return $check;
2428 }
2429
2430 function theme_valid($theme) {
2431 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2432
2433 if (in_array($theme, $bundled_themes)) return true;
2434
2435 $file = "themes/" . basename($theme);
2436
2437 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2438
2439 if (file_exists($file) && is_readable($file)) {
2440 $fh = fopen($file, "r");
2441
2442 if ($fh) {
2443 $header = fgets($fh);
2444 fclose($fh);
2445
2446 return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
2447 }
2448 }
2449
2450 return false;
2451 }
2452
2453 function error_json($code) {
2454 require_once "errors.php";
2455
2456 @$message = $ERRORS[$code];
2457
2458 return json_encode(array("error" =>
2459 array("code" => $code, "message" => $message)));
2460
2461 }
2462
2463 function abs_to_rel_path($dir) {
2464 $tmp = str_replace(dirname(__DIR__), "", $dir);
2465
2466 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2467
2468 return $tmp;
2469 }
2470
2471 function get_upload_error_message($code) {
2472
2473 $errors = array(
2474 0 => __('There is no error, the file uploaded with success'),
2475 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2476 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2477 3 => __('The uploaded file was only partially uploaded'),
2478 4 => __('No file was uploaded'),
2479 6 => __('Missing a temporary folder'),
2480 7 => __('Failed to write file to disk.'),
2481 8 => __('A PHP extension stopped the file upload.'),
2482 );
2483
2484 return $errors[$code];
2485 }
2486 ?>