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