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