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