]> git.wh0rd.org - tt-rss.git/blob - include/functions.php
move JShrink Minifier to vendor/
[tt-rss.git] / include / functions.php
1 <?php
2 define('EXPECTED_CONFIG_VERSION', 26);
3 define('SCHEMA_VERSION', 134);
4
5 define('LABEL_BASE_INDEX', -1024);
6 define('PLUGIN_FEED_BASE_INDEX', -128);
7
8 define('COOKIE_LIFETIME_LONG', 86400*365);
9
10 $fetch_last_error = false;
11 $fetch_last_error_code = false;
12 $fetch_last_content_type = false;
13 $fetch_last_error_content = false; // curl only for the time being
14 $fetch_effective_url = false;
15 $fetch_curl_used = false;
16 $suppress_debugging = false;
17
18 libxml_disable_entity_loader(true);
19
20 // separate test because this is included before sanity checks
21 if (function_exists("mb_internal_encoding")) mb_internal_encoding("UTF-8");
22
23 date_default_timezone_set('UTC');
24 if (defined('E_DEPRECATED')) {
25 error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
26 } else {
27 error_reporting(E_ALL & ~E_NOTICE);
28 }
29
30 require_once 'config.php';
31
32 /**
33 * Define a constant if not already defined
34 */
35 function define_default($name, $value) {
36 defined($name) or define($name, $value);
37 }
38
39 /* Some tunables you can override in config.php using define(): */
40
41 define_default('FEED_FETCH_TIMEOUT', 45);
42 // How may seconds to wait for response when requesting feed from a site
43 define_default('FEED_FETCH_NO_CACHE_TIMEOUT', 15);
44 // How may seconds to wait for response when requesting feed from a
45 // site when that feed wasn't cached before
46 define_default('FILE_FETCH_TIMEOUT', 45);
47 // Default timeout when fetching files from remote sites
48 define_default('FILE_FETCH_CONNECT_TIMEOUT', 15);
49 // How many seconds to wait for initial response from website when
50 // fetching files from remote sites
51 define_default('DAEMON_UPDATE_LOGIN_LIMIT', 30);
52 // stop updating feeds if users haven't logged in for X days
53 define_default('DAEMON_FEED_LIMIT', 500);
54 // feed limit for one update batch
55 define_default('DAEMON_SLEEP_INTERVAL', 120);
56 // default sleep interval between feed updates (sec)
57 define_default('MIN_CACHE_FILE_SIZE', 1024);
58 // do not cache files smaller than that (bytes)
59 define_default('MAX_CACHE_FILE_SIZE', 64*1024*1024);
60 // do not cache files larger than that (bytes)
61 define_default('MAX_DOWNLOAD_FILE_SIZE', 16*1024*1024);
62 // do not download general files larger than that (bytes)
63 define_default('CACHE_MAX_DAYS', 7);
64 // max age in days for various automatically cached (temporary) files
65 define_default('MAX_CONDITIONAL_INTERVAL', 3600*12);
66 // max interval between forced unconditional updates for servers
67 // not complying with http if-modified-since (seconds)
68
69 /* tunables end here */
70
71 if (DB_TYPE == "pgsql") {
72 define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
73 } else {
74 define('SUBSTRING_FOR_DATE', 'SUBSTRING');
75 }
76
77 /**
78 * Return available translations names.
79 *
80 * @access public
81 * @return array A array of available translations.
82 */
83 function get_translations() {
84 $tr = array(
85 "auto" => "Detect automatically",
86 "ar_SA" => "العربيّة (Arabic)",
87 "bg_BG" => "Bulgarian",
88 "da_DA" => "Dansk",
89 "ca_CA" => "Català",
90 "cs_CZ" => "Česky",
91 "en_US" => "English",
92 "el_GR" => "Ελληνικά",
93 "es_ES" => "Español (España)",
94 "es_LA" => "Español",
95 "de_DE" => "Deutsch",
96 "fr_FR" => "Français",
97 "hu_HU" => "Magyar (Hungarian)",
98 "it_IT" => "Italiano",
99 "ja_JP" => "日本語 (Japanese)",
100 "lv_LV" => "Latviešu",
101 "nb_NO" => "Norwegian bokmål",
102 "nl_NL" => "Dutch",
103 "pl_PL" => "Polski",
104 "ru_RU" => "Русский",
105 "pt_BR" => "Portuguese/Brazil",
106 "pt_PT" => "Portuguese/Portugal",
107 "zh_CN" => "Simplified Chinese",
108 "zh_TW" => "Traditional Chinese",
109 "sv_SE" => "Svenska",
110 "fi_FI" => "Suomi",
111 "tr_TR" => "Türkçe");
112
113 return $tr;
114 }
115
116 require_once "lib/accept-to-gettext.php";
117 require_once "lib/gettext/gettext.inc";
118
119 function startup_gettext() {
120
121 # Get locale from Accept-Language header
122 $lang = al2gt(array_keys(get_translations()), "text/html");
123
124 if (defined('_TRANSLATION_OVERRIDE_DEFAULT')) {
125 $lang = _TRANSLATION_OVERRIDE_DEFAULT;
126 }
127
128 if ($_SESSION["uid"] && get_schema_version() >= 120) {
129 $pref_lang = get_pref("USER_LANGUAGE", $_SESSION["uid"]);
130
131 if ($pref_lang && $pref_lang != 'auto') {
132 $lang = $pref_lang;
133 }
134 }
135
136 if ($lang) {
137 if (defined('LC_MESSAGES')) {
138 _setlocale(LC_MESSAGES, $lang);
139 } else if (defined('LC_ALL')) {
140 _setlocale(LC_ALL, $lang);
141 }
142
143 _bindtextdomain("messages", "locale");
144
145 _textdomain("messages");
146 _bind_textdomain_codeset("messages", "UTF-8");
147 }
148 }
149
150 require_once 'db-prefs.php';
151 require_once 'version.php';
152 require_once 'controls.php';
153
154 define('SELF_USER_AGENT', 'Tiny Tiny RSS/' . VERSION . ' (http://tt-rss.org/)');
155 ini_set('user_agent', SELF_USER_AGENT);
156
157 $schema_version = false;
158
159 function _debug_suppress($suppress) {
160 global $suppress_debugging;
161
162 $suppress_debugging = $suppress;
163 }
164
165 /**
166 * Print a timestamped debug message.
167 *
168 * @param string $msg The debug message.
169 * @return void
170 */
171 function _debug($msg, $show = true) {
172 global $suppress_debugging;
173
174 //echo "[$suppress_debugging] $msg $show\n";
175
176 if ($suppress_debugging) return false;
177
178 $ts = strftime("%H:%M:%S", time());
179 if (function_exists('posix_getpid')) {
180 $ts = "$ts/" . posix_getpid();
181 }
182
183 if ($show && !(defined('QUIET') && QUIET)) {
184 print "[$ts] $msg\n";
185 }
186
187 if (defined('LOGFILE')) {
188 $fp = fopen(LOGFILE, 'a+');
189
190 if ($fp) {
191 $locked = false;
192
193 if (function_exists("flock")) {
194 $tries = 0;
195
196 // try to lock logfile for writing
197 while ($tries < 5 && !$locked = flock($fp, LOCK_EX | LOCK_NB)) {
198 sleep(1);
199 ++$tries;
200 }
201
202 if (!$locked) {
203 fclose($fp);
204 return;
205 }
206 }
207
208 fputs($fp, "[$ts] $msg\n");
209
210 if (function_exists("flock")) {
211 flock($fp, LOCK_UN);
212 }
213
214 fclose($fp);
215 }
216 }
217
218 } // function _debug
219
220 /**
221 * Purge a feed old posts.
222 *
223 * @param mixed $link A database connection.
224 * @param mixed $feed_id The id of the purged feed.
225 * @param mixed $purge_interval Olderness of purged posts.
226 * @param boolean $debug Set to True to enable the debug. False by default.
227 * @access public
228 * @return void
229 */
230 function purge_feed($feed_id, $purge_interval, $debug = false) {
231
232 if (!$purge_interval) $purge_interval = feed_purge_interval($feed_id);
233
234 $pdo = Db::pdo();
235
236 $sth = $pdo->prepare("SELECT owner_uid FROM ttrss_feeds WHERE id = ?");
237 $sth->execute([$feed_id]);
238
239 $owner_uid = false;
240
241 if ($row = $sth->fetch()) {
242 $owner_uid = $row["owner_uid"];
243 }
244
245 if ($purge_interval == -1 || !$purge_interval) {
246 if ($owner_uid) {
247 CCache::update($feed_id, $owner_uid);
248 }
249 return;
250 }
251
252 if (!$owner_uid) return;
253
254 if (FORCE_ARTICLE_PURGE == 0) {
255 $purge_unread = get_pref("PURGE_UNREAD_ARTICLES",
256 $owner_uid, false);
257 } else {
258 $purge_unread = true;
259 $purge_interval = FORCE_ARTICLE_PURGE;
260 }
261
262 if (!$purge_unread)
263 $query_limit = " unread = false AND ";
264 else
265 $query_limit = "";
266
267 $purge_interval = (int) $purge_interval;
268
269 if (DB_TYPE == "pgsql") {
270 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
271 USING ttrss_entries
272 WHERE ttrss_entries.id = ref_id AND
273 marked = false AND
274 feed_id = ? AND
275 $query_limit
276 ttrss_entries.date_updated < NOW() - INTERVAL '$purge_interval days'");
277 $sth->execute([$feed_id]);
278
279 } else {
280 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
281 USING ttrss_user_entries, ttrss_entries
282 WHERE ttrss_entries.id = ref_id AND
283 marked = false AND
284 feed_id = ? AND
285 $query_limit
286 ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)");
287 $sth->execute([$feed_id]);
288
289 }
290
291 $rows = $sth->rowCount();
292
293 CCache::update($feed_id, $owner_uid);
294
295 if ($debug) {
296 _debug("Purged feed $feed_id ($purge_interval): deleted $rows articles");
297 }
298
299 return $rows;
300 } // function purge_feed
301
302 function feed_purge_interval($feed_id) {
303
304 $pdo = DB::pdo();
305
306 $sth = $pdo->prepare("SELECT purge_interval, owner_uid FROM ttrss_feeds
307 WHERE id = ?");
308 $sth->execute([$feed_id]);
309
310 if ($row = $sth->fetch()) {
311 $purge_interval = $row["purge_interval"];
312 $owner_uid = $row["owner_uid"];
313
314 if ($purge_interval == 0) $purge_interval = get_pref(
315 'PURGE_OLD_DAYS', $owner_uid);
316
317 return $purge_interval;
318
319 } else {
320 return -1;
321 }
322 }
323
324 // TODO: max_size currently only works for CURL transfers
325 // TODO: multiple-argument way is deprecated, first parameter is a hash now
326 function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
327 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
328
329 global $fetch_last_error;
330 global $fetch_last_error_code;
331 global $fetch_last_error_content;
332 global $fetch_last_content_type;
333 global $fetch_last_modified;
334 global $fetch_effective_url;
335 global $fetch_curl_used;
336
337 $fetch_last_error = false;
338 $fetch_last_error_code = -1;
339 $fetch_last_error_content = "";
340 $fetch_last_content_type = "";
341 $fetch_curl_used = false;
342 $fetch_last_modified = "";
343 $fetch_effective_url = "";
344
345 if (!is_array($options)) {
346
347 // falling back on compatibility shim
348 $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
349 $tmp = [];
350
351 for ($i = 0; $i < func_num_args(); $i++) {
352 $tmp[$option_names[$i]] = func_get_arg($i);
353 }
354
355 $options = $tmp;
356
357 /*$options = array(
358 "url" => func_get_arg(0),
359 "type" => @func_get_arg(1),
360 "login" => @func_get_arg(2),
361 "pass" => @func_get_arg(3),
362 "post_query" => @func_get_arg(4),
363 "timeout" => @func_get_arg(5),
364 "timestamp" => @func_get_arg(6),
365 "useragent" => @func_get_arg(7)
366 ); */
367 }
368
369 $url = $options["url"];
370 $type = isset($options["type"]) ? $options["type"] : false;
371 $login = isset($options["login"]) ? $options["login"] : false;
372 $pass = isset($options["pass"]) ? $options["pass"] : false;
373 $post_query = isset($options["post_query"]) ? $options["post_query"] : false;
374 $timeout = isset($options["timeout"]) ? $options["timeout"] : false;
375 $last_modified = isset($options["last_modified"]) ? $options["last_modified"] : "";
376 $useragent = isset($options["useragent"]) ? $options["useragent"] : false;
377 $followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
378 $max_size = isset($options["max_size"]) ? $options["max_size"] : MAX_DOWNLOAD_FILE_SIZE; // in bytes
379 $http_accept = isset($options["http_accept"]) ? $options["http_accept"] : false;
380
381 $url = ltrim($url, ' ');
382 $url = str_replace(' ', '%20', $url);
383
384 if (strpos($url, "//") === 0)
385 $url = 'http:' . $url;
386
387 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
388
389 $fetch_curl_used = true;
390
391 $ch = curl_init($url);
392
393 $curl_http_headers = [];
394
395 if ($last_modified && !$post_query)
396 array_push($curl_http_headers, "If-Modified-Since: $last_modified");
397
398 if ($http_accept)
399 array_push($curl_http_headers, "Accept: " . $http_accept);
400
401 if (count($curl_http_headers) > 0)
402 curl_setopt($ch, CURLOPT_HTTPHEADER, $curl_http_headers);
403
404 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout ? $timeout : FILE_FETCH_CONNECT_TIMEOUT);
405 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ? $timeout : FILE_FETCH_TIMEOUT);
406 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, !ini_get("open_basedir") && $followlocation);
407 curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
408 curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
409 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
410 curl_setopt($ch, CURLOPT_HEADER, true);
411 curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
412 curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent :
413 SELF_USER_AGENT);
414 curl_setopt($ch, CURLOPT_ENCODING, "");
415 //curl_setopt($ch, CURLOPT_REFERER, $url);
416
417 if ($max_size) {
418 curl_setopt($ch, CURLOPT_NOPROGRESS, false);
419 curl_setopt($ch, CURLOPT_BUFFERSIZE, 16384); // needed to get 5 arguments in progress function?
420
421 // holy shit closures in php
422 // download & upload are *expected* sizes respectively, could be zero
423 curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($curl_handle, $download_size, $downloaded, $upload_size, $uploaded) use( &$max_size) {
424 //_debug("[curl progressfunction] $downloaded $max_size");
425
426 return ($downloaded > $max_size) ? 1 : 0; // if max size is set, abort when exceeding it
427 });
428
429 }
430
431 if (!ini_get("open_basedir")) {
432 curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
433 }
434
435 if (defined('_HTTP_PROXY')) {
436 curl_setopt($ch, CURLOPT_PROXY, _HTTP_PROXY);
437 }
438
439 if ($post_query) {
440 curl_setopt($ch, CURLOPT_POST, true);
441 curl_setopt($ch, CURLOPT_POSTFIELDS, $post_query);
442 }
443
444 if ($login && $pass)
445 curl_setopt($ch, CURLOPT_USERPWD, "$login:$pass");
446
447 $ret = @curl_exec($ch);
448
449 $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
450 $headers = explode("\r\n", substr($ret, 0, $headers_length));
451 $contents = substr($ret, $headers_length);
452
453 foreach ($headers as $header) {
454 if (strstr($header, ": ") !== FALSE) {
455 list ($key, $value) = explode(": ", $header);
456
457 if (strtolower($key) == "last-modified") {
458 $fetch_last_modified = $value;
459 }
460 }
461
462 if (substr(strtolower($header), 0, 7) == 'http/1.') {
463 $fetch_last_error_code = (int) substr($header, 9, 3);
464 $fetch_last_error = $header;
465 }
466 }
467
468 if (curl_errno($ch) === 23 || curl_errno($ch) === 61) {
469 curl_setopt($ch, CURLOPT_ENCODING, 'none');
470 $contents = @curl_exec($ch);
471 }
472
473 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
474 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
475
476 $fetch_effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
477
478 $fetch_last_error_code = $http_code;
479
480 if ($http_code != 200 || $type && strpos($fetch_last_content_type, "$type") === false) {
481
482 if (curl_errno($ch) != 0) {
483 $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
484 }
485
486 $fetch_last_error_content = $contents;
487 curl_close($ch);
488 return false;
489 }
490
491 if (!$contents) {
492 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
493 curl_close($ch);
494 return false;
495 }
496
497 curl_close($ch);
498
499 return $contents;
500 } else {
501
502 $fetch_curl_used = false;
503
504 if ($login && $pass){
505 $url_parts = array();
506
507 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
508
509 $pass = urlencode($pass);
510
511 if ($url_parts[1] && $url_parts[2]) {
512 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
513 }
514 }
515
516 // TODO: should this support POST requests or not? idk
517
518 $context_options = array(
519 'http' => array(
520 'header' => array(
521 'Connection: close'
522 ),
523 'method' => 'GET',
524 'ignore_errors' => true,
525 'timeout' => $timeout ? $timeout : FILE_FETCH_TIMEOUT,
526 'protocol_version'=> 1.1)
527 );
528
529 if (!$post_query && $last_modified)
530 array_push($context_options['http']['header'], "If-Modified-Since: $last_modified");
531
532 if ($http_accept)
533 array_push($context_options['http']['header'], "Accept: $http_accept");
534
535 if (defined('_HTTP_PROXY')) {
536 $context_options['http']['request_fulluri'] = true;
537 $context_options['http']['proxy'] = _HTTP_PROXY;
538 }
539
540 $context = stream_context_create($context_options);
541
542 $old_error = error_get_last();
543
544 $fetch_effective_url = $url;
545
546 $data = @file_get_contents($url, false, $context);
547
548 if (isset($http_response_header) && is_array($http_response_header)) {
549 foreach ($http_response_header as $header) {
550 if (strstr($header, ": ") !== FALSE) {
551 list ($key, $value) = explode(": ", $header);
552
553 $key = strtolower($key);
554
555 if ($key == 'content-type') {
556 $fetch_last_content_type = $value;
557 // don't abort here b/c there might be more than one
558 // e.g. if we were being redirected -- last one is the right one
559 } else if ($key == 'last-modified') {
560 $fetch_last_modified = $value;
561 } else if ($key == 'location') {
562 $fetch_effective_url = $value;
563 }
564 }
565
566 if (substr(strtolower($header), 0, 7) == 'http/1.') {
567 $fetch_last_error_code = (int) substr($header, 9, 3);
568 $fetch_last_error = $header;
569 }
570 }
571 }
572
573 if ($fetch_last_error_code != 200) {
574 $error = error_get_last();
575
576 if ($error['message'] != $old_error['message']) {
577 $fetch_last_error .= "; " . $error["message"];
578 }
579
580 $fetch_last_error_content = $data;
581
582 return false;
583 }
584 return $data;
585 }
586
587 }
588
589 /**
590 * Try to determine the favicon URL for a feed.
591 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
592 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
593 *
594 * @param string $url A feed or page URL
595 * @access public
596 * @return mixed The favicon URL, or false if none was found.
597 */
598 function get_favicon_url($url) {
599
600 $favicon_url = false;
601
602 if ($html = @fetch_file_contents($url)) {
603
604 libxml_use_internal_errors(true);
605
606 $doc = new DOMDocument();
607 $doc->loadHTML($html);
608 $xpath = new DOMXPath($doc);
609
610 $base = $xpath->query('/html/head/base[@href]');
611 foreach ($base as $b) {
612 $url = rewrite_relative_url($url, $b->getAttribute("href"));
613 break;
614 }
615
616 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
617 if (count($entries) > 0) {
618 foreach ($entries as $entry) {
619 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
620 break;
621 }
622 }
623 }
624
625 if (!$favicon_url)
626 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
627
628 return $favicon_url;
629 } // function get_favicon_url
630
631 function initialize_user_prefs($uid, $profile = false) {
632
633 if (get_schema_version() < 63) $profile_qpart = "";
634
635 $pdo = DB::pdo();
636 $in_nested_tr = false;
637
638 try {
639 $pdo->beginTransaction();
640 } catch (Exception $e) {
641 $in_nested_tr = true;
642 }
643
644 $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
645
646 $profile = $profile ? $profile : null;
647
648 $u_sth = $pdo->prepare("SELECT pref_name
649 FROM ttrss_user_prefs WHERE owner_uid = :uid AND
650 (profile = :profile OR (:profile IS NULL AND profile IS NULL))");
651 $u_sth->execute([':uid' => $uid, ':profile' => $profile]);
652
653 $active_prefs = array();
654
655 while ($line = $u_sth->fetch()) {
656 array_push($active_prefs, $line["pref_name"]);
657 }
658
659 while ($line = $sth->fetch()) {
660 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
661 // print "adding " . $line["pref_name"] . "<br>";
662
663 if (get_schema_version() < 63) {
664 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
665 (owner_uid,pref_name,value) VALUES
666 (?, ?, ?)");
667 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
668
669 } else {
670 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
671 (owner_uid,pref_name,value, profile) VALUES
672 (?, ?, ?, ?)");
673 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
674 }
675
676 }
677 }
678
679 if (!$in_nested_tr) $pdo->commit();
680
681 }
682
683 function get_ssl_certificate_id() {
684 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
685 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
686 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
687 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
688 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
689 }
690 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
691 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
692 $_SERVER["SSL_CLIENT_V_START"] .
693 $_SERVER["SSL_CLIENT_V_END"] .
694 $_SERVER["SSL_CLIENT_S_DN"]);
695 }
696 return "";
697 }
698
699 function authenticate_user($login, $password, $check_only = false) {
700
701 if (!SINGLE_USER_MODE) {
702 $user_id = false;
703
704 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_AUTH_USER) as $plugin) {
705
706 $user_id = (int) $plugin->authenticate($login, $password);
707
708 if ($user_id) {
709 $_SESSION["auth_module"] = strtolower(get_class($plugin));
710 break;
711 }
712 }
713
714 if ($user_id && !$check_only) {
715 @session_start();
716
717 $_SESSION["uid"] = $user_id;
718 $_SESSION["version"] = VERSION_STATIC;
719
720 $pdo = DB::pdo();
721 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
722 WHERE id = ?");
723 $sth->execute([$user_id]);
724 $row = $sth->fetch();
725
726 $_SESSION["name"] = $row["login"];
727 $_SESSION["access_level"] = $row["access_level"];
728 $_SESSION["csrf_token"] = uniqid_short();
729
730 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
731 $usth->execute([$user_id]);
732
733 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
734 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
735 $_SESSION["pwd_hash"] = $row["pwd_hash"];
736
737 $_SESSION["last_version_check"] = time();
738
739 initialize_user_prefs($_SESSION["uid"]);
740
741 return true;
742 }
743
744 return false;
745
746 } else {
747
748 $_SESSION["uid"] = 1;
749 $_SESSION["name"] = "admin";
750 $_SESSION["access_level"] = 10;
751
752 $_SESSION["hide_hello"] = true;
753 $_SESSION["hide_logout"] = true;
754
755 $_SESSION["auth_module"] = false;
756
757 if (!$_SESSION["csrf_token"]) {
758 $_SESSION["csrf_token"] = uniqid_short();
759 }
760
761 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
762
763 initialize_user_prefs($_SESSION["uid"]);
764
765 return true;
766 }
767 }
768
769 // this is used for user http parameters unless HTML code is actually needed
770 function clean($param) {
771 if (is_array($param)) {
772 return array_map("strip_tags", $param);
773 } else if (is_string($param)) {
774 return strip_tags($param);
775 } else {
776 return $param;
777 }
778 }
779
780 function make_password($length = 8) {
781
782 $password = "";
783 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
784
785 $i = 0;
786
787 while ($i < $length) {
788 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
789
790 if (!strstr($password, $char)) {
791 $password .= $char;
792 $i++;
793 }
794 }
795 return $password;
796 }
797
798 // this is called after user is created to initialize default feeds, labels
799 // or whatever else
800
801 // user preferences are checked on every login, not here
802
803 function initialize_user($uid) {
804
805 $pdo = DB::pdo();
806
807 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
808 values (?, 'Tiny Tiny RSS: Forum',
809 'http://tt-rss.org/forum/rss.php')");
810 $sth->execute([$uid]);
811 }
812
813 function logout_user() {
814 session_destroy();
815 if (isset($_COOKIE[session_name()])) {
816 setcookie(session_name(), '', time()-42000, '/');
817 }
818 }
819
820 function validate_csrf($csrf_token) {
821 return $csrf_token == $_SESSION['csrf_token'];
822 }
823
824 function load_user_plugins($owner_uid, $pluginhost = false) {
825
826 if (!$pluginhost) $pluginhost = PluginHost::getInstance();
827
828 if ($owner_uid && SCHEMA_VERSION >= 100) {
829 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
830
831 $pluginhost->load($plugins, PluginHost::KIND_USER, $owner_uid);
832
833 if (get_schema_version() > 100) {
834 $pluginhost->load_data();
835 }
836 }
837 }
838
839 function login_sequence() {
840 $pdo = Db::pdo();
841
842 if (SINGLE_USER_MODE) {
843 @session_start();
844 authenticate_user("admin", null);
845 startup_gettext();
846 load_user_plugins($_SESSION["uid"]);
847 } else {
848 if (!validate_session()) $_SESSION["uid"] = false;
849
850 if (!$_SESSION["uid"]) {
851
852 if (AUTH_AUTO_LOGIN && authenticate_user(null, null)) {
853 $_SESSION["ref_schema_version"] = get_schema_version(true);
854 } else {
855 authenticate_user(null, null, true);
856 }
857
858 if (!$_SESSION["uid"]) {
859 @session_destroy();
860 setcookie(session_name(), '', time()-42000, '/');
861
862 render_login_form();
863 exit;
864 }
865
866 } else {
867 /* bump login timestamp */
868 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
869 $sth->execute([$_SESSION['uid']]);
870
871 $_SESSION["last_login_update"] = time();
872 }
873
874 if ($_SESSION["uid"]) {
875 startup_gettext();
876 load_user_plugins($_SESSION["uid"]);
877
878 /* cleanup ccache */
879
880 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
881 AND
882 (SELECT COUNT(id) FROM ttrss_feeds WHERE
883 ttrss_feeds.id = feed_id) = 0");
884
885 $sth->execute([$_SESSION['uid']]);
886
887 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
888 AND
889 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
890 ttrss_feed_categories.id = feed_id) = 0");
891
892 $sth->execute([$_SESSION['uid']]);
893 }
894
895 }
896 }
897
898 function truncate_string($str, $max_len, $suffix = '&hellip;') {
899 if (mb_strlen($str, "utf-8") > $max_len) {
900 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
901 } else {
902 return $str;
903 }
904 }
905
906 // is not utf8 clean
907 function truncate_middle($str, $max_len, $suffix = '&hellip;') {
908 if (strlen($str) > $max_len) {
909 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
910 } else {
911 return $str;
912 }
913 }
914
915 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
916
917 try {
918 $source_tz = new DateTimeZone($source_tz);
919 } catch (Exception $e) {
920 $source_tz = new DateTimeZone('UTC');
921 }
922
923 try {
924 $dest_tz = new DateTimeZone($dest_tz);
925 } catch (Exception $e) {
926 $dest_tz = new DateTimeZone('UTC');
927 }
928
929 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
930 return $dt->format('U') + $dest_tz->getOffset($dt);
931 }
932
933 function make_local_datetime($timestamp, $long, $owner_uid = false,
934 $no_smart_dt = false, $eta_min = false) {
935
936 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
937 if (!$timestamp) $timestamp = '1970-01-01 0:00';
938
939 global $utc_tz;
940 global $user_tz;
941
942 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
943
944 $timestamp = substr($timestamp, 0, 19);
945
946 # We store date in UTC internally
947 $dt = new DateTime($timestamp, $utc_tz);
948
949 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
950
951 if ($user_tz_string != 'Automatic') {
952
953 try {
954 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
955 } catch (Exception $e) {
956 $user_tz = $utc_tz;
957 }
958
959 $tz_offset = $user_tz->getOffset($dt);
960 } else {
961 $tz_offset = (int) -$_SESSION["clientTzOffset"];
962 }
963
964 $user_timestamp = $dt->format('U') + $tz_offset;
965
966 if (!$no_smart_dt) {
967 return smart_date_time($user_timestamp,
968 $tz_offset, $owner_uid, $eta_min);
969 } else {
970 if ($long)
971 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
972 else
973 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
974
975 return date($format, $user_timestamp);
976 }
977 }
978
979 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
980 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
981
982 if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
983 return T_sprintf("%d min", date("i", time() + $tz_offset - $timestamp));
984 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() + $tz_offset)) {
985 return date("G:i", $timestamp);
986 } else if (date("Y", $timestamp) == date("Y", time() + $tz_offset)) {
987 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
988 return date($format, $timestamp);
989 } else {
990 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
991 return date($format, $timestamp);
992 }
993 }
994
995 function sql_bool_to_bool($s) {
996 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
997 }
998
999 function bool_to_sql_bool($s) {
1000 return $s ? 1 : 0;
1001 }
1002
1003 // Session caching removed due to causing wrong redirects to upgrade
1004 // script when get_schema_version() is called on an obsolete session
1005 // created on a previous schema version.
1006 function get_schema_version($nocache = false) {
1007 global $schema_version;
1008
1009 $pdo = DB::pdo();
1010
1011 if (!$schema_version && !$nocache) {
1012 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
1013 $version = $row["schema_version"];
1014 $schema_version = $version;
1015 return $version;
1016 } else {
1017 return $schema_version;
1018 }
1019 }
1020
1021 function sanity_check() {
1022 require_once 'errors.php';
1023 global $ERRORS;
1024
1025 $error_code = 0;
1026 $schema_version = get_schema_version(true);
1027
1028 if ($schema_version != SCHEMA_VERSION) {
1029 $error_code = 5;
1030 }
1031
1032 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
1033 }
1034
1035 function file_is_locked($filename) {
1036 if (file_exists(LOCK_DIRECTORY . "/$filename")) {
1037 if (function_exists('flock')) {
1038 $fp = @fopen(LOCK_DIRECTORY . "/$filename", "r");
1039 if ($fp) {
1040 if (flock($fp, LOCK_EX | LOCK_NB)) {
1041 flock($fp, LOCK_UN);
1042 fclose($fp);
1043 return false;
1044 }
1045 fclose($fp);
1046 return true;
1047 } else {
1048 return false;
1049 }
1050 }
1051 return true; // consider the file always locked and skip the test
1052 } else {
1053 return false;
1054 }
1055 }
1056
1057
1058 function make_lockfile($filename) {
1059 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
1060
1061 if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
1062 $stat_h = fstat($fp);
1063 $stat_f = stat(LOCK_DIRECTORY . "/$filename");
1064
1065 if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
1066 if ($stat_h["ino"] != $stat_f["ino"] ||
1067 $stat_h["dev"] != $stat_f["dev"]) {
1068
1069 return false;
1070 }
1071 }
1072
1073 if (function_exists('posix_getpid')) {
1074 fwrite($fp, posix_getpid() . "\n");
1075 }
1076 return $fp;
1077 } else {
1078 return false;
1079 }
1080 }
1081
1082 function make_stampfile($filename) {
1083 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
1084
1085 if (flock($fp, LOCK_EX | LOCK_NB)) {
1086 fwrite($fp, time() . "\n");
1087 flock($fp, LOCK_UN);
1088 fclose($fp);
1089 return true;
1090 } else {
1091 return false;
1092 }
1093 }
1094
1095 function sql_random_function() {
1096 if (DB_TYPE == "mysql") {
1097 return "RAND()";
1098 } else {
1099 return "RANDOM()";
1100 }
1101 }
1102
1103 function getFeedUnread($feed, $is_cat = false) {
1104 return Feeds::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1105 }
1106
1107 function checkbox_to_sql_bool($val) {
1108 return ($val == "on") ? 1 : 0;
1109 }
1110
1111 function uniqid_short() {
1112 return uniqid(base_convert(rand(), 10, 36));
1113 }
1114
1115 function make_init_params() {
1116 $params = array();
1117
1118 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1119 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1120 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1121 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1122
1123 $params[strtolower($param)] = (int) get_pref($param);
1124 }
1125
1126 $params["icons_url"] = ICONS_URL;
1127 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
1128 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1129 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1130 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1131 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1132 $params["is_default_pw"] = Pref_Prefs::isdefaultpassword();
1133 $params["label_base_index"] = (int) LABEL_BASE_INDEX;
1134
1135 $theme = get_pref( "USER_CSS_THEME", false, false);
1136 $params["theme"] = theme_valid("$theme") ? $theme : "";
1137
1138 $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
1139
1140 $params["php_platform"] = PHP_OS;
1141 $params["php_version"] = PHP_VERSION;
1142
1143 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1144
1145 $pdo = Db::pdo();
1146
1147 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1148 ttrss_feeds WHERE owner_uid = ?");
1149 $sth->execute([$_SESSION['uid']]);
1150 $row = $sth->fetch();
1151
1152 $max_feed_id = $row["mid"];
1153 $num_feeds = $row["nf"];
1154
1155 $params["max_feed_id"] = (int) $max_feed_id;
1156 $params["num_feeds"] = (int) $num_feeds;
1157
1158 $params["hotkeys"] = get_hotkeys_map();
1159
1160 $params["csrf_token"] = $_SESSION["csrf_token"];
1161 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1162
1163 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE;
1164
1165 $params["icon_alert"] = base64_img("images/alert.png");
1166 $params["icon_information"] = base64_img("images/information.png");
1167 $params["icon_cross"] = base64_img("images/cross.png");
1168 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1169
1170 $params["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1171
1172 return $params;
1173 }
1174
1175 function get_hotkeys_info() {
1176 $hotkeys = array(
1177 __("Navigation") => array(
1178 "next_feed" => __("Open next feed"),
1179 "prev_feed" => __("Open previous feed"),
1180 "next_article" => __("Open next article"),
1181 "prev_article" => __("Open previous article"),
1182 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1183 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1184 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1185 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1186 "search_dialog" => __("Show search dialog")),
1187 __("Article") => array(
1188 "toggle_mark" => __("Toggle starred"),
1189 "toggle_publ" => __("Toggle published"),
1190 "toggle_unread" => __("Toggle unread"),
1191 "edit_tags" => __("Edit tags"),
1192 "open_in_new_window" => __("Open in new window"),
1193 "catchup_below" => __("Mark below as read"),
1194 "catchup_above" => __("Mark above as read"),
1195 "article_scroll_down" => __("Scroll down"),
1196 "article_scroll_up" => __("Scroll up"),
1197 "select_article_cursor" => __("Select article under cursor"),
1198 "email_article" => __("Email article"),
1199 "close_article" => __("Close/collapse article"),
1200 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1201 "toggle_widescreen" => __("Toggle widescreen mode"),
1202 "toggle_embed_original" => __("Toggle embed original")),
1203 __("Article selection") => array(
1204 "select_all" => __("Select all articles"),
1205 "select_unread" => __("Select unread"),
1206 "select_marked" => __("Select starred"),
1207 "select_published" => __("Select published"),
1208 "select_invert" => __("Invert selection"),
1209 "select_none" => __("Deselect everything")),
1210 __("Feed") => array(
1211 "feed_refresh" => __("Refresh current feed"),
1212 "feed_unhide_read" => __("Un/hide read feeds"),
1213 "feed_subscribe" => __("Subscribe to feed"),
1214 "feed_edit" => __("Edit feed"),
1215 "feed_catchup" => __("Mark as read"),
1216 "feed_reverse" => __("Reverse headlines"),
1217 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1218 "feed_debug_update" => __("Debug feed update"),
1219 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1220 "catchup_all" => __("Mark all feeds as read"),
1221 "cat_toggle_collapse" => __("Un/collapse current category"),
1222 "toggle_combined_mode" => __("Toggle combined mode"),
1223 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1224 __("Go to") => array(
1225 "goto_all" => __("All articles"),
1226 "goto_fresh" => __("Fresh"),
1227 "goto_marked" => __("Starred"),
1228 "goto_published" => __("Published"),
1229 "goto_tagcloud" => __("Tag cloud"),
1230 "goto_prefs" => __("Preferences")),
1231 __("Other") => array(
1232 "create_label" => __("Create label"),
1233 "create_filter" => __("Create filter"),
1234 "collapse_sidebar" => __("Un/collapse sidebar"),
1235 "help_dialog" => __("Show help dialog"))
1236 );
1237
1238 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
1239 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1240 }
1241
1242 return $hotkeys;
1243 }
1244
1245 function get_hotkeys_map() {
1246 $hotkeys = array(
1247 // "navigation" => array(
1248 "k" => "next_feed",
1249 "j" => "prev_feed",
1250 "n" => "next_article",
1251 "p" => "prev_article",
1252 "(38)|up" => "prev_article",
1253 "(40)|down" => "next_article",
1254 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1255 // "^(40)|Ctrl-down" => "next_article_noscroll",
1256 "(191)|/" => "search_dialog",
1257 // "article" => array(
1258 "s" => "toggle_mark",
1259 "*s" => "toggle_publ",
1260 "u" => "toggle_unread",
1261 "*t" => "edit_tags",
1262 "o" => "open_in_new_window",
1263 "c p" => "catchup_below",
1264 "c n" => "catchup_above",
1265 "*n" => "article_scroll_down",
1266 "*p" => "article_scroll_up",
1267 "*(38)|Shift+up" => "article_scroll_up",
1268 "*(40)|Shift+down" => "article_scroll_down",
1269 "a *w" => "toggle_widescreen",
1270 "a e" => "toggle_embed_original",
1271 "e" => "email_article",
1272 "a q" => "close_article",
1273 // "article_selection" => array(
1274 "a a" => "select_all",
1275 "a u" => "select_unread",
1276 "a *u" => "select_marked",
1277 "a p" => "select_published",
1278 "a i" => "select_invert",
1279 "a n" => "select_none",
1280 // "feed" => array(
1281 "f r" => "feed_refresh",
1282 "f a" => "feed_unhide_read",
1283 "f s" => "feed_subscribe",
1284 "f e" => "feed_edit",
1285 "f q" => "feed_catchup",
1286 "f x" => "feed_reverse",
1287 "f g" => "feed_toggle_vgroup",
1288 "f *d" => "feed_debug_update",
1289 "f *g" => "feed_debug_viewfeed",
1290 "f *c" => "toggle_combined_mode",
1291 "f c" => "toggle_cdm_expanded",
1292 "*q" => "catchup_all",
1293 "x" => "cat_toggle_collapse",
1294 // "goto" => array(
1295 "g a" => "goto_all",
1296 "g f" => "goto_fresh",
1297 "g s" => "goto_marked",
1298 "g p" => "goto_published",
1299 "g t" => "goto_tagcloud",
1300 "g *p" => "goto_prefs",
1301 // "other" => array(
1302 "(9)|Tab" => "select_article_cursor", // tab
1303 "c l" => "create_label",
1304 "c f" => "create_filter",
1305 "c s" => "collapse_sidebar",
1306 "^(191)|Ctrl+/" => "help_dialog",
1307 );
1308
1309 if (get_pref('COMBINED_DISPLAY_MODE')) {
1310 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1311 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1312 }
1313
1314 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
1315 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1316 }
1317
1318 $prefixes = array();
1319
1320 foreach (array_keys($hotkeys) as $hotkey) {
1321 $pair = explode(" ", $hotkey, 2);
1322
1323 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1324 array_push($prefixes, $pair[0]);
1325 }
1326 }
1327
1328 return array($prefixes, $hotkeys);
1329 }
1330
1331 function check_for_update() {
1332 if (defined("GIT_VERSION_TIMESTAMP")) {
1333 $content = @fetch_file_contents(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1334
1335 if ($content) {
1336 $content = json_decode($content, true);
1337
1338 if ($content && isset($content["changeset"])) {
1339 if ((int)GIT_VERSION_TIMESTAMP < (int)$content["changeset"]["timestamp"] &&
1340 GIT_VERSION_HEAD != $content["changeset"]["id"]) {
1341
1342 return $content["changeset"]["id"];
1343 }
1344 }
1345 }
1346 }
1347
1348 return "";
1349 }
1350
1351 function make_runtime_info($disable_update_check = false) {
1352 $data = array();
1353
1354 $pdo = Db::pdo();
1355
1356 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1357 ttrss_feeds WHERE owner_uid = ?");
1358 $sth->execute([$_SESSION['uid']]);
1359 $row = $sth->fetch();
1360
1361 $max_feed_id = $row['mid'];
1362 $num_feeds = $row['nf'];
1363
1364 $data["max_feed_id"] = (int) $max_feed_id;
1365 $data["num_feeds"] = (int) $num_feeds;
1366
1367 $data['last_article_id'] = Article::getLastArticleId();
1368 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1369
1370 $data['dep_ts'] = calculate_dep_timestamp();
1371 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1372
1373 $data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1374
1375 if (CHECK_FOR_UPDATES && !$disable_update_check && $_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
1376 $update_result = @check_for_update();
1377
1378 $data["update_result"] = $update_result;
1379
1380 $_SESSION["last_version_check"] = time();
1381 }
1382
1383 if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
1384
1385 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1386
1387 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1388
1389 $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
1390
1391 if ($stamp) {
1392 $stamp_delta = time() - $stamp;
1393
1394 if ($stamp_delta > 1800) {
1395 $stamp_check = 0;
1396 } else {
1397 $stamp_check = 1;
1398 $_SESSION["daemon_stamp_check"] = time();
1399 }
1400
1401 $data['daemon_stamp_ok'] = $stamp_check;
1402
1403 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1404
1405 $data['daemon_stamp'] = $stamp_fmt;
1406 }
1407 }
1408 }
1409
1410 return $data;
1411 }
1412
1413 function search_to_sql($search, $search_language) {
1414
1415 $keywords = str_getcsv(trim($search), " ");
1416 $query_keywords = array();
1417 $search_words = array();
1418 $search_query_leftover = array();
1419
1420 $pdo = Db::pdo();
1421
1422 if ($search_language)
1423 $search_language = $pdo->quote(mb_strtolower($search_language));
1424 else
1425 $search_language = $pdo->quote("english");
1426
1427 foreach ($keywords as $k) {
1428 if (strpos($k, "-") === 0) {
1429 $k = substr($k, 1);
1430 $not = "NOT";
1431 } else {
1432 $not = "";
1433 }
1434
1435 $commandpair = explode(":", mb_strtolower($k), 2);
1436
1437 switch ($commandpair[0]) {
1438 case "title":
1439 if ($commandpair[1]) {
1440 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1441 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1442 } else {
1443 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1444 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1445 array_push($search_words, $k);
1446 }
1447 break;
1448 case "author":
1449 if ($commandpair[1]) {
1450 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1451 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1452 } else {
1453 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1454 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1455 array_push($search_words, $k);
1456 }
1457 break;
1458 case "note":
1459 if ($commandpair[1]) {
1460 if ($commandpair[1] == "true")
1461 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1462 else if ($commandpair[1] == "false")
1463 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1464 else
1465 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1466 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1467 } else {
1468 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1469 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1470 if (!$not) array_push($search_words, $k);
1471 }
1472 break;
1473 case "star":
1474
1475 if ($commandpair[1]) {
1476 if ($commandpair[1] == "true")
1477 array_push($query_keywords, "($not (marked = true))");
1478 else
1479 array_push($query_keywords, "($not (marked = false))");
1480 } else {
1481 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1482 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1483 if (!$not) array_push($search_words, $k);
1484 }
1485 break;
1486 case "pub":
1487 if ($commandpair[1]) {
1488 if ($commandpair[1] == "true")
1489 array_push($query_keywords, "($not (published = true))");
1490 else
1491 array_push($query_keywords, "($not (published = false))");
1492
1493 } else {
1494 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1495 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1496 if (!$not) array_push($search_words, $k);
1497 }
1498 break;
1499 case "unread":
1500 if ($commandpair[1]) {
1501 if ($commandpair[1] == "true")
1502 array_push($query_keywords, "($not (unread = true))");
1503 else
1504 array_push($query_keywords, "($not (unread = false))");
1505
1506 } else {
1507 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1508 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1509 if (!$not) array_push($search_words, $k);
1510 }
1511 break;
1512 default:
1513 if (strpos($k, "@") === 0) {
1514
1515 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1516 $orig_ts = strtotime(substr($k, 1));
1517 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1518
1519 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1520
1521 array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
1522 } else {
1523
1524 if (DB_TYPE == "pgsql") {
1525 $k = mb_strtolower($k);
1526 array_push($search_query_leftover, $not ? "!$k" : $k);
1527 } else {
1528 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1529 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1530 }
1531
1532 if (!$not) array_push($search_words, $k);
1533 }
1534 }
1535 }
1536
1537 if (count($search_query_leftover) > 0) {
1538 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1539
1540 if (DB_TYPE == "pgsql") {
1541 array_push($query_keywords,
1542 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1543 }
1544
1545 }
1546
1547 $search_query_part = implode("AND", $query_keywords);
1548
1549 return array($search_query_part, $search_words);
1550 }
1551
1552 function iframe_whitelisted($entry) {
1553 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1554
1555 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
1556
1557 if ($src) {
1558 foreach ($whitelist as $w) {
1559 if ($src == $w || $src == "www.$w")
1560 return true;
1561 }
1562 }
1563
1564 return false;
1565 }
1566
1567 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1568 if (!$owner) $owner = $_SESSION["uid"];
1569
1570 $res = trim($str); if (!$res) return '';
1571
1572 $charset_hack = '<head>
1573 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1574 </head>';
1575
1576 $res = trim($res); if (!$res) return '';
1577
1578 libxml_use_internal_errors(true);
1579
1580 $doc = new DOMDocument();
1581 $doc->loadHTML($charset_hack . $res);
1582 $xpath = new DOMXPath($doc);
1583
1584 $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
1585
1586 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1587
1588 foreach ($entries as $entry) {
1589
1590 if ($entry->hasAttribute('href')) {
1591 $entry->setAttribute('href',
1592 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1593
1594 $entry->setAttribute('rel', 'noopener noreferrer');
1595 }
1596
1597 if ($entry->hasAttribute('src')) {
1598 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1599 $cached_filename = CACHE_DIR . '/images/' . sha1($src);
1600
1601 if (file_exists($cached_filename)) {
1602
1603 // this is strictly cosmetic
1604 if ($entry->tagName == 'img') {
1605 $suffix = ".png";
1606 } else if ($entry->parentNode && $entry->parentNode->tagName == "video") {
1607 $suffix = ".mp4";
1608 } else if ($entry->parentNode && $entry->parentNode->tagName == "audio") {
1609 $suffix = ".ogg";
1610 } else {
1611 $suffix = "";
1612 }
1613
1614 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1615
1616 if ($entry->hasAttribute('srcset')) {
1617 $entry->removeAttribute('srcset');
1618 }
1619
1620 if ($entry->hasAttribute('sizes')) {
1621 $entry->removeAttribute('sizes');
1622 }
1623 }
1624
1625 $entry->setAttribute('src', $src);
1626 }
1627
1628 if ($entry->nodeName == 'img') {
1629 $entry->setAttribute('referrerpolicy', 'no-referrer');
1630
1631 $entry->removeAttribute('width');
1632 $entry->removeAttribute('height');
1633
1634 if ($entry->hasAttribute('src')) {
1635 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
1636
1637 if (is_prefix_https() && !$is_https_url) {
1638
1639 if ($entry->hasAttribute('srcset')) {
1640 $entry->removeAttribute('srcset');
1641 }
1642
1643 if ($entry->hasAttribute('sizes')) {
1644 $entry->removeAttribute('sizes');
1645 }
1646 }
1647 }
1648
1649 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1650 $force_remove_images || $_SESSION["bw_limit"]) {
1651
1652 $p = $doc->createElement('p');
1653
1654 $a = $doc->createElement('a');
1655 $a->setAttribute('href', $entry->getAttribute('src'));
1656
1657 $a->appendChild(new DOMText($entry->getAttribute('src')));
1658 $a->setAttribute('target', '_blank');
1659 $a->setAttribute('rel', 'noopener noreferrer');
1660
1661 $p->appendChild($a);
1662
1663 $entry->parentNode->replaceChild($p, $entry);
1664 }
1665 }
1666
1667 if (strtolower($entry->nodeName) == "a") {
1668 $entry->setAttribute("target", "_blank");
1669 $entry->setAttribute("rel", "noopener noreferrer");
1670 }
1671 }
1672
1673 $entries = $xpath->query('//iframe');
1674 foreach ($entries as $entry) {
1675 if (!iframe_whitelisted($entry)) {
1676 $entry->setAttribute('sandbox', 'allow-scripts');
1677 } else {
1678 if (is_prefix_https()) {
1679 $entry->setAttribute("src",
1680 str_replace("http://", "https://",
1681 $entry->getAttribute("src")));
1682 }
1683 }
1684 }
1685
1686 $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
1687 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1688 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1689 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1690 'dt', 'em', 'footer', 'figure', 'figcaption',
1691 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1692 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1693 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1694 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1695 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1696 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1697
1698 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1699
1700 $disallowed_attributes = array('id', 'style', 'class');
1701
1702 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1703 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1704 if (is_array($retval)) {
1705 $doc = $retval[0];
1706 $allowed_elements = $retval[1];
1707 $disallowed_attributes = $retval[2];
1708 } else {
1709 $doc = $retval;
1710 }
1711 }
1712
1713 $doc->removeChild($doc->firstChild); //remove doctype
1714 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1715
1716 if ($highlight_words) {
1717 foreach ($highlight_words as $word) {
1718
1719 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1720
1721 $elements = $xpath->query("//*/text()");
1722
1723 foreach ($elements as $child) {
1724
1725 $fragment = $doc->createDocumentFragment();
1726 $text = $child->textContent;
1727
1728 while (($pos = mb_stripos($text, $word)) !== false) {
1729 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1730 $word = mb_substr($text, $pos, mb_strlen($word));
1731 $highlight = $doc->createElement('span');
1732 $highlight->appendChild(new DomText($word));
1733 $highlight->setAttribute('class', 'highlight');
1734 $fragment->appendChild($highlight);
1735 $text = mb_substr($text, $pos + mb_strlen($word));
1736 }
1737
1738 if (!empty($text)) $fragment->appendChild(new DomText($text));
1739
1740 $child->parentNode->replaceChild($fragment, $child);
1741 }
1742 }
1743 }
1744
1745 $res = $doc->saveHTML();
1746
1747 /* strip everything outside of <body>...</body> */
1748
1749 $res_frag = array();
1750 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1751 return $res_frag[1];
1752 } else {
1753 return $res;
1754 }
1755 }
1756
1757 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1758 $xpath = new DOMXPath($doc);
1759 $entries = $xpath->query('//*');
1760
1761 foreach ($entries as $entry) {
1762 if (!in_array($entry->nodeName, $allowed_elements)) {
1763 $entry->parentNode->removeChild($entry);
1764 }
1765
1766 if ($entry->hasAttributes()) {
1767 $attrs_to_remove = array();
1768
1769 foreach ($entry->attributes as $attr) {
1770
1771 if (strpos($attr->nodeName, 'on') === 0) {
1772 array_push($attrs_to_remove, $attr);
1773 }
1774
1775 if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1776 array_push($attrs_to_remove, $attr);
1777 }
1778
1779 if (in_array($attr->nodeName, $disallowed_attributes)) {
1780 array_push($attrs_to_remove, $attr);
1781 }
1782 }
1783
1784 foreach ($attrs_to_remove as $attr) {
1785 $entry->removeAttributeNode($attr);
1786 }
1787 }
1788 }
1789
1790 return $doc;
1791 }
1792
1793 function trim_array($array) {
1794 $tmp = $array;
1795 array_walk($tmp, 'trim');
1796 return $tmp;
1797 }
1798
1799 function tag_is_valid($tag) {
1800 if (!$tag || is_numeric($tag) || mb_strlen($tag) > 250)
1801 return false;
1802
1803 return true;
1804 }
1805
1806 function render_login_form() {
1807 header('Cache-Control: public');
1808
1809 require_once "login_form.php";
1810 exit;
1811 }
1812
1813 function T_sprintf() {
1814 $args = func_get_args();
1815 return vsprintf(__(array_shift($args)), $args);
1816 }
1817
1818 function print_checkpoint($n, $s) {
1819 $ts = microtime(true);
1820 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1821 return $ts;
1822 }
1823
1824 function sanitize_tag($tag) {
1825 $tag = trim($tag);
1826
1827 $tag = mb_strtolower($tag, 'utf-8');
1828
1829 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1830
1831 if (DB_TYPE == "mysql") {
1832 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1833 }
1834
1835 return $tag;
1836 }
1837
1838 function is_server_https() {
1839 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1840 }
1841
1842 function is_prefix_https() {
1843 return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
1844 }
1845
1846 // this returns SELF_URL_PATH sans ending slash
1847 function get_self_url_prefix() {
1848 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1849 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1850 } else {
1851 return SELF_URL_PATH;
1852 }
1853 }
1854
1855 function encrypt_password($pass, $salt = '', $mode2 = false) {
1856 if ($salt && $mode2) {
1857 return "MODE2:" . hash('sha256', $salt . $pass);
1858 } else if ($salt) {
1859 return "SHA1X:" . sha1("$salt:$pass");
1860 } else {
1861 return "SHA1:" . sha1($pass);
1862 }
1863 } // function encrypt_password
1864
1865 function load_filters($feed_id, $owner_uid) {
1866 $filters = array();
1867
1868 $feed_id = (int) $feed_id;
1869 $cat_id = (int)Feeds::getFeedCategory($feed_id);
1870
1871 if ($cat_id == 0)
1872 $null_cat_qpart = "cat_id IS NULL OR";
1873 else
1874 $null_cat_qpart = "";
1875
1876 $pdo = Db::pdo();
1877
1878 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1879 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1880 $sth->execute([$owner_uid]);
1881
1882 $check_cats = array_merge(
1883 Feeds::getParentCategories($cat_id, $owner_uid),
1884 [$cat_id]);
1885
1886 $check_cats_str = join(",", $check_cats);
1887 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1888
1889 while ($line = $sth->fetch()) {
1890 $filter_id = $line["id"];
1891
1892 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1893
1894 $sth2 = $pdo->prepare("SELECT
1895 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1896 FROM ttrss_filters2_rules AS r,
1897 ttrss_filter_types AS t
1898 WHERE
1899 (match_on IS NOT NULL OR
1900 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1901 (feed_id IS NULL OR feed_id = ?))) AND
1902 filter_type = t.id AND filter_id = ?");
1903 $sth2->execute([$feed_id, $filter_id]);
1904
1905 $rules = array();
1906 $actions = array();
1907
1908 while ($rule_line = $sth2->fetch()) {
1909 # print_r($rule_line);
1910
1911 if ($rule_line["match_on"]) {
1912 $match_on = json_decode($rule_line["match_on"], true);
1913
1914 if (in_array("0", $match_on) || in_array($feed_id, $match_on) || count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1915
1916 $rule = array();
1917 $rule["reg_exp"] = $rule_line["reg_exp"];
1918 $rule["type"] = $rule_line["type_name"];
1919 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1920
1921 array_push($rules, $rule);
1922 } else if (!$match_any_rule) {
1923 // this filter contains a rule that doesn't match to this feed/category combination
1924 // thus filter has to be rejected
1925
1926 $rules = [];
1927 break;
1928 }
1929
1930 } else {
1931
1932 $rule = array();
1933 $rule["reg_exp"] = $rule_line["reg_exp"];
1934 $rule["type"] = $rule_line["type_name"];
1935 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1936
1937 array_push($rules, $rule);
1938 }
1939 }
1940
1941 if (count($rules) > 0) {
1942 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1943 FROM ttrss_filters2_actions AS a,
1944 ttrss_filter_actions AS t
1945 WHERE
1946 action_id = t.id AND filter_id = ?");
1947 $sth2->execute([$filter_id]);
1948
1949 while ($action_line = $sth2->fetch()) {
1950 # print_r($action_line);
1951
1952 $action = array();
1953 $action["type"] = $action_line["type_name"];
1954 $action["param"] = $action_line["action_param"];
1955
1956 array_push($actions, $action);
1957 }
1958 }
1959
1960 $filter = array();
1961 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1962 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1963 $filter["rules"] = $rules;
1964 $filter["actions"] = $actions;
1965
1966 if (count($rules) > 0 && count($actions) > 0) {
1967 array_push($filters, $filter);
1968 }
1969 }
1970
1971 return $filters;
1972 }
1973
1974 function get_score_pic($score) {
1975 if ($score > 100) {
1976 return "score_high.png";
1977 } else if ($score > 0) {
1978 return "score_half_high.png";
1979 } else if ($score < -100) {
1980 return "score_low.png";
1981 } else if ($score < 0) {
1982 return "score_half_low.png";
1983 } else {
1984 return "score_neutral.png";
1985 }
1986 }
1987
1988 function init_plugins() {
1989 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
1990
1991 return true;
1992 }
1993
1994 function add_feed_category($feed_cat, $parent_cat_id = false) {
1995
1996 if (!$feed_cat) return false;
1997
1998 $feed_cat = mb_substr($feed_cat, 0, 250);
1999 if (!$parent_cat_id) $parent_cat_id = null;
2000
2001 $pdo = Db::pdo();
2002 $tr_in_progress = false;
2003
2004 try {
2005 $pdo->beginTransaction();
2006 } catch (Exception $e) {
2007 $tr_in_progress = true;
2008 }
2009
2010 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
2011 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
2012 AND title = :title AND owner_uid = :uid");
2013 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
2014
2015 if (!$sth->fetch()) {
2016
2017 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
2018 VALUES (?, ?, ?)");
2019 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
2020
2021 if (!$tr_in_progress) $pdo->commit();
2022
2023 return true;
2024 }
2025
2026 $pdo->commit();
2027
2028 return false;
2029 }
2030
2031 /**
2032 * Fixes incomplete URLs by prepending "http://".
2033 * Also replaces feed:// with http://, and
2034 * prepends a trailing slash if the url is a domain name only.
2035 *
2036 * @param string $url Possibly incomplete URL
2037 *
2038 * @return string Fixed URL.
2039 */
2040 function fix_url($url) {
2041
2042 // support schema-less urls
2043 if (strpos($url, '//') === 0) {
2044 $url = 'https:' . $url;
2045 }
2046
2047 if (strpos($url, '://') === false) {
2048 $url = 'http://' . $url;
2049 } else if (substr($url, 0, 5) == 'feed:') {
2050 $url = 'http:' . substr($url, 5);
2051 }
2052
2053 //prepend slash if the URL has no slash in it
2054 // "http://www.example" -> "http://www.example/"
2055 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
2056 $url .= '/';
2057 }
2058
2059 //convert IDNA hostname to punycode if possible
2060 if (function_exists("idn_to_ascii")) {
2061 $parts = parse_url($url);
2062 if (mb_detect_encoding($parts['host']) != 'ASCII')
2063 {
2064 $parts['host'] = idn_to_ascii($parts['host']);
2065 $url = build_url($parts);
2066 }
2067 }
2068
2069 if ($url != "http:///")
2070 return $url;
2071 else
2072 return '';
2073 }
2074
2075 function validate_feed_url($url) {
2076 $parts = parse_url($url);
2077
2078 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
2079
2080 }
2081
2082 /* function save_email_address($email) {
2083 // FIXME: implement persistent storage of emails
2084
2085 if (!$_SESSION['stored_emails'])
2086 $_SESSION['stored_emails'] = array();
2087
2088 if (!in_array($email, $_SESSION['stored_emails']))
2089 array_push($_SESSION['stored_emails'], $email);
2090 } */
2091
2092
2093 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2094
2095 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2096
2097 $is_cat = bool_to_sql_bool($is_cat);
2098
2099 $pdo = Db::pdo();
2100
2101 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2102 WHERE feed_id = ? AND is_cat = ?
2103 AND owner_uid = ?");
2104 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2105
2106 if ($row = $sth->fetch()) {
2107 return $row["access_key"];
2108 } else {
2109 $key = uniqid_short();
2110
2111 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2112 (access_key, feed_id, is_cat, owner_uid)
2113 VALUES (?, ?, ?, ?)");
2114
2115 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2116
2117 return $key;
2118 }
2119 }
2120
2121 function get_feeds_from_html($url, $content)
2122 {
2123 $url = fix_url($url);
2124 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
2125
2126 libxml_use_internal_errors(true);
2127
2128 $doc = new DOMDocument();
2129 $doc->loadHTML($content);
2130 $xpath = new DOMXPath($doc);
2131 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2132 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2133 $feedUrls = array();
2134 foreach ($entries as $entry) {
2135 if ($entry->hasAttribute('href')) {
2136 $title = $entry->getAttribute('title');
2137 if ($title == '') {
2138 $title = $entry->getAttribute('type');
2139 }
2140 $feedUrl = rewrite_relative_url(
2141 $baseUrl, $entry->getAttribute('href')
2142 );
2143 $feedUrls[$feedUrl] = $title;
2144 }
2145 }
2146 return $feedUrls;
2147 }
2148
2149 function is_html($content) {
2150 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2151 }
2152
2153 function url_is_html($url, $login = false, $pass = false) {
2154 return is_html(fetch_file_contents($url, false, $login, $pass));
2155 }
2156
2157 function build_url($parts) {
2158 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2159 }
2160
2161 function cleanup_url_path($path) {
2162 $path = str_replace("/./", "/", $path);
2163 $path = str_replace("//", "/", $path);
2164
2165 return $path;
2166 }
2167
2168 /**
2169 * Converts a (possibly) relative URL to a absolute one.
2170 *
2171 * @param string $url Base URL (i.e. from where the document is)
2172 * @param string $rel_url Possibly relative URL in the document
2173 *
2174 * @return string Absolute URL
2175 */
2176 function rewrite_relative_url($url, $rel_url) {
2177 if (strpos($rel_url, "://") !== false) {
2178 return $rel_url;
2179 } else if (strpos($rel_url, "//") === 0) {
2180 # protocol-relative URL (rare but they exist)
2181 return $rel_url;
2182 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2183 # magnet:, feed:, etc
2184 return $rel_url;
2185 } else if (strpos($rel_url, "/") === 0) {
2186 $parts = parse_url($url);
2187 $parts['path'] = $rel_url;
2188 $parts['path'] = cleanup_url_path($parts['path']);
2189
2190 return build_url($parts);
2191
2192 } else {
2193 $parts = parse_url($url);
2194 if (!isset($parts['path'])) {
2195 $parts['path'] = '/';
2196 }
2197 $dir = $parts['path'];
2198 if (substr($dir, -1) !== '/') {
2199 $dir = dirname($parts['path']);
2200 $dir !== '/' && $dir .= '/';
2201 }
2202 $parts['path'] = $dir . $rel_url;
2203 $parts['path'] = cleanup_url_path($parts['path']);
2204
2205 return build_url($parts);
2206 }
2207 }
2208
2209 function cleanup_tags($days = 14, $limit = 1000) {
2210
2211 $days = (int) $days;
2212
2213 if (DB_TYPE == "pgsql") {
2214 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2215 } else if (DB_TYPE == "mysql") {
2216 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2217 }
2218
2219 $tags_deleted = 0;
2220
2221 $pdo = Db::pdo();
2222
2223 while ($limit > 0) {
2224 $limit_part = 500;
2225
2226 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2227 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2228 WHERE post_int_id = int_id AND $interval_query AND
2229 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2230 $sth->execute([$limit]);
2231
2232 $ids = array();
2233
2234 while ($line = $sth->fetch()) {
2235 array_push($ids, $line['id']);
2236 }
2237
2238 if (count($ids) > 0) {
2239 $ids = join(",", $ids);
2240
2241 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2242 $tags_deleted = $usth->rowCount();
2243 } else {
2244 break;
2245 }
2246
2247 $limit -= $limit_part;
2248 }
2249
2250 return $tags_deleted;
2251 }
2252
2253 function print_user_stylesheet() {
2254 $value = get_pref('USER_STYLESHEET');
2255
2256 if ($value) {
2257 print "<style type=\"text/css\">";
2258 print str_replace("<br/>", "\n", $value);
2259 print "</style>";
2260 }
2261
2262 }
2263
2264 function filter_to_sql($filter, $owner_uid) {
2265 $query = array();
2266
2267 $pdo = Db::pdo();
2268
2269 if (DB_TYPE == "pgsql")
2270 $reg_qpart = "~";
2271 else
2272 $reg_qpart = "REGEXP";
2273
2274 foreach ($filter["rules"] AS $rule) {
2275 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2276 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2277 $rule['reg_exp']) !== FALSE;
2278
2279 if ($regexp_valid) {
2280
2281 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2282
2283 switch ($rule["type"]) {
2284 case "title":
2285 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2286 $rule['reg_exp'] . "')";
2287 break;
2288 case "content":
2289 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2290 $rule['reg_exp'] . "')";
2291 break;
2292 case "both":
2293 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2294 $rule['reg_exp'] . "') OR LOWER(" .
2295 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2296 break;
2297 case "tag":
2298 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2299 $rule['reg_exp'] . "')";
2300 break;
2301 case "link":
2302 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2303 $rule['reg_exp'] . "')";
2304 break;
2305 case "author":
2306 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2307 $rule['reg_exp'] . "')";
2308 break;
2309 }
2310
2311 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2312
2313 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2314 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2315 }
2316
2317 if (isset($rule["cat_id"])) {
2318
2319 if ($rule["cat_id"] > 0) {
2320 $children = Feeds::getChildCategories($rule["cat_id"], $owner_uid);
2321 array_push($children, $rule["cat_id"]);
2322 $children = array_map("intval", $children);
2323
2324 $children = join(",", $children);
2325
2326 $cat_qpart = "cat_id IN ($children)";
2327 } else {
2328 $cat_qpart = "cat_id IS NULL";
2329 }
2330
2331 $qpart .= " AND $cat_qpart";
2332 }
2333
2334 $qpart .= " AND feed_id IS NOT NULL";
2335
2336 array_push($query, "($qpart)");
2337
2338 }
2339 }
2340
2341 if (count($query) > 0) {
2342 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2343 } else {
2344 $fullquery = "(false)";
2345 }
2346
2347 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2348
2349 return $fullquery;
2350 }
2351
2352 if (!function_exists('gzdecode')) {
2353 function gzdecode($string) { // no support for 2nd argument
2354 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2355 base64_encode($string));
2356 }
2357 }
2358
2359 function get_random_bytes($length) {
2360 if (function_exists('openssl_random_pseudo_bytes')) {
2361 return openssl_random_pseudo_bytes($length);
2362 } else {
2363 $output = "";
2364
2365 for ($i = 0; $i < $length; $i++)
2366 $output .= chr(mt_rand(0, 255));
2367
2368 return $output;
2369 }
2370 }
2371
2372 function read_stdin() {
2373 $fp = fopen("php://stdin", "r");
2374
2375 if ($fp) {
2376 $line = trim(fgets($fp));
2377 fclose($fp);
2378 return $line;
2379 }
2380
2381 return null;
2382 }
2383
2384 function implements_interface($class, $interface) {
2385 return in_array($interface, class_implements($class));
2386 }
2387
2388 function get_minified_js($files) {
2389
2390 $rv = '';
2391
2392 foreach ($files as $js) {
2393 if (!isset($_GET['debug'])) {
2394 $cached_file = CACHE_DIR . "/js/".basename($js);
2395
2396 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2397
2398 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2399
2400 if ($header && $contents) {
2401 list($htag, $hversion) = explode(":", $header);
2402
2403 if ($htag == "tt-rss" && $hversion == VERSION) {
2404 $rv .= $contents;
2405 continue;
2406 }
2407 }
2408 }
2409
2410 $minified = JShrink\Minifier::minify(file_get_contents("js/$js"));
2411 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
2412 $rv .= $minified;
2413
2414 } else {
2415 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2416 }
2417 }
2418
2419 return $rv;
2420 }
2421
2422 function calculate_dep_timestamp() {
2423 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2424
2425 $max_ts = -1;
2426
2427 foreach ($files as $file) {
2428 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2429 }
2430
2431 return $max_ts;
2432 }
2433
2434 function T_js_decl($s1, $s2) {
2435 if ($s1 && $s2) {
2436 $s1 = preg_replace("/\n/", "", $s1);
2437 $s2 = preg_replace("/\n/", "", $s2);
2438
2439 $s1 = preg_replace("/\"/", "\\\"", $s1);
2440 $s2 = preg_replace("/\"/", "\\\"", $s2);
2441
2442 return "T_messages[\"$s1\"] = \"$s2\";\n";
2443 }
2444 }
2445
2446 function init_js_translations() {
2447
2448 print 'var T_messages = new Object();
2449
2450 function __(msg) {
2451 if (T_messages[msg]) {
2452 return T_messages[msg];
2453 } else {
2454 return msg;
2455 }
2456 }
2457
2458 function ngettext(msg1, msg2, n) {
2459 return __((parseInt(n) > 1) ? msg2 : msg1);
2460 }';
2461
2462 $l10n = _get_reader();
2463
2464 for ($i = 0; $i < $l10n->total; $i++) {
2465 $orig = $l10n->get_original_string($i);
2466 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2467 $key = explode(chr(0), $orig);
2468 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2469 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2470 } else {
2471 $translation = __($orig);
2472 print T_js_decl($orig, $translation);
2473 }
2474 }
2475 }
2476
2477 function get_theme_path($theme) {
2478 if ($theme == "default.php")
2479 return "css/default.css";
2480
2481 $check = "themes/$theme";
2482 if (file_exists($check)) return $check;
2483
2484 $check = "themes.local/$theme";
2485 if (file_exists($check)) return $check;
2486 }
2487
2488 function theme_valid($theme) {
2489 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2490
2491 if (in_array($theme, $bundled_themes)) return true;
2492
2493 $file = "themes/" . basename($theme);
2494
2495 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2496
2497 if (file_exists($file) && is_readable($file)) {
2498 $fh = fopen($file, "r");
2499
2500 if ($fh) {
2501 $header = fgets($fh);
2502 fclose($fh);
2503
2504 return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
2505 }
2506 }
2507
2508 return false;
2509 }
2510
2511 /**
2512 * @SuppressWarnings(unused)
2513 */
2514 function error_json($code) {
2515 require_once "errors.php";
2516
2517 @$message = $ERRORS[$code];
2518
2519 return json_encode(array("error" =>
2520 array("code" => $code, "message" => $message)));
2521
2522 }
2523
2524 /*function abs_to_rel_path($dir) {
2525 $tmp = str_replace(dirname(__DIR__), "", $dir);
2526
2527 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2528
2529 return $tmp;
2530 }*/
2531
2532 function get_upload_error_message($code) {
2533
2534 $errors = array(
2535 0 => __('There is no error, the file uploaded with success'),
2536 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2537 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2538 3 => __('The uploaded file was only partially uploaded'),
2539 4 => __('No file was uploaded'),
2540 6 => __('Missing a temporary folder'),
2541 7 => __('Failed to write file to disk.'),
2542 8 => __('A PHP extension stopped the file upload.'),
2543 );
2544
2545 return $errors[$code];
2546 }
2547
2548 function base64_img($filename) {
2549 if (file_exists($filename)) {
2550 $ext = pathinfo($filename, PATHINFO_EXTENSION);
2551
2552 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2553 } else {
2554 return "";
2555 }
2556 }
2557
2558 /* this is essentially a wrapper for readfile() which allows plugins to hook
2559 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2560
2561 hook function should return true if request was handled (or at least attempted to)
2562
2563 note that this can be called without user context so the plugin to handle this
2564 should be loaded systemwide in config.php */
2565 function send_local_file($filename) {
2566 if (file_exists($filename)) {
2567 $tmppluginhost = new PluginHost();
2568
2569 $tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
2570 $tmppluginhost->load_data();
2571
2572 foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
2573 if ($plugin->hook_send_local_file($filename)) return true;
2574 }
2575
2576 $mimetype = mime_content_type($filename);
2577 header("Content-type: $mimetype");
2578
2579 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2580 header("Last-Modified: $stamp", true);
2581
2582 return readfile($filename);
2583 } else {
2584 return false;
2585 }
2586 }
2587
2588 function check_mysql_tables() {
2589 $pdo = Db::pdo();
2590
2591 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2592 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2593 $sth->execute([DB_NAME]);
2594
2595 $bad_tables = [];
2596
2597 while ($line = $sth->fetch()) {
2598 array_push($bad_tables, $line);
2599 }
2600
2601 return $bad_tables;
2602 }
2603
2604 function validate_field($string, $allowed, $default = "") {
2605 if (in_array($string, $allowed))
2606 return $string;
2607 else
2608 return $default;
2609 }
2610
2611 function arr_qmarks($arr) {
2612 return str_repeat('?,', count($arr) - 1) . '?';
2613 }