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