2 function make_init_params() {
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) {
10 $params[strtolower($param)] = (int) get_pref($param);
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;
21 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
22 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
24 $max_feed_id = db_fetch_result($result, 0, "mid");
25 $num_feeds = db_fetch_result($result, 0, "nf");
27 $params["max_feed_id"] = (int) $max_feed_id;
28 $params["num_feeds"] = (int) $num_feeds;
30 $params["hotkeys"] = get_hotkeys_map();
32 $params["csrf_token"] = $_SESSION["csrf_token"];
33 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
35 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE;
40 function get_hotkeys_info() {
42 __("Navigation") => array(
43 "next_feed" => __("Open next feed"),
44 "prev_feed" => __("Open previous feed"),
45 "next_article" => __("Open next article"),
46 "prev_article" => __("Open previous article"),
47 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
48 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
49 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
50 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
51 "search_dialog" => __("Show search dialog")),
52 __("Article") => array(
53 "toggle_mark" => __("Toggle starred"),
54 "toggle_publ" => __("Toggle published"),
55 "toggle_unread" => __("Toggle unread"),
56 "edit_tags" => __("Edit tags"),
57 "dismiss_selected" => __("Dismiss selected"),
58 "dismiss_read" => __("Dismiss read"),
59 "open_in_new_window" => __("Open in new window"),
60 "catchup_below" => __("Mark below as read"),
61 "catchup_above" => __("Mark above as read"),
62 "article_scroll_down" => __("Scroll down"),
63 "article_scroll_up" => __("Scroll up"),
64 "select_article_cursor" => __("Select article under cursor"),
65 "email_article" => __("Email article"),
66 "close_article" => __("Close/collapse article"),
67 "toggle_expand" => __("Toggle article expansion (combined mode)"),
68 "toggle_widescreen" => __("Toggle widescreen mode"),
69 "toggle_embed_original" => __("Toggle embed original")),
70 __("Article selection") => array(
71 "select_all" => __("Select all articles"),
72 "select_unread" => __("Select unread"),
73 "select_marked" => __("Select starred"),
74 "select_published" => __("Select published"),
75 "select_invert" => __("Invert selection"),
76 "select_none" => __("Deselect everything")),
78 "feed_refresh" => __("Refresh current feed"),
79 "feed_unhide_read" => __("Un/hide read feeds"),
80 "feed_subscribe" => __("Subscribe to feed"),
81 "feed_edit" => __("Edit feed"),
82 "feed_catchup" => __("Mark as read"),
83 "feed_reverse" => __("Reverse headlines"),
84 "feed_debug_update" => __("Debug feed update"),
85 "catchup_all" => __("Mark all feeds as read"),
86 "cat_toggle_collapse" => __("Un/collapse current category"),
87 "toggle_combined_mode" => __("Toggle combined mode"),
88 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
90 "goto_all" => __("All articles"),
91 "goto_fresh" => __("Fresh"),
92 "goto_marked" => __("Starred"),
93 "goto_published" => __("Published"),
94 "goto_tagcloud" => __("Tag cloud"),
95 "goto_prefs" => __("Preferences")),
97 "create_label" => __("Create label"),
98 "create_filter" => __("Create filter"),
99 "collapse_sidebar" => __("Un/collapse sidebar"),
100 "help_dialog" => __("Show help dialog"))
103 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
104 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
110 function get_hotkeys_map() {
112 // "navigation" => array(
115 "n" => "next_article",
116 "p" => "prev_article",
117 "(38)|up" => "prev_article",
118 "(40)|down" => "next_article",
119 // "^(38)|Ctrl-up" => "prev_article_noscroll",
120 // "^(40)|Ctrl-down" => "next_article_noscroll",
121 "(191)|/" => "search_dialog",
122 // "article" => array(
123 "s" => "toggle_mark",
124 "*s" => "toggle_publ",
125 "u" => "toggle_unread",
127 "*d" => "dismiss_selected",
128 "*x" => "dismiss_read",
129 "o" => "open_in_new_window",
130 "c p" => "catchup_below",
131 "c n" => "catchup_above",
132 "*n" => "article_scroll_down",
133 "*p" => "article_scroll_up",
134 "*(38)|Shift+up" => "article_scroll_up",
135 "*(40)|Shift+down" => "article_scroll_down",
136 "a *w" => "toggle_widescreen",
137 "a e" => "toggle_embed_original",
138 "e" => "email_article",
139 "a q" => "close_article",
140 // "article_selection" => array(
141 "a a" => "select_all",
142 "a u" => "select_unread",
143 "a *u" => "select_marked",
144 "a p" => "select_published",
145 "a i" => "select_invert",
146 "a n" => "select_none",
148 "f r" => "feed_refresh",
149 "f a" => "feed_unhide_read",
150 "f s" => "feed_subscribe",
151 "f e" => "feed_edit",
152 "f q" => "feed_catchup",
153 "f x" => "feed_reverse",
154 "f *d" => "feed_debug_update",
155 "f *c" => "toggle_combined_mode",
156 "f c" => "toggle_cdm_expanded",
157 "*q" => "catchup_all",
158 "x" => "cat_toggle_collapse",
161 "g f" => "goto_fresh",
162 "g s" => "goto_marked",
163 "g p" => "goto_published",
164 "g t" => "goto_tagcloud",
165 "g *p" => "goto_prefs",
167 "(9)|Tab" => "select_article_cursor", // tab
168 "c l" => "create_label",
169 "c f" => "create_filter",
170 "c s" => "collapse_sidebar",
171 "^(191)|Ctrl+/" => "help_dialog",
174 if (get_pref('COMBINED_DISPLAY_MODE')) {
175 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
176 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
179 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
180 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
185 foreach (array_keys($hotkeys) as $hotkey) {
186 $pair = explode(" ", $hotkey, 2);
188 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
189 array_push($prefixes, $pair[0]);
193 return array($prefixes, $hotkeys);
196 function make_runtime_info() {
199 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
200 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
202 $max_feed_id = db_fetch_result($result, 0, "mid");
203 $num_feeds = db_fetch_result($result, 0, "nf");
205 $data["max_feed_id"] = (int) $max_feed_id;
206 $data["num_feeds"] = (int) $num_feeds;
208 $data['last_article_id'] = getLastArticleId();
209 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
211 $data['dep_ts'] = calculate_dep_timestamp();
212 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
214 if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
216 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
218 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
220 $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
223 $stamp_delta = time() - $stamp;
225 if ($stamp_delta > 1800) {
229 $_SESSION["daemon_stamp_check"] = time();
232 $data['daemon_stamp_ok'] = $stamp_check;
234 $stamp_fmt = date("Y.m.d, G:i", $stamp);
236 $data['daemon_stamp'] = $stamp_fmt;
241 if ($_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
242 $new_version_details = @check_for_update();
244 $data['new_version_available'] = (int) ($new_version_details != false);
246 $_SESSION["last_version_check"] = time();
247 $_SESSION["version_data"] = $new_version_details;
253 function search_to_sql($search) {
255 $search_query_part = "";
257 $keywords = str_getcsv($search, " ");
258 $query_keywords = array();
259 $search_words = array();
261 foreach ($keywords as $k) {
262 if (strpos($k, "-") === 0) {
269 $commandpair = explode(":", mb_strtolower($k), 2);
271 switch ($commandpair[0]) {
273 if ($commandpair[1]) {
274 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE '%".
275 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
277 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
278 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
279 array_push($search_words, $k);
283 if ($commandpair[1]) {
284 array_push($query_keywords, "($not (LOWER(author) LIKE '%".
285 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
287 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
288 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
289 array_push($search_words, $k);
293 if ($commandpair[1]) {
294 if ($commandpair[1] == "true")
295 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
296 else if ($commandpair[1] == "false")
297 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
299 array_push($query_keywords, "($not (LOWER(note) LIKE '%".
300 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
302 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
303 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
304 if (!$not) array_push($search_words, $k);
309 if ($commandpair[1]) {
310 if ($commandpair[1] == "true")
311 array_push($query_keywords, "($not (marked = true))");
313 array_push($query_keywords, "($not (marked = false))");
315 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
316 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
317 if (!$not) array_push($search_words, $k);
321 if ($commandpair[1]) {
322 if ($commandpair[1] == "true")
323 array_push($query_keywords, "($not (published = true))");
325 array_push($query_keywords, "($not (published = false))");
328 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
329 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
330 if (!$not) array_push($search_words, $k);
334 if (strpos($k, "@") === 0) {
336 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
337 $orig_ts = strtotime(substr($k, 1));
338 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
340 //$k = date("Y-m-d", strtotime(substr($k, 1)));
342 array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
344 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
345 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
347 if (!$not) array_push($search_words, $k);
352 $search_query_part = implode("AND", $query_keywords);
354 return array($search_query_part, $search_words);
357 function getParentCategories($cat, $owner_uid) {
360 $result = db_query("SELECT parent_cat FROM ttrss_feed_categories
361 WHERE id = '$cat' AND parent_cat IS NOT NULL AND owner_uid = $owner_uid");
363 while ($line = db_fetch_assoc($result)) {
364 array_push($rv, $line["parent_cat"]);
365 $rv = array_merge($rv, getParentCategories($line["parent_cat"], $owner_uid));
371 function getChildCategories($cat, $owner_uid) {
374 $result = db_query("SELECT id FROM ttrss_feed_categories
375 WHERE parent_cat = '$cat' AND owner_uid = $owner_uid");
377 while ($line = db_fetch_assoc($result)) {
378 array_push($rv, $line["id"]);
379 $rv = array_merge($rv, getChildCategories($line["id"], $owner_uid));
385 function queryFeedHeadlines($feed, $limit, $view_mode, $cat_view, $search, $search_mode, $override_order = false, $offset = 0, $owner_uid = 0, $filter = false, $since_id = 0, $include_children = false, $ignore_vfeed_group = false, $override_strategy = false, $override_vfeed = false, $start_ts = false) {
387 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
389 $ext_tables_part = "";
390 $search_words = array();
394 if (SPHINX_ENABLED) {
395 $ids = join(",", @sphinx_search($search, 0, 500));
398 $search_query_part = "ref_id IN ($ids) AND ";
400 $search_query_part = "ref_id = -1 AND ";
403 list($search_query_part, $search_words) = search_to_sql($search);
404 $search_query_part .= " AND ";
408 $search_query_part = "";
413 if (DB_TYPE == "pgsql") {
414 $query_strategy_part .= " AND updated > NOW() - INTERVAL '14 days' ";
416 $query_strategy_part .= " AND updated > DATE_SUB(NOW(), INTERVAL 14 DAY) ";
419 $override_order = "updated DESC";
421 $filter_query_part = filter_to_sql($filter, $owner_uid);
423 // Try to check if SQL regexp implementation chokes on a valid regexp
426 $result = db_query("SELECT true AS true_val FROM ttrss_entries,
427 ttrss_user_entries, ttrss_feeds
428 WHERE $filter_query_part LIMIT 1", false);
431 $test = db_fetch_result($result, 0, "true_val");
434 $filter_query_part = "false AND";
436 $filter_query_part .= " AND";
439 $filter_query_part = "false AND";
443 $filter_query_part = "";
447 $since_id_part = "ttrss_entries.id > $since_id AND ";
452 $view_query_part = "";
454 if ($view_mode == "adaptive") {
456 $view_query_part = " ";
457 } else if ($feed != -1) {
459 $unread = getFeedUnread($feed, $cat_view);
461 if ($cat_view && $feed > 0 && $include_children)
462 $unread += getCategoryChildrenUnread($feed);
465 $view_query_part = " unread = true AND ";
470 if ($view_mode == "marked") {
471 $view_query_part = " marked = true AND ";
474 if ($view_mode == "has_note") {
475 $view_query_part = " (note IS NOT NULL AND note != '') AND ";
478 if ($view_mode == "published") {
479 $view_query_part = " published = true AND ";
482 if ($view_mode == "unread" && $feed != -6) {
483 $view_query_part = " unread = true AND ";
487 $limit_query_part = "LIMIT " . $limit;
490 $allow_archived = false;
492 $vfeed_query_part = "";
494 // override query strategy and enable feed display when searching globally
495 if ($search && $search_mode == "all_feeds") {
496 $query_strategy_part = "true";
497 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
499 } else if (!is_numeric($feed)) {
500 $query_strategy_part = "true";
501 $vfeed_query_part = "(SELECT title FROM ttrss_feeds WHERE
502 id = feed_id) as feed_title,";
503 } else if ($search && $search_mode == "this_cat") {
504 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
507 if ($include_children) {
508 $subcats = getChildCategories($feed, $owner_uid);
509 array_push($subcats, $feed);
510 $cats_qpart = join(",", $subcats);
515 $query_strategy_part = "ttrss_feeds.cat_id IN ($cats_qpart)";
518 $query_strategy_part = "ttrss_feeds.cat_id IS NULL";
521 } else if ($feed > 0) {
526 if ($include_children) {
528 $subcats = getChildCategories($feed, $owner_uid);
530 array_push($subcats, $feed);
531 $query_strategy_part = "cat_id IN (".
532 implode(",", $subcats).")";
535 $query_strategy_part = "cat_id = '$feed'";
539 $query_strategy_part = "cat_id IS NULL";
542 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
545 $query_strategy_part = "feed_id = '$feed'";
547 } else if ($feed == 0 && !$cat_view) { // archive virtual feed
548 $query_strategy_part = "feed_id IS NULL";
549 $allow_archived = true;
550 } else if ($feed == 0 && $cat_view) { // uncategorized
551 $query_strategy_part = "cat_id IS NULL AND feed_id IS NOT NULL";
552 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
553 } else if ($feed == -1) { // starred virtual feed
554 $query_strategy_part = "marked = true";
555 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
556 $allow_archived = true;
558 if (!$override_order) {
559 $override_order = "last_marked DESC, date_entered DESC, updated DESC";
562 } else if ($feed == -2) { // published virtual feed OR labels category
565 $query_strategy_part = "published = true";
566 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
567 $allow_archived = true;
569 if (!$override_order) {
570 $override_order = "last_published DESC, date_entered DESC, updated DESC";
574 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
576 $ext_tables_part = ",ttrss_labels2,ttrss_user_labels2";
578 $query_strategy_part = "ttrss_labels2.id = ttrss_user_labels2.label_id AND
579 ttrss_user_labels2.article_id = ref_id";
582 } else if ($feed == -6) { // recently read
583 $query_strategy_part = "unread = false AND last_read IS NOT NULL";
584 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
585 $allow_archived = true;
587 if (!$override_order) $override_order = "last_read DESC";
589 /* } else if ($feed == -7) { // shared
590 $query_strategy_part = "uuid != ''";
591 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
592 $allow_archived = true; */
593 } else if ($feed == -3) { // fresh virtual feed
594 $query_strategy_part = "unread = true AND score >= 0";
596 $intl = get_pref("FRESH_ARTICLE_MAX_AGE", $owner_uid);
598 if (DB_TYPE == "pgsql") {
599 $query_strategy_part .= " AND date_entered > NOW() - INTERVAL '$intl hour' ";
601 $query_strategy_part .= " AND date_entered > DATE_SUB(NOW(), INTERVAL $intl HOUR) ";
604 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
605 } else if ($feed == -4) { // all articles virtual feed
606 $allow_archived = true;
607 $query_strategy_part = "true";
608 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
609 } else if ($feed <= LABEL_BASE_INDEX) { // labels
610 $label_id = feed_to_label_id($feed);
612 $query_strategy_part = "label_id = '$label_id' AND
613 ttrss_labels2.id = ttrss_user_labels2.label_id AND
614 ttrss_user_labels2.article_id = ref_id";
616 $vfeed_query_part = "ttrss_feeds.title AS feed_title,";
617 $ext_tables_part = ",ttrss_labels2,ttrss_user_labels2";
618 $allow_archived = true;
621 $query_strategy_part = "true";
624 $order_by = "score DESC, date_entered DESC, updated DESC";
626 if ($view_mode == "unread_first") {
627 $order_by = "unread DESC, $order_by";
630 if ($override_order) {
631 $order_by = $override_order;
634 if ($override_strategy) {
635 $query_strategy_part = $override_strategy;
638 if ($override_vfeed) {
639 $vfeed_query_part = $override_vfeed;
645 $feed_title = T_sprintf("Search results: %s", $search);
648 $feed_title = getCategoryTitle($feed);
650 if (is_numeric($feed) && $feed > 0) {
651 $result = db_query("SELECT title,site_url,last_error,last_updated
652 FROM ttrss_feeds WHERE id = '$feed' AND owner_uid = $owner_uid");
654 $feed_title = db_fetch_result($result, 0, "title");
655 $feed_site_url = db_fetch_result($result, 0, "site_url");
656 $last_error = db_fetch_result($result, 0, "last_error");
657 $last_updated = db_fetch_result($result, 0, "last_updated");
659 $feed_title = getFeedTitle($feed);
665 $content_query_part = "content, ";
668 if (is_numeric($feed)) {
671 $feed_kind = "Feeds";
673 $feed_kind = "Labels";
676 if ($limit_query_part) {
677 $offset_query_part = "OFFSET $offset";
680 // proper override_order applied above
681 if ($vfeed_query_part && !$ignore_vfeed_group && get_pref('VFEED_GROUP_BY_FEED', $owner_uid)) {
682 if (!$override_order) {
683 $order_by = "ttrss_feeds.title, $order_by";
685 $order_by = "ttrss_feeds.title, $override_order";
689 if (!$allow_archived) {
690 $from_qpart = "ttrss_entries,ttrss_user_entries,ttrss_feeds$ext_tables_part";
691 $feed_check_qpart = "ttrss_user_entries.feed_id = ttrss_feeds.id AND";
694 $from_qpart = "ttrss_entries$ext_tables_part,ttrss_user_entries
695 LEFT JOIN ttrss_feeds ON (feed_id = ttrss_feeds.id)";
698 if ($vfeed_query_part)
699 $vfeed_query_part .= "favicon_avg_color,";
702 $start_ts_formatted = date("Y/m/d H:i:s", strtotime($start_ts));
703 $start_ts_query_part = "date_entered >= '$start_ts_formatted' AND";
705 $start_ts_query_part = "";
708 $query = "SELECT DISTINCT
711 ttrss_entries.id,ttrss_entries.title,
715 always_display_enclosures,
724 unread,feed_id,marked,published,link,last_read,orig_feed_id,
725 last_marked, last_published,
733 ttrss_user_entries.ref_id = ttrss_entries.id AND
734 ttrss_user_entries.owner_uid = '$owner_uid' AND
740 $query_strategy_part ORDER BY $order_by
741 $limit_query_part $offset_query_part";
743 if ($_REQUEST["debug"]) print $query;
745 $result = db_query($query);
750 $select_qpart = "SELECT DISTINCT " .
754 "ttrss_entries.id as id," .
769 "(SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) AS hide_images," .
770 "last_marked, last_published, " .
773 $content_query_part .
777 $all_tags = explode(",", $feed);
778 if ($search_mode == 'any') {
779 $tag_sql = "tag_name in (" . implode(", ", array_map("db_quote", $all_tags)) . ")";
780 $from_qpart = " FROM ttrss_entries,ttrss_user_entries,ttrss_tags ";
781 $where_qpart = " WHERE " .
782 "ref_id = ttrss_entries.id AND " .
783 "ttrss_user_entries.owner_uid = $owner_uid AND " .
784 "post_int_id = int_id AND $tag_sql AND " .
787 $query_strategy_part . " ORDER BY $order_by " .
792 $sub_selects = array();
794 foreach ($all_tags as $term) {
795 array_push($sub_selects, "(SELECT post_int_id from ttrss_tags WHERE tag_name = " . db_quote($term) . " AND owner_uid = $owner_uid) as A$i");
802 array_push($sub_ands, "A$x.post_int_id = A$y.post_int_id");
807 array_push($sub_ands, "A1.post_int_id = ttrss_user_entries.int_id and ttrss_user_entries.owner_uid = $owner_uid");
808 array_push($sub_ands, "ttrss_user_entries.ref_id = ttrss_entries.id");
809 $from_qpart = " FROM " . implode(", ", $sub_selects) . ", ttrss_user_entries, ttrss_entries";
810 $where_qpart = " WHERE " . implode(" AND ", $sub_ands);
812 // error_log("TAG SQL: " . $tag_sql);
813 // $tag_sql = "tag_name = '$feed'"; DEFAULT way
815 // error_log("[". $select_qpart . "][" . $from_qpart . "][" .$where_qpart . "]");
816 $result = db_query($select_qpart . $from_qpart . $where_qpart);
819 return array($result, $feed_title, $feed_site_url, $last_error, $last_updated, $search_words);
823 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
824 if (!$owner) $owner = $_SESSION["uid"];
826 $res = trim($str); if (!$res) return '';
828 $charset_hack = '<head>
829 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
832 $res = trim($res); if (!$res) return '';
834 libxml_use_internal_errors(true);
836 $doc = new DOMDocument();
837 $doc->loadHTML($charset_hack . $res);
838 $xpath = new DOMXPath($doc);
840 $entries = $xpath->query('(//a[@href]|//img[@src])');
842 foreach ($entries as $entry) {
846 if ($entry->hasAttribute('href')) {
847 $entry->setAttribute('href',
848 rewrite_relative_url($site_url, $entry->getAttribute('href')));
850 $entry->setAttribute('rel', 'noreferrer');
853 if ($entry->hasAttribute('src')) {
854 $src = rewrite_relative_url($site_url, $entry->getAttribute('src'));
856 $cached_filename = CACHE_DIR . '/images/' . sha1($src) . '.png';
858 if (file_exists($cached_filename)) {
859 $src = SELF_URL_PATH . '/image.php?hash=' . sha1($src);
862 $entry->setAttribute('src', $src);
865 if ($entry->nodeName == 'img') {
866 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
867 $force_remove_images || $_SESSION["bw_limit"]) {
869 $p = $doc->createElement('p');
871 $a = $doc->createElement('a');
872 $a->setAttribute('href', $entry->getAttribute('src'));
874 $a->appendChild(new DOMText($entry->getAttribute('src')));
875 $a->setAttribute('target', '_blank');
879 $entry->parentNode->replaceChild($p, $entry);
884 if (strtolower($entry->nodeName) == "a") {
885 $entry->setAttribute("target", "_blank");
889 $entries = $xpath->query('//iframe');
890 foreach ($entries as $entry) {
891 $entry->setAttribute('sandbox', 'allow-scripts');
895 $allowed_elements = array('a', 'address', 'audio', 'article', 'aside',
896 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
897 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
898 'data', 'dd', 'del', 'details', 'div', 'dl', 'font',
899 'dt', 'em', 'footer', 'figure', 'figcaption',
900 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'html', 'i',
901 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
902 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
903 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
904 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
905 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video' );
907 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
909 $disallowed_attributes = array('id', 'style', 'class');
911 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
912 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
913 if (is_array($retval)) {
915 $allowed_elements = $retval[1];
916 $disallowed_attributes = $retval[2];
922 $doc->removeChild($doc->firstChild); //remove doctype
923 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
925 if ($highlight_words) {
926 foreach ($highlight_words as $word) {
928 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
930 $elements = $xpath->query("//*/text()");
932 foreach ($elements as $child) {
934 $fragment = $doc->createDocumentFragment();
935 $text = $child->textContent;
937 while (($pos = mb_stripos($text, $word)) !== false) {
938 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
939 $word = mb_substr($text, $pos, mb_strlen($word));
940 $highlight = $doc->createElement('span');
941 $highlight->appendChild(new DomText($word));
942 $highlight->setAttribute('class', 'highlight');
943 $fragment->appendChild($highlight);
944 $text = mb_substr($text, $pos + mb_strlen($word));
947 if (!empty($text)) $fragment->appendChild(new DomText($text));
949 $child->parentNode->replaceChild($fragment, $child);
954 $res = $doc->saveHTML();
959 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
960 $xpath = new DOMXPath($doc);
961 $entries = $xpath->query('//*');
963 foreach ($entries as $entry) {
964 if (!in_array($entry->nodeName, $allowed_elements)) {
965 $entry->parentNode->removeChild($entry);
968 if ($entry->hasAttributes()) {
969 $attrs_to_remove = array();
971 foreach ($entry->attributes as $attr) {
973 if (strpos($attr->nodeName, 'on') === 0) {
974 array_push($attrs_to_remove, $attr);
977 if (in_array($attr->nodeName, $disallowed_attributes)) {
978 array_push($attrs_to_remove, $attr);
982 foreach ($attrs_to_remove as $attr) {
983 $entry->removeAttributeNode($attr);
991 function check_for_update() {
992 if (CHECK_FOR_NEW_VERSION && $_SESSION['access_level'] >= 10) {
993 $version_url = "http://tt-rss.org/version.php?ver=" . VERSION .
994 "&iid=" . sha1(SELF_URL_PATH);
996 $version_data = @fetch_file_contents($version_url);
999 $version_data = json_decode($version_data, true);
1000 if ($version_data && $version_data['version']) {
1001 if (version_compare(VERSION_STATIC, $version_data['version']) == -1) {
1002 return $version_data;
1010 function catchupArticlesById($ids, $cmode, $owner_uid = false) {
1012 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
1013 if (count($ids) == 0) return;
1017 foreach ($ids as $id) {
1018 array_push($tmp_ids, "ref_id = '$id'");
1021 $ids_qpart = join(" OR ", $tmp_ids);
1024 db_query("UPDATE ttrss_user_entries SET
1025 unread = false,last_read = NOW()
1026 WHERE ($ids_qpart) AND owner_uid = $owner_uid");
1027 } else if ($cmode == 1) {
1028 db_query("UPDATE ttrss_user_entries SET
1030 WHERE ($ids_qpart) AND owner_uid = $owner_uid");
1032 db_query("UPDATE ttrss_user_entries SET
1033 unread = NOT unread,last_read = NOW()
1034 WHERE ($ids_qpart) AND owner_uid = $owner_uid");
1039 $result = db_query("SELECT DISTINCT feed_id FROM ttrss_user_entries
1040 WHERE ($ids_qpart) AND owner_uid = $owner_uid");
1042 while ($line = db_fetch_assoc($result)) {
1043 ccache_update($line["feed_id"], $owner_uid);
1047 function get_article_tags($id, $owner_uid = 0, $tag_cache = false) {
1049 $a_id = db_escape_string($id);
1051 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
1053 $query = "SELECT DISTINCT tag_name,
1054 owner_uid as owner FROM
1055 ttrss_tags WHERE post_int_id = (SELECT int_id FROM ttrss_user_entries WHERE
1056 ref_id = '$a_id' AND owner_uid = '$owner_uid' LIMIT 1) ORDER BY tag_name";
1060 /* check cache first */
1062 if ($tag_cache === false) {
1063 $result = db_query("SELECT tag_cache FROM ttrss_user_entries
1064 WHERE ref_id = '$id' AND owner_uid = $owner_uid");
1066 $tag_cache = db_fetch_result($result, 0, "tag_cache");
1070 $tags = explode(",", $tag_cache);
1073 /* do it the hard way */
1075 $tmp_result = db_query($query);
1077 while ($tmp_line = db_fetch_assoc($tmp_result)) {
1078 array_push($tags, $tmp_line["tag_name"]);
1081 /* update the cache */
1083 $tags_str = db_escape_string(join(",", $tags));
1085 db_query("UPDATE ttrss_user_entries
1086 SET tag_cache = '$tags_str' WHERE ref_id = '$id'
1087 AND owner_uid = $owner_uid");
1093 function trim_array($array) {
1095 array_walk($tmp, 'trim');
1099 function tag_is_valid($tag) {
1100 if ($tag == '') return false;
1101 if (preg_match("/^[0-9]*$/", $tag)) return false;
1102 if (mb_strlen($tag) > 250) return false;
1104 if (!$tag) return false;
1109 function render_login_form() {
1110 header('Cache-Control: public');
1112 require_once "login_form.php";
1116 function format_warning($msg, $id = "") {
1117 return "<div class=\"warning\" id=\"$id\">
1118 <span><img src=\"images/alert.png\"></span><span>$msg</span></div>";
1121 function format_notice($msg, $id = "") {
1122 return "<div class=\"notice\" id=\"$id\">
1123 <span><img src=\"images/information.png\"></span><span>$msg</span></div>";
1126 function format_error($msg, $id = "") {
1127 return "<div class=\"error\" id=\"$id\">
1128 <span><img src=\"images/alert.png\"></span><span>$msg</span></div>";
1131 function print_notice($msg) {
1132 return print format_notice($msg);
1135 function print_warning($msg) {
1136 return print format_warning($msg);
1139 function print_error($msg) {
1140 return print format_error($msg);
1144 function T_sprintf() {
1145 $args = func_get_args();
1146 return vsprintf(__(array_shift($args)), $args);
1149 function format_inline_player($url, $ctype) {
1153 $url = htmlspecialchars($url);
1155 if (strpos($ctype, "audio/") === 0) {
1157 if ($_SESSION["hasAudio"] && (strpos($ctype, "ogg") !== false ||
1158 $_SESSION["hasMp3"])) {
1160 $entry .= "<audio preload=\"none\" controls>
1161 <source type=\"$ctype\" src=\"$url\"></source>
1166 $entry .= "<object type=\"application/x-shockwave-flash\"
1167 data=\"lib/button/musicplayer.swf?song_url=$url\"
1168 width=\"17\" height=\"17\" style='float : left; margin-right : 5px;'>
1169 <param name=\"movie\"
1170 value=\"lib/button/musicplayer.swf?song_url=$url\" />
1174 if ($entry) $entry .= " <a target=\"_blank\"
1175 href=\"$url\">" . basename($url) . "</a>";
1183 /* $filename = substr($url, strrpos($url, "/")+1);
1185 $entry .= " <a target=\"_blank\" href=\"" . htmlspecialchars($url) . "\">" .
1186 $filename . " (" . $ctype . ")" . "</a>"; */
1190 function format_article($id, $mark_as_read = true, $zoom_mode = false, $owner_uid = false) {
1191 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
1197 /* we can figure out feed_id from article id anyway, why do we
1198 * pass feed_id here? let's ignore the argument :(*/
1200 $result = db_query("SELECT feed_id FROM ttrss_user_entries
1201 WHERE ref_id = '$id'");
1203 $feed_id = (int) db_fetch_result($result, 0, "feed_id");
1205 $rv['feed_id'] = $feed_id;
1207 //if (!$zoom_mode) { print "<article id='$id'><![CDATA["; };
1209 if ($mark_as_read) {
1210 $result = db_query("UPDATE ttrss_user_entries
1211 SET unread = false,last_read = NOW()
1212 WHERE ref_id = '$id' AND owner_uid = $owner_uid");
1214 ccache_update($feed_id, $owner_uid);
1217 $result = db_query("SELECT id,title,link,content,feed_id,comments,int_id,lang,
1218 ".SUBSTRING_FOR_DATE."(updated,1,16) as updated,
1219 (SELECT site_url FROM ttrss_feeds WHERE id = feed_id) as site_url,
1220 (SELECT title FROM ttrss_feeds WHERE id = feed_id) as feed_title,
1221 (SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) as hide_images,
1222 (SELECT always_display_enclosures FROM ttrss_feeds WHERE id = feed_id) as always_display_enclosures,
1228 FROM ttrss_entries,ttrss_user_entries
1229 WHERE id = '$id' AND ref_id = id AND owner_uid = $owner_uid");
1233 $line = db_fetch_assoc($result);
1235 $line["tags"] = get_article_tags($id, $owner_uid, $line["tag_cache"]);
1236 unset($line["tag_cache"]);
1238 $line["content"] = sanitize($line["content"],
1239 sql_bool_to_bool($line['hide_images']),
1240 $owner_uid, $line["site_url"], false, $line["id"]);
1242 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE) as $p) {
1243 $line = $p->hook_render_article($line);
1246 $num_comments = $line["num_comments"];
1247 $entry_comments = "";
1249 if ($num_comments > 0) {
1250 if ($line["comments"]) {
1251 $comments_url = htmlspecialchars($line["comments"]);
1253 $comments_url = htmlspecialchars($line["link"]);
1255 $entry_comments = "<a class=\"postComments\"
1256 target='_blank' href=\"$comments_url\">$num_comments ".
1257 _ngettext("comment", "comments", $num_comments)."</a>";
1260 if ($line["comments"] && $line["link"] != $line["comments"]) {
1261 $entry_comments = "<a class=\"postComments\" target='_blank' href=\"".htmlspecialchars($line["comments"])."\">".__("comments")."</a>";
1266 header("Content-Type: text/html");
1267 $rv['content'] .= "<html><head>
1268 <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>
1269 <title>Tiny Tiny RSS - ".$line["title"]."</title>".
1270 stylesheet_tag("css/tt-rss.css").
1271 stylesheet_tag("css/zoom.css").
1272 stylesheet_tag("css/dijit.css")."
1274 <link rel=\"shortcut icon\" type=\"image/png\" href=\"images/favicon.png\">
1275 <link rel=\"icon\" type=\"image/png\" sizes=\"72x72\" href=\"images/favicon-72px.png\">
1277 <script type=\"text/javascript\">
1278 function openSelectedAttachment(elem) {
1280 var url = elem[elem.selectedIndex].value;
1284 elem.selectedIndex = 0;
1288 exception_error(\"openSelectedAttachment\", e);
1292 </head><body id=\"ttrssZoom\">";
1295 $rv['content'] .= "<div class=\"postReply\" id=\"POST-$id\">";
1297 $rv['content'] .= "<div class=\"postHeader\" id=\"POSTHDR-$id\">";
1299 $entry_author = $line["author"];
1301 if ($entry_author) {
1302 $entry_author = __(" - ") . $entry_author;
1305 $parsed_updated = make_local_datetime($line["updated"], true,
1309 $rv['content'] .= "<div class=\"postDate\">$parsed_updated</div>";
1311 if ($line["link"]) {
1312 $rv['content'] .= "<div class='postTitle'><a target='_blank'
1313 title=\"".htmlspecialchars($line['title'])."\"
1315 htmlspecialchars($line["link"]) . "\">" .
1316 $line["title"] . "</a>" .
1317 "<span class='author'>$entry_author</span></div>";
1319 $rv['content'] .= "<div class='postTitle'>" . $line["title"] . "$entry_author</div>";
1323 $feed_title = "<a href=\"".htmlspecialchars($line["site_url"]).
1324 "\" target=\"_blank\">".
1325 htmlspecialchars($line["feed_title"])."</a>";
1327 $rv['content'] .= "<div class=\"postFeedTitle\">$feed_title</div>";
1329 $rv['content'] .= "<div class=\"postDate\">$parsed_updated</div>";
1332 $tags_str = format_tags_string($line["tags"], $id);
1333 $tags_str_full = join(", ", $line["tags"]);
1335 if (!$tags_str_full) $tags_str_full = __("no tags");
1337 if (!$entry_comments) $entry_comments = " "; # placeholder
1339 $rv['content'] .= "<div class='postTags' style='float : right'>
1340 <img src='images/tag.png'
1341 class='tagsPic' alt='Tags' title='Tags'> ";
1344 $rv['content'] .= "<span id=\"ATSTR-$id\">$tags_str</span>
1345 <a title=\"".__('Edit tags for this article')."\"
1346 href=\"#\" onclick=\"editArticleTags($id, $feed_id)\">(+)</a>";
1348 $rv['content'] .= "<div dojoType=\"dijit.Tooltip\"
1349 id=\"ATSTRTIP-$id\" connectId=\"ATSTR-$id\"
1350 position=\"below\">$tags_str_full</div>";
1352 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_BUTTON) as $p) {
1353 $rv['content'] .= $p->hook_article_button($line);
1357 $tags_str = strip_tags($tags_str);
1358 $rv['content'] .= "<span id=\"ATSTR-$id\">$tags_str</span>";
1360 $rv['content'] .= "</div>";
1361 $rv['content'] .= "<div clear='both'>";
1363 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_LEFT_BUTTON) as $p) {
1364 $rv['content'] .= $p->hook_article_left_button($line);
1367 $rv['content'] .= "$entry_comments</div>";
1369 if ($line["orig_feed_id"]) {
1371 $tmp_result = db_query("SELECT * FROM ttrss_archived_feeds
1372 WHERE id = ".$line["orig_feed_id"]);
1374 if (db_num_rows($tmp_result) != 0) {
1376 $rv['content'] .= "<div clear='both'>";
1377 $rv['content'] .= __("Originally from:");
1379 $rv['content'] .= " ";
1381 $tmp_line = db_fetch_assoc($tmp_result);
1383 $rv['content'] .= "<a target='_blank'
1384 href=' " . htmlspecialchars($tmp_line['site_url']) . "'>" .
1385 $tmp_line['title'] . "</a>";
1387 $rv['content'] .= " ";
1389 $rv['content'] .= "<a target='_blank' href='" . htmlspecialchars($tmp_line['feed_url']) . "'>";
1390 $rv['content'] .= "<img title='".__('Feed URL')."' class='tinyFeedIcon' src='images/pub_set.png'></a>";
1392 $rv['content'] .= "</div>";
1396 $rv['content'] .= "</div>";
1398 $rv['content'] .= "<div id=\"POSTNOTE-$id\">";
1399 if ($line['note']) {
1400 $rv['content'] .= format_article_note($id, $line['note'], !$zoom_mode);
1402 $rv['content'] .= "</div>";
1404 if (!$line['lang']) $line['lang'] = 'en';
1406 $rv['content'] .= "<div class=\"postContent\" lang=\"".$line['lang']."\">";
1408 $rv['content'] .= $line["content"];
1409 $rv['content'] .= format_article_enclosures($id,
1410 sql_bool_to_bool($line["always_display_enclosures"]),
1412 sql_bool_to_bool($line["hide_images"]));
1414 $rv['content'] .= "</div>";
1416 $rv['content'] .= "</div>";
1422 <div class='footer'>
1423 <button onclick=\"return window.close()\">".
1424 __("Close this window")."</button></div>";
1425 $rv['content'] .= "</body></html>";
1432 function print_checkpoint($n, $s) {
1433 $ts = microtime(true);
1434 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1438 function sanitize_tag($tag) {
1441 $tag = mb_strtolower($tag, 'utf-8');
1443 $tag = preg_replace('/[\'\"\+\>\<]/', "", $tag);
1445 // $tag = str_replace('"', "", $tag);
1446 // $tag = str_replace("+", " ", $tag);
1447 $tag = str_replace("technorati tag: ", "", $tag);
1452 function get_self_url_prefix() {
1453 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1454 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1456 return SELF_URL_PATH;
1461 * Compute the Mozilla Firefox feed adding URL from server HOST and REQUEST_URI.
1463 * @return string The Mozilla Firefox feed adding URL.
1465 function add_feed_url() {
1466 //$url_path = ($_SERVER['HTTPS'] != "on" ? 'http://' : 'https://') . $_SERVER["HTTP_HOST"] . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
1468 $url_path = get_self_url_prefix() .
1469 "/public.php?op=subscribe&feed_url=%s";
1471 } // function add_feed_url
1473 function encrypt_password($pass, $salt = '', $mode2 = false) {
1474 if ($salt && $mode2) {
1475 return "MODE2:" . hash('sha256', $salt . $pass);
1477 return "SHA1X:" . sha1("$salt:$pass");
1479 return "SHA1:" . sha1($pass);
1481 } // function encrypt_password
1483 function load_filters($feed_id, $owner_uid, $action_id = false) {
1486 $cat_id = (int)getFeedCategory($feed_id);
1489 $null_cat_qpart = "cat_id IS NULL OR";
1491 $null_cat_qpart = "";
1493 $result = db_query("SELECT * FROM ttrss_filters2 WHERE
1494 owner_uid = $owner_uid AND enabled = true ORDER BY order_id, title");
1496 $check_cats = join(",", array_merge(
1497 getParentCategories($cat_id, $owner_uid),
1500 while ($line = db_fetch_assoc($result)) {
1501 $filter_id = $line["id"];
1503 $result2 = db_query("SELECT
1504 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, t.name AS type_name
1505 FROM ttrss_filters2_rules AS r,
1506 ttrss_filter_types AS t
1508 ($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats)) AND
1509 (feed_id IS NULL OR feed_id = '$feed_id') AND
1510 filter_type = t.id AND filter_id = '$filter_id'");
1515 while ($rule_line = db_fetch_assoc($result2)) {
1516 # print_r($rule_line);
1519 $rule["reg_exp"] = $rule_line["reg_exp"];
1520 $rule["type"] = $rule_line["type_name"];
1521 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1523 array_push($rules, $rule);
1526 $result2 = db_query("SELECT a.action_param,t.name AS type_name
1527 FROM ttrss_filters2_actions AS a,
1528 ttrss_filter_actions AS t
1530 action_id = t.id AND filter_id = '$filter_id'");
1532 while ($action_line = db_fetch_assoc($result2)) {
1533 # print_r($action_line);
1536 $action["type"] = $action_line["type_name"];
1537 $action["param"] = $action_line["action_param"];
1539 array_push($actions, $action);
1544 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1545 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1546 $filter["rules"] = $rules;
1547 $filter["actions"] = $actions;
1549 if (count($rules) > 0 && count($actions) > 0) {
1550 array_push($filters, $filter);
1557 function get_score_pic($score) {
1559 return "score_high.png";
1560 } else if ($score > 0) {
1561 return "score_half_high.png";
1562 } else if ($score < -100) {
1563 return "score_low.png";
1564 } else if ($score < 0) {
1565 return "score_half_low.png";
1567 return "score_neutral.png";
1571 function feed_has_icon($id) {
1572 return is_file(ICONS_DIR . "/$id.ico") && filesize(ICONS_DIR . "/$id.ico") > 0;
1575 function init_plugins() {
1576 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
1581 function format_tags_string($tags, $id) {
1582 if (!is_array($tags) || count($tags) == 0) {
1583 return __("no tags");
1585 $maxtags = min(5, count($tags));
1587 for ($i = 0; $i < $maxtags; $i++) {
1588 $tags_str .= "<a class=\"tag\" href=\"#\" onclick=\"viewfeed('".$tags[$i]."')\">" . $tags[$i] . "</a>, ";
1591 $tags_str = mb_substr($tags_str, 0, mb_strlen($tags_str)-2);
1593 if (count($tags) > $maxtags)
1594 $tags_str .= ", …";
1600 function format_article_labels($labels, $id) {
1602 if (!is_array($labels)) return '';
1606 foreach ($labels as $l) {
1607 $labels_str .= sprintf("<span class='hlLabelRef'
1608 style='color : %s; background-color : %s'>%s</span>",
1609 $l[2], $l[3], $l[1]);
1616 function format_article_note($id, $note, $allow_edit = true) {
1618 $str = "<div class='articleNote' onclick=\"editArticleNote($id)\">
1619 <div class='noteEdit' onclick=\"editArticleNote($id)\">".
1620 ($allow_edit ? __('(edit note)') : "")."</div>$note</div>";
1626 function get_feed_category($feed_cat, $parent_cat_id = false) {
1627 if ($parent_cat_id) {
1628 $parent_qpart = "parent_cat = '$parent_cat_id'";
1629 $parent_insert = "'$parent_cat_id'";
1631 $parent_qpart = "parent_cat IS NULL";
1632 $parent_insert = "NULL";
1636 "SELECT id FROM ttrss_feed_categories
1637 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]);
1639 if (db_num_rows($result) == 0) {
1642 return db_fetch_result($result, 0, "id");
1646 function add_feed_category($feed_cat, $parent_cat_id = false) {
1648 if (!$feed_cat) return false;
1652 if ($parent_cat_id) {
1653 $parent_qpart = "parent_cat = '$parent_cat_id'";
1654 $parent_insert = "'$parent_cat_id'";
1656 $parent_qpart = "parent_cat IS NULL";
1657 $parent_insert = "NULL";
1660 $feed_cat = mb_substr($feed_cat, 0, 250);
1663 "SELECT id FROM ttrss_feed_categories
1664 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]);
1666 if (db_num_rows($result) == 0) {
1669 "INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1670 VALUES ('".$_SESSION["uid"]."', '$feed_cat', $parent_insert)");
1680 function getArticleFeed($id) {
1681 $result = db_query("SELECT feed_id FROM ttrss_user_entries
1682 WHERE ref_id = '$id' AND owner_uid = " . $_SESSION["uid"]);
1684 if (db_num_rows($result) != 0) {
1685 return db_fetch_result($result, 0, "feed_id");
1692 * Fixes incomplete URLs by prepending "http://".
1693 * Also replaces feed:// with http://, and
1694 * prepends a trailing slash if the url is a domain name only.
1696 * @param string $url Possibly incomplete URL
1698 * @return string Fixed URL.
1700 function fix_url($url) {
1701 if (strpos($url, '://') === false) {
1702 $url = 'http://' . $url;
1703 } else if (substr($url, 0, 5) == 'feed:') {
1704 $url = 'http:' . substr($url, 5);
1707 //prepend slash if the URL has no slash in it
1708 // "http://www.example" -> "http://www.example/"
1709 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
1713 if ($url != "http:///")
1719 function validate_feed_url($url) {
1720 $parts = parse_url($url);
1722 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
1726 function get_article_enclosures($id) {
1728 $query = "SELECT * FROM ttrss_enclosures
1729 WHERE post_id = '$id' AND content_url != ''";
1733 $result = db_query($query);
1735 if (db_num_rows($result) > 0) {
1736 while ($line = db_fetch_assoc($result)) {
1737 array_push($rv, $line);
1744 function save_email_address($email) {
1745 // FIXME: implement persistent storage of emails
1747 if (!$_SESSION['stored_emails'])
1748 $_SESSION['stored_emails'] = array();
1750 if (!in_array($email, $_SESSION['stored_emails']))
1751 array_push($_SESSION['stored_emails'], $email);
1755 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
1757 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
1759 $sql_is_cat = bool_to_sql_bool($is_cat);
1761 $result = db_query("SELECT access_key FROM ttrss_access_keys
1762 WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat
1763 AND owner_uid = " . $owner_uid);
1765 if (db_num_rows($result) == 1) {
1766 return db_fetch_result($result, 0, "access_key");
1768 $key = db_escape_string(uniqid(base_convert(rand(), 10, 36)));
1770 $result = db_query("INSERT INTO ttrss_access_keys
1771 (access_key, feed_id, is_cat, owner_uid)
1772 VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')");
1779 function get_feeds_from_html($url, $content)
1781 $url = fix_url($url);
1782 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
1784 libxml_use_internal_errors(true);
1786 $doc = new DOMDocument();
1787 $doc->loadHTML($content);
1788 $xpath = new DOMXPath($doc);
1789 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
1790 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
1791 $feedUrls = array();
1792 foreach ($entries as $entry) {
1793 if ($entry->hasAttribute('href')) {
1794 $title = $entry->getAttribute('title');
1796 $title = $entry->getAttribute('type');
1798 $feedUrl = rewrite_relative_url(
1799 $baseUrl, $entry->getAttribute('href')
1801 $feedUrls[$feedUrl] = $title;
1807 function is_html($content) {
1808 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 20)) !== 0;
1811 function url_is_html($url, $login = false, $pass = false) {
1812 return is_html(fetch_file_contents($url, false, $login, $pass));
1815 function print_label_select($name, $value, $attributes = "") {
1817 $result = db_query("SELECT caption FROM ttrss_labels2
1818 WHERE owner_uid = '".$_SESSION["uid"]."' ORDER BY caption");
1820 print "<select default=\"$value\" name=\"" . htmlspecialchars($name) .
1821 "\" $attributes onchange=\"labelSelectOnChange(this)\" >";
1823 while ($line = db_fetch_assoc($result)) {
1825 $issel = ($line["caption"] == $value) ? "selected=\"1\"" : "";
1827 print "<option value=\"".htmlspecialchars($line["caption"])."\"
1828 $issel>" . htmlspecialchars($line["caption"]) . "</option>";
1832 # print "<option value=\"ADD_LABEL\">" .__("Add label...") . "</option>";
1839 function format_article_enclosures($id, $always_display_enclosures,
1840 $article_content, $hide_images = false) {
1842 $result = get_article_enclosures($id);
1845 if (count($result) > 0) {
1847 $entries_html = array();
1849 $entries_inline = array();
1851 foreach ($result as $line) {
1853 $url = $line["content_url"];
1854 $ctype = $line["content_type"];
1855 $title = $line["title"];
1857 if (!$ctype) $ctype = __("unknown type");
1859 $filename = substr($url, strrpos($url, "/")+1);
1861 $player = format_inline_player($url, $ctype);
1863 if ($player) array_push($entries_inline, $player);
1865 # $entry .= " <a target=\"_blank\" href=\"" . htmlspecialchars($url) . "\">" .
1866 # $filename . " (" . $ctype . ")" . "</a>";
1868 $entry = "<div onclick=\"window.open('".htmlspecialchars($url)."')\"
1869 dojoType=\"dijit.MenuItem\">$filename ($ctype)</div>";
1871 array_push($entries_html, $entry);
1875 $entry["type"] = $ctype;
1876 $entry["filename"] = $filename;
1877 $entry["url"] = $url;
1878 $entry["title"] = $title;
1880 array_push($entries, $entry);
1883 if ($_SESSION['uid'] && !get_pref("STRIP_IMAGES") && !$_SESSION["bw_limit"]) {
1884 if ($always_display_enclosures ||
1885 !preg_match("/<img/i", $article_content)) {
1887 foreach ($entries as $entry) {
1889 if (preg_match("/image/", $entry["type"]) ||
1890 preg_match("/\.(jpg|png|gif|bmp)/i", $entry["filename"])) {
1892 if (!$hide_images) {
1894 alt=\"".htmlspecialchars($entry["filename"])."\"
1895 src=\"" .htmlspecialchars($entry["url"]) . "\"/></p>";
1897 $rv .= "<p><a target=\"_blank\"
1898 href=\"".htmlspecialchars($entry["url"])."\"
1899 >" .htmlspecialchars($entry["url"]) . "</a></p>";
1902 if ($entry['title']) {
1903 $rv.= "<div class=\"enclosure_title\">${entry['title']}</div>";
1910 if (count($entries_inline) > 0) {
1911 $rv .= "<hr clear='both'/>";
1912 foreach ($entries_inline as $entry) { $rv .= $entry; };
1913 $rv .= "<hr clear='both'/>";
1916 $rv .= "<select class=\"attachments\" onchange=\"openSelectedAttachment(this)\">".
1917 "<option value=''>" . __('Attachments')."</option>";
1919 foreach ($entries as $entry) {
1920 if ($entry["title"])
1921 $title = "— " . truncate_string($entry["title"], 30);
1925 $rv .= "<option value=\"".htmlspecialchars($entry["url"])."\">" . htmlspecialchars($entry["filename"]) . "$title</option>";
1935 function getLastArticleId() {
1936 $result = db_query("SELECT MAX(ref_id) AS id FROM ttrss_user_entries
1937 WHERE owner_uid = " . $_SESSION["uid"]);
1939 if (db_num_rows($result) == 1) {
1940 return db_fetch_result($result, 0, "id");
1946 function build_url($parts) {
1947 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
1951 * Converts a (possibly) relative URL to a absolute one.
1953 * @param string $url Base URL (i.e. from where the document is)
1954 * @param string $rel_url Possibly relative URL in the document
1956 * @return string Absolute URL
1958 function rewrite_relative_url($url, $rel_url) {
1959 if (strpos($rel_url, ":") !== false) {
1961 } else if (strpos($rel_url, "://") !== false) {
1963 } else if (strpos($rel_url, "//") === 0) {
1964 # protocol-relative URL (rare but they exist)
1966 } else if (strpos($rel_url, "/") === 0)
1968 $parts = parse_url($url);
1969 $parts['path'] = $rel_url;
1971 return build_url($parts);
1974 $parts = parse_url($url);
1975 if (!isset($parts['path'])) {
1976 $parts['path'] = '/';
1978 $dir = $parts['path'];
1979 if (substr($dir, -1) !== '/') {
1980 $dir = dirname($parts['path']);
1981 $dir !== '/' && $dir .= '/';
1983 $parts['path'] = $dir . $rel_url;
1985 return build_url($parts);
1989 function sphinx_search($query, $offset = 0, $limit = 30) {
1990 require_once 'lib/sphinxapi.php';
1992 $sphinxClient = new SphinxClient();
1994 $sphinxpair = explode(":", SPHINX_SERVER, 2);
1996 $sphinxClient->SetServer($sphinxpair[0], (int)$sphinxpair[1]);
1997 $sphinxClient->SetConnectTimeout(1);
1999 $sphinxClient->SetFieldWeights(array('title' => 70, 'content' => 30,
2000 'feed_title' => 20));
2002 $sphinxClient->SetMatchMode(SPH_MATCH_EXTENDED2);
2003 $sphinxClient->SetRankingMode(SPH_RANK_PROXIMITY_BM25);
2004 $sphinxClient->SetLimits($offset, $limit, 1000);
2005 $sphinxClient->SetArrayResult(false);
2006 $sphinxClient->SetFilter('owner_uid', array($_SESSION['uid']));
2008 $result = $sphinxClient->Query($query, SPHINX_INDEX);
2012 if (is_array($result['matches'])) {
2013 foreach (array_keys($result['matches']) as $int_id) {
2014 $ref_id = $result['matches'][$int_id]['attrs']['ref_id'];
2015 array_push($ids, $ref_id);
2022 function cleanup_tags($days = 14, $limit = 1000) {
2024 if (DB_TYPE == "pgsql") {
2025 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2026 } else if (DB_TYPE == "mysql") {
2027 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2032 while ($limit > 0) {
2035 $query = "SELECT ttrss_tags.id AS id
2036 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2037 WHERE post_int_id = int_id AND $interval_query AND
2038 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT $limit_part";
2040 $result = db_query($query);
2044 while ($line = db_fetch_assoc($result)) {
2045 array_push($ids, $line['id']);
2048 if (count($ids) > 0) {
2049 $ids = join(",", $ids);
2051 $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2052 $tags_deleted += db_affected_rows($tmp_result);
2057 $limit -= $limit_part;
2060 return $tags_deleted;
2063 function print_user_stylesheet() {
2064 $value = get_pref('USER_STYLESHEET');
2067 print "<style type=\"text/css\">";
2068 print str_replace("<br/>", "\n", $value);
2074 function filter_to_sql($filter, $owner_uid) {
2077 if (DB_TYPE == "pgsql")
2080 $reg_qpart = "REGEXP";
2082 foreach ($filter["rules"] AS $rule) {
2083 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2084 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2085 $rule['reg_exp']) !== FALSE;
2087 if ($regexp_valid) {
2089 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
2091 switch ($rule["type"]) {
2093 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2094 $rule['reg_exp'] . "')";
2097 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2098 $rule['reg_exp'] . "')";
2101 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2102 $rule['reg_exp'] . "') OR LOWER(" .
2103 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2106 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2107 $rule['reg_exp'] . "')";
2110 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2111 $rule['reg_exp'] . "')";
2114 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2115 $rule['reg_exp'] . "')";
2119 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2121 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2122 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
2125 if (isset($rule["cat_id"])) {
2127 if ($rule["cat_id"] > 0) {
2128 $children = getChildCategories($rule["cat_id"], $owner_uid);
2129 array_push($children, $rule["cat_id"]);
2131 $children = join(",", $children);
2133 $cat_qpart = "cat_id IN ($children)";
2135 $cat_qpart = "cat_id IS NULL";
2138 $qpart .= " AND $cat_qpart";
2141 $qpart .= " AND feed_id IS NOT NULL";
2143 array_push($query, "($qpart)");
2148 if (count($query) > 0) {
2149 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2151 $fullquery = "(false)";
2154 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2159 if (!function_exists('gzdecode')) {
2160 function gzdecode($string) { // no support for 2nd argument
2161 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2162 base64_encode($string));
2166 function get_random_bytes($length) {
2167 if (function_exists('openssl_random_pseudo_bytes')) {
2168 return openssl_random_pseudo_bytes($length);
2172 for ($i = 0; $i < $length; $i++)
2173 $output .= chr(mt_rand(0, 255));
2179 function read_stdin() {
2180 $fp = fopen("php://stdin", "r");
2183 $line = trim(fgets($fp));
2191 function tmpdirname($path, $prefix) {
2192 // Use PHP's tmpfile function to create a temporary
2193 // directory name. Delete the file and keep the name.
2194 $tempname = tempnam($path,$prefix);
2198 if (!unlink($tempname))
2204 function getFeedCategory($feed) {
2205 $result = db_query("SELECT cat_id FROM ttrss_feeds
2206 WHERE id = '$feed'");
2208 if (db_num_rows($result) > 0) {
2209 return db_fetch_result($result, 0, "cat_id");
2216 function implements_interface($class, $interface) {
2217 return in_array($interface, class_implements($class));
2220 function geturl($url, $depth = 0){
2222 if ($depth == 20) return $url;
2224 if (!function_exists('curl_init'))
2225 return user_error('CURL Must be installed for geturl function to work. Ask your host to enable it or uncomment extension=php_curl.dll in php.ini', E_USER_ERROR);
2227 $curl = curl_init();
2228 $header[0] = "Accept: text/xml,application/xml,application/xhtml+xml,";
2229 $header[0] .= "text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5";
2230 $header[] = "Cache-Control: max-age=0";
2231 $header[] = "Connection: keep-alive";
2232 $header[] = "Keep-Alive: 300";
2233 $header[] = "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7";
2234 $header[] = "Accept-Language: en-us,en;q=0.5";
2235 $header[] = "Pragma: ";
2237 curl_setopt($curl, CURLOPT_URL, $url);
2238 curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 5.1; rv:5.0) Gecko/20100101 Firefox/5.0 Firefox/5.0');
2239 curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
2240 curl_setopt($curl, CURLOPT_HEADER, true);
2241 curl_setopt($curl, CURLOPT_REFERER, $url);
2242 curl_setopt($curl, CURLOPT_ENCODING, 'gzip,deflate');
2243 curl_setopt($curl, CURLOPT_AUTOREFERER, true);
2244 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
2245 //curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); //CURLOPT_FOLLOWLOCATION Disabled...
2246 curl_setopt($curl, CURLOPT_TIMEOUT, 60);
2247 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
2249 if (defined('_CURL_HTTP_PROXY')) {
2250 curl_setopt($curl, CURLOPT_PROXY, _CURL_HTTP_PROXY);
2253 if ((OPENSSL_VERSION_NUMBER >= 0x0090808f) && (OPENSSL_VERSION_NUMBER < 0x10000000)) {
2254 curl_setopt($curl, CURLOPT_SSLVERSION, 3);
2257 $html = curl_exec($curl);
2259 $status = curl_getinfo($curl);
2261 if($status['http_code']!=200){
2262 if($status['http_code'] == 301 || $status['http_code'] == 302) {
2264 list($header) = explode("\r\n\r\n", $html, 2);
2266 preg_match("/(Location:|URI:)[^(\n)]*/", $header, $matches);
2267 $url = trim(str_replace($matches[1],"",$matches[0]));
2268 $url_parsed = parse_url($url);
2269 return (isset($url_parsed))? geturl($url, $depth + 1):'';
2272 global $fetch_last_error;
2274 $fetch_last_error = curl_errno($curl) . " " . curl_error($curl);
2278 # foreach($status as $key=>$eline){$oline.='['.$key.']'.$eline.' ';}
2279 # $line =$oline." \r\n ".$url."\r\n-----------------\r\n";
2280 # $handle = @fopen('./curl.error.log', 'a');
2281 # fwrite($handle, $line);
2288 function get_minified_js($files) {
2289 require_once 'lib/jshrink/Minifier.php';
2293 foreach ($files as $js) {
2294 if (!isset($_GET['debug'])) {
2295 $cached_file = CACHE_DIR . "/js/".basename($js).".js";
2297 if (file_exists($cached_file) &&
2298 is_readable($cached_file) &&
2299 filemtime($cached_file) >= filemtime("js/$js.js")) {
2301 $rv .= file_get_contents($cached_file);
2304 $minified = JShrink\Minifier::minify(file_get_contents("js/$js.js"));
2305 file_put_contents($cached_file, $minified);
2309 $rv .= file_get_contents("js/$js.js");
2316 function stylesheet_tag($filename) {
2317 $timestamp = filemtime($filename);
2319 return "<link rel=\"stylesheet\" type=\"text/css\" href=\"$filename?$timestamp\"/>\n";
2322 function javascript_tag($filename) {
2325 if (!(strpos($filename, "?") === FALSE)) {
2326 $query = substr($filename, strpos($filename, "?")+1);
2327 $filename = substr($filename, 0, strpos($filename, "?"));
2330 $timestamp = filemtime($filename);
2332 if ($query) $timestamp .= "&$query";
2334 return "<script type=\"text/javascript\" charset=\"utf-8\" src=\"$filename?$timestamp\"></script>\n";
2337 function calculate_dep_timestamp() {
2338 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2342 foreach ($files as $file) {
2343 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2349 function T_js_decl($s1, $s2) {
2351 $s1 = preg_replace("/\n/", "", $s1);
2352 $s2 = preg_replace("/\n/", "", $s2);
2354 $s1 = preg_replace("/\"/", "\\\"", $s1);
2355 $s2 = preg_replace("/\"/", "\\\"", $s2);
2357 return "T_messages[\"$s1\"] = \"$s2\";\n";
2361 function init_js_translations() {
2363 print 'var T_messages = new Object();
2366 if (T_messages[msg]) {
2367 return T_messages[msg];
2373 function ngettext(msg1, msg2, n) {
2374 return __((parseInt(n) > 1) ? msg2 : msg1);
2377 $l10n = _get_reader();
2379 for ($i = 0; $i < $l10n->total; $i++) {
2380 $orig = $l10n->get_original_string($i);
2381 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2382 $key = explode(chr(0), $orig);
2383 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2384 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2386 $translation = __($orig);
2387 print T_js_decl($orig, $translation);
2392 function label_to_feed_id($label) {
2393 return LABEL_BASE_INDEX - 1 - abs($label);
2396 function feed_to_label_id($feed) {
2397 return LABEL_BASE_INDEX - 1 + abs($feed);
2400 function format_libxml_error($error) {
2401 return T_sprintf("LibXML error %s at line %d (column %d): %s",
2402 $error->code, $error->line, $error->column,