]> git.wh0rd.org - tt-rss.git/blob - include/functions.php
fix session write handler always assuming that database entry exists and failing...
[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 $auth_module = false;
704
705 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_AUTH_USER) as $plugin) {
706
707 $user_id = (int) $plugin->authenticate($login, $password);
708
709 if ($user_id) {
710 $auth_module = strtolower(get_class($plugin));
711 break;
712 }
713 }
714
715 if ($user_id && !$check_only) {
716
717 session_start();
718 session_regenerate_id(true);
719
720 $_SESSION["uid"] = $user_id;
721 $_SESSION["version"] = VERSION_STATIC;
722 $_SESSION["auth_module"] = $auth_module;
723
724 $pdo = DB::pdo();
725 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
726 WHERE id = ?");
727 $sth->execute([$user_id]);
728 $row = $sth->fetch();
729
730 $_SESSION["name"] = $row["login"];
731 $_SESSION["access_level"] = $row["access_level"];
732 $_SESSION["csrf_token"] = uniqid_short();
733
734 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
735 $usth->execute([$user_id]);
736
737 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
738 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
739 $_SESSION["pwd_hash"] = $row["pwd_hash"];
740
741 $_SESSION["last_version_check"] = time();
742
743 initialize_user_prefs($_SESSION["uid"]);
744
745 return true;
746 }
747
748 return false;
749
750 } else {
751
752 $_SESSION["uid"] = 1;
753 $_SESSION["name"] = "admin";
754 $_SESSION["access_level"] = 10;
755
756 $_SESSION["hide_hello"] = true;
757 $_SESSION["hide_logout"] = true;
758
759 $_SESSION["auth_module"] = false;
760
761 if (!$_SESSION["csrf_token"]) {
762 $_SESSION["csrf_token"] = uniqid_short();
763 }
764
765 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
766
767 initialize_user_prefs($_SESSION["uid"]);
768
769 return true;
770 }
771 }
772
773 // this is used for user http parameters unless HTML code is actually needed
774 function clean($param) {
775 if (is_array($param)) {
776 return array_map("strip_tags", $param);
777 } else if (is_string($param)) {
778 return strip_tags($param);
779 } else {
780 return $param;
781 }
782 }
783
784 function make_password($length = 8) {
785
786 $password = "";
787 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
788
789 $i = 0;
790
791 while ($i < $length) {
792 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
793
794 if (!strstr($password, $char)) {
795 $password .= $char;
796 $i++;
797 }
798 }
799 return $password;
800 }
801
802 // this is called after user is created to initialize default feeds, labels
803 // or whatever else
804
805 // user preferences are checked on every login, not here
806
807 function initialize_user($uid) {
808
809 $pdo = DB::pdo();
810
811 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
812 values (?, 'Tiny Tiny RSS: Forum',
813 'http://tt-rss.org/forum/rss.php')");
814 $sth->execute([$uid]);
815 }
816
817 function logout_user() {
818 @session_destroy();
819 if (isset($_COOKIE[session_name()])) {
820 setcookie(session_name(), '', time()-42000, '/');
821 }
822 session_commit();
823 }
824
825 function validate_csrf($csrf_token) {
826 return $csrf_token == $_SESSION['csrf_token'];
827 }
828
829 function load_user_plugins($owner_uid, $pluginhost = false) {
830
831 if (!$pluginhost) $pluginhost = PluginHost::getInstance();
832
833 if ($owner_uid && SCHEMA_VERSION >= 100) {
834 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
835
836 $pluginhost->load($plugins, PluginHost::KIND_USER, $owner_uid);
837
838 if (get_schema_version() > 100) {
839 $pluginhost->load_data();
840 }
841 }
842 }
843
844 function login_sequence() {
845 $pdo = Db::pdo();
846
847 if (SINGLE_USER_MODE) {
848 @session_start();
849 authenticate_user("admin", null);
850 startup_gettext();
851 load_user_plugins($_SESSION["uid"]);
852 } else {
853 if (!validate_session()) $_SESSION["uid"] = false;
854
855 if (!$_SESSION["uid"]) {
856
857 if (AUTH_AUTO_LOGIN && authenticate_user(null, null)) {
858 $_SESSION["ref_schema_version"] = get_schema_version(true);
859 } else {
860 authenticate_user(null, null, true);
861 }
862
863 if (!$_SESSION["uid"]) {
864 logout_user();
865
866 render_login_form();
867 exit;
868 }
869
870 } else {
871 /* bump login timestamp */
872 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
873 $sth->execute([$_SESSION['uid']]);
874
875 $_SESSION["last_login_update"] = time();
876 }
877
878 if ($_SESSION["uid"]) {
879 startup_gettext();
880 load_user_plugins($_SESSION["uid"]);
881
882 /* cleanup ccache */
883
884 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
885 AND
886 (SELECT COUNT(id) FROM ttrss_feeds WHERE
887 ttrss_feeds.id = feed_id) = 0");
888
889 $sth->execute([$_SESSION['uid']]);
890
891 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
892 AND
893 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
894 ttrss_feed_categories.id = feed_id) = 0");
895
896 $sth->execute([$_SESSION['uid']]);
897 }
898
899 }
900 }
901
902 function truncate_string($str, $max_len, $suffix = '&hellip;') {
903 if (mb_strlen($str, "utf-8") > $max_len) {
904 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
905 } else {
906 return $str;
907 }
908 }
909
910 // is not utf8 clean
911 function truncate_middle($str, $max_len, $suffix = '&hellip;') {
912 if (strlen($str) > $max_len) {
913 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
914 } else {
915 return $str;
916 }
917 }
918
919 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
920
921 try {
922 $source_tz = new DateTimeZone($source_tz);
923 } catch (Exception $e) {
924 $source_tz = new DateTimeZone('UTC');
925 }
926
927 try {
928 $dest_tz = new DateTimeZone($dest_tz);
929 } catch (Exception $e) {
930 $dest_tz = new DateTimeZone('UTC');
931 }
932
933 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
934 return $dt->format('U') + $dest_tz->getOffset($dt);
935 }
936
937 function make_local_datetime($timestamp, $long, $owner_uid = false,
938 $no_smart_dt = false, $eta_min = false) {
939
940 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
941 if (!$timestamp) $timestamp = '1970-01-01 0:00';
942
943 global $utc_tz;
944 global $user_tz;
945
946 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
947
948 $timestamp = substr($timestamp, 0, 19);
949
950 # We store date in UTC internally
951 $dt = new DateTime($timestamp, $utc_tz);
952
953 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
954
955 if ($user_tz_string != 'Automatic') {
956
957 try {
958 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
959 } catch (Exception $e) {
960 $user_tz = $utc_tz;
961 }
962
963 $tz_offset = $user_tz->getOffset($dt);
964 } else {
965 $tz_offset = (int) -$_SESSION["clientTzOffset"];
966 }
967
968 $user_timestamp = $dt->format('U') + $tz_offset;
969
970 if (!$no_smart_dt) {
971 return smart_date_time($user_timestamp,
972 $tz_offset, $owner_uid, $eta_min);
973 } else {
974 if ($long)
975 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
976 else
977 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
978
979 return date($format, $user_timestamp);
980 }
981 }
982
983 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
984 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
985
986 if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
987 return T_sprintf("%d min", date("i", time() + $tz_offset - $timestamp));
988 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() + $tz_offset)) {
989 return date("G:i", $timestamp);
990 } else if (date("Y", $timestamp) == date("Y", time() + $tz_offset)) {
991 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
992 return date($format, $timestamp);
993 } else {
994 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
995 return date($format, $timestamp);
996 }
997 }
998
999 function sql_bool_to_bool($s) {
1000 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
1001 }
1002
1003 function bool_to_sql_bool($s) {
1004 return $s ? 1 : 0;
1005 }
1006
1007 // Session caching removed due to causing wrong redirects to upgrade
1008 // script when get_schema_version() is called on an obsolete session
1009 // created on a previous schema version.
1010 function get_schema_version($nocache = false) {
1011 global $schema_version;
1012
1013 $pdo = DB::pdo();
1014
1015 if (!$schema_version && !$nocache) {
1016 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
1017 $version = $row["schema_version"];
1018 $schema_version = $version;
1019 return $version;
1020 } else {
1021 return $schema_version;
1022 }
1023 }
1024
1025 function sanity_check() {
1026 require_once 'errors.php';
1027 global $ERRORS;
1028
1029 $error_code = 0;
1030 $schema_version = get_schema_version(true);
1031
1032 if ($schema_version != SCHEMA_VERSION) {
1033 $error_code = 5;
1034 }
1035
1036 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
1037 }
1038
1039 function file_is_locked($filename) {
1040 if (file_exists(LOCK_DIRECTORY . "/$filename")) {
1041 if (function_exists('flock')) {
1042 $fp = @fopen(LOCK_DIRECTORY . "/$filename", "r");
1043 if ($fp) {
1044 if (flock($fp, LOCK_EX | LOCK_NB)) {
1045 flock($fp, LOCK_UN);
1046 fclose($fp);
1047 return false;
1048 }
1049 fclose($fp);
1050 return true;
1051 } else {
1052 return false;
1053 }
1054 }
1055 return true; // consider the file always locked and skip the test
1056 } else {
1057 return false;
1058 }
1059 }
1060
1061
1062 function make_lockfile($filename) {
1063 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
1064
1065 if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
1066 $stat_h = fstat($fp);
1067 $stat_f = stat(LOCK_DIRECTORY . "/$filename");
1068
1069 if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
1070 if ($stat_h["ino"] != $stat_f["ino"] ||
1071 $stat_h["dev"] != $stat_f["dev"]) {
1072
1073 return false;
1074 }
1075 }
1076
1077 if (function_exists('posix_getpid')) {
1078 fwrite($fp, posix_getpid() . "\n");
1079 }
1080 return $fp;
1081 } else {
1082 return false;
1083 }
1084 }
1085
1086 function make_stampfile($filename) {
1087 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
1088
1089 if (flock($fp, LOCK_EX | LOCK_NB)) {
1090 fwrite($fp, time() . "\n");
1091 flock($fp, LOCK_UN);
1092 fclose($fp);
1093 return true;
1094 } else {
1095 return false;
1096 }
1097 }
1098
1099 function sql_random_function() {
1100 if (DB_TYPE == "mysql") {
1101 return "RAND()";
1102 } else {
1103 return "RANDOM()";
1104 }
1105 }
1106
1107 function getFeedUnread($feed, $is_cat = false) {
1108 return Feeds::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1109 }
1110
1111 function checkbox_to_sql_bool($val) {
1112 return ($val == "on") ? 1 : 0;
1113 }
1114
1115 function uniqid_short() {
1116 return uniqid(base_convert(rand(), 10, 36));
1117 }
1118
1119 function make_init_params() {
1120 $params = array();
1121
1122 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1123 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1124 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1125 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1126
1127 $params[strtolower($param)] = (int) get_pref($param);
1128 }
1129
1130 $params["icons_url"] = ICONS_URL;
1131 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
1132 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1133 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1134 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1135 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1136 $params["is_default_pw"] = Pref_Prefs::isdefaultpassword();
1137 $params["label_base_index"] = (int) LABEL_BASE_INDEX;
1138
1139 $theme = get_pref( "USER_CSS_THEME", false, false);
1140 $params["theme"] = theme_valid("$theme") ? $theme : "";
1141
1142 $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
1143
1144 $params["php_platform"] = PHP_OS;
1145 $params["php_version"] = PHP_VERSION;
1146
1147 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1148
1149 $pdo = Db::pdo();
1150
1151 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1152 ttrss_feeds WHERE owner_uid = ?");
1153 $sth->execute([$_SESSION['uid']]);
1154 $row = $sth->fetch();
1155
1156 $max_feed_id = $row["mid"];
1157 $num_feeds = $row["nf"];
1158
1159 $params["max_feed_id"] = (int) $max_feed_id;
1160 $params["num_feeds"] = (int) $num_feeds;
1161
1162 $params["hotkeys"] = get_hotkeys_map();
1163
1164 $params["csrf_token"] = $_SESSION["csrf_token"];
1165 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1166
1167 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE;
1168
1169 $params["icon_alert"] = base64_img("images/alert.png");
1170 $params["icon_information"] = base64_img("images/information.png");
1171 $params["icon_cross"] = base64_img("images/cross.png");
1172 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1173
1174 $params["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1175
1176 return $params;
1177 }
1178
1179 function get_hotkeys_info() {
1180 $hotkeys = array(
1181 __("Navigation") => array(
1182 "next_feed" => __("Open next feed"),
1183 "prev_feed" => __("Open previous feed"),
1184 "next_article" => __("Open next article"),
1185 "prev_article" => __("Open previous article"),
1186 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1187 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1188 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1189 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1190 "search_dialog" => __("Show search dialog")),
1191 __("Article") => array(
1192 "toggle_mark" => __("Toggle starred"),
1193 "toggle_publ" => __("Toggle published"),
1194 "toggle_unread" => __("Toggle unread"),
1195 "edit_tags" => __("Edit tags"),
1196 "open_in_new_window" => __("Open in new window"),
1197 "catchup_below" => __("Mark below as read"),
1198 "catchup_above" => __("Mark above as read"),
1199 "article_scroll_down" => __("Scroll down"),
1200 "article_scroll_up" => __("Scroll up"),
1201 "select_article_cursor" => __("Select article under cursor"),
1202 "email_article" => __("Email article"),
1203 "close_article" => __("Close/collapse article"),
1204 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1205 "toggle_widescreen" => __("Toggle widescreen mode"),
1206 "toggle_embed_original" => __("Toggle embed original")),
1207 __("Article selection") => array(
1208 "select_all" => __("Select all articles"),
1209 "select_unread" => __("Select unread"),
1210 "select_marked" => __("Select starred"),
1211 "select_published" => __("Select published"),
1212 "select_invert" => __("Invert selection"),
1213 "select_none" => __("Deselect everything")),
1214 __("Feed") => array(
1215 "feed_refresh" => __("Refresh current feed"),
1216 "feed_unhide_read" => __("Un/hide read feeds"),
1217 "feed_subscribe" => __("Subscribe to feed"),
1218 "feed_edit" => __("Edit feed"),
1219 "feed_catchup" => __("Mark as read"),
1220 "feed_reverse" => __("Reverse headlines"),
1221 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1222 "feed_debug_update" => __("Debug feed update"),
1223 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1224 "catchup_all" => __("Mark all feeds as read"),
1225 "cat_toggle_collapse" => __("Un/collapse current category"),
1226 "toggle_combined_mode" => __("Toggle combined mode"),
1227 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1228 __("Go to") => array(
1229 "goto_all" => __("All articles"),
1230 "goto_fresh" => __("Fresh"),
1231 "goto_marked" => __("Starred"),
1232 "goto_published" => __("Published"),
1233 "goto_tagcloud" => __("Tag cloud"),
1234 "goto_prefs" => __("Preferences")),
1235 __("Other") => array(
1236 "create_label" => __("Create label"),
1237 "create_filter" => __("Create filter"),
1238 "collapse_sidebar" => __("Un/collapse sidebar"),
1239 "help_dialog" => __("Show help dialog"))
1240 );
1241
1242 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
1243 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1244 }
1245
1246 return $hotkeys;
1247 }
1248
1249 function get_hotkeys_map() {
1250 $hotkeys = array(
1251 // "navigation" => array(
1252 "k" => "next_feed",
1253 "j" => "prev_feed",
1254 "n" => "next_article",
1255 "p" => "prev_article",
1256 "(38)|up" => "prev_article",
1257 "(40)|down" => "next_article",
1258 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1259 // "^(40)|Ctrl-down" => "next_article_noscroll",
1260 "(191)|/" => "search_dialog",
1261 // "article" => array(
1262 "s" => "toggle_mark",
1263 "*s" => "toggle_publ",
1264 "u" => "toggle_unread",
1265 "*t" => "edit_tags",
1266 "o" => "open_in_new_window",
1267 "c p" => "catchup_below",
1268 "c n" => "catchup_above",
1269 "*n" => "article_scroll_down",
1270 "*p" => "article_scroll_up",
1271 "*(38)|Shift+up" => "article_scroll_up",
1272 "*(40)|Shift+down" => "article_scroll_down",
1273 "a *w" => "toggle_widescreen",
1274 "a e" => "toggle_embed_original",
1275 "e" => "email_article",
1276 "a q" => "close_article",
1277 // "article_selection" => array(
1278 "a a" => "select_all",
1279 "a u" => "select_unread",
1280 "a *u" => "select_marked",
1281 "a p" => "select_published",
1282 "a i" => "select_invert",
1283 "a n" => "select_none",
1284 // "feed" => array(
1285 "f r" => "feed_refresh",
1286 "f a" => "feed_unhide_read",
1287 "f s" => "feed_subscribe",
1288 "f e" => "feed_edit",
1289 "f q" => "feed_catchup",
1290 "f x" => "feed_reverse",
1291 "f g" => "feed_toggle_vgroup",
1292 "f *d" => "feed_debug_update",
1293 "f *g" => "feed_debug_viewfeed",
1294 "f *c" => "toggle_combined_mode",
1295 "f c" => "toggle_cdm_expanded",
1296 "*q" => "catchup_all",
1297 "x" => "cat_toggle_collapse",
1298 // "goto" => array(
1299 "g a" => "goto_all",
1300 "g f" => "goto_fresh",
1301 "g s" => "goto_marked",
1302 "g p" => "goto_published",
1303 "g t" => "goto_tagcloud",
1304 "g *p" => "goto_prefs",
1305 // "other" => array(
1306 "(9)|Tab" => "select_article_cursor", // tab
1307 "c l" => "create_label",
1308 "c f" => "create_filter",
1309 "c s" => "collapse_sidebar",
1310 "^(191)|Ctrl+/" => "help_dialog",
1311 );
1312
1313 if (get_pref('COMBINED_DISPLAY_MODE')) {
1314 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1315 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1316 }
1317
1318 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
1319 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1320 }
1321
1322 $prefixes = array();
1323
1324 foreach (array_keys($hotkeys) as $hotkey) {
1325 $pair = explode(" ", $hotkey, 2);
1326
1327 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1328 array_push($prefixes, $pair[0]);
1329 }
1330 }
1331
1332 return array($prefixes, $hotkeys);
1333 }
1334
1335 function check_for_update() {
1336 if (defined("GIT_VERSION_TIMESTAMP")) {
1337 $content = @fetch_file_contents(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1338
1339 if ($content) {
1340 $content = json_decode($content, true);
1341
1342 if ($content && isset($content["changeset"])) {
1343 if ((int)GIT_VERSION_TIMESTAMP < (int)$content["changeset"]["timestamp"] &&
1344 GIT_VERSION_HEAD != $content["changeset"]["id"]) {
1345
1346 return $content["changeset"]["id"];
1347 }
1348 }
1349 }
1350 }
1351
1352 return "";
1353 }
1354
1355 function make_runtime_info($disable_update_check = false) {
1356 $data = array();
1357
1358 $pdo = Db::pdo();
1359
1360 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1361 ttrss_feeds WHERE owner_uid = ?");
1362 $sth->execute([$_SESSION['uid']]);
1363 $row = $sth->fetch();
1364
1365 $max_feed_id = $row['mid'];
1366 $num_feeds = $row['nf'];
1367
1368 $data["max_feed_id"] = (int) $max_feed_id;
1369 $data["num_feeds"] = (int) $num_feeds;
1370
1371 $data['last_article_id'] = Article::getLastArticleId();
1372 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1373
1374 $data['dep_ts'] = calculate_dep_timestamp();
1375 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1376
1377 $data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1378
1379 if (CHECK_FOR_UPDATES && !$disable_update_check && $_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
1380 $update_result = @check_for_update();
1381
1382 $data["update_result"] = $update_result;
1383
1384 $_SESSION["last_version_check"] = time();
1385 }
1386
1387 if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
1388
1389 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1390
1391 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1392
1393 $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
1394
1395 if ($stamp) {
1396 $stamp_delta = time() - $stamp;
1397
1398 if ($stamp_delta > 1800) {
1399 $stamp_check = 0;
1400 } else {
1401 $stamp_check = 1;
1402 $_SESSION["daemon_stamp_check"] = time();
1403 }
1404
1405 $data['daemon_stamp_ok'] = $stamp_check;
1406
1407 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1408
1409 $data['daemon_stamp'] = $stamp_fmt;
1410 }
1411 }
1412 }
1413
1414 return $data;
1415 }
1416
1417 function search_to_sql($search, $search_language) {
1418
1419 $keywords = str_getcsv(trim($search), " ");
1420 $query_keywords = array();
1421 $search_words = array();
1422 $search_query_leftover = array();
1423
1424 $pdo = Db::pdo();
1425
1426 if ($search_language)
1427 $search_language = $pdo->quote(mb_strtolower($search_language));
1428 else
1429 $search_language = $pdo->quote("english");
1430
1431 foreach ($keywords as $k) {
1432 if (strpos($k, "-") === 0) {
1433 $k = substr($k, 1);
1434 $not = "NOT";
1435 } else {
1436 $not = "";
1437 }
1438
1439 $commandpair = explode(":", mb_strtolower($k), 2);
1440
1441 switch ($commandpair[0]) {
1442 case "title":
1443 if ($commandpair[1]) {
1444 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1445 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1446 } else {
1447 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1448 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1449 array_push($search_words, $k);
1450 }
1451 break;
1452 case "author":
1453 if ($commandpair[1]) {
1454 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1455 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1456 } else {
1457 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1458 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1459 array_push($search_words, $k);
1460 }
1461 break;
1462 case "note":
1463 if ($commandpair[1]) {
1464 if ($commandpair[1] == "true")
1465 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1466 else if ($commandpair[1] == "false")
1467 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1468 else
1469 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1470 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1471 } else {
1472 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1473 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1474 if (!$not) array_push($search_words, $k);
1475 }
1476 break;
1477 case "star":
1478
1479 if ($commandpair[1]) {
1480 if ($commandpair[1] == "true")
1481 array_push($query_keywords, "($not (marked = true))");
1482 else
1483 array_push($query_keywords, "($not (marked = false))");
1484 } else {
1485 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1486 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1487 if (!$not) array_push($search_words, $k);
1488 }
1489 break;
1490 case "pub":
1491 if ($commandpair[1]) {
1492 if ($commandpair[1] == "true")
1493 array_push($query_keywords, "($not (published = true))");
1494 else
1495 array_push($query_keywords, "($not (published = false))");
1496
1497 } else {
1498 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1499 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1500 if (!$not) array_push($search_words, $k);
1501 }
1502 break;
1503 case "unread":
1504 if ($commandpair[1]) {
1505 if ($commandpair[1] == "true")
1506 array_push($query_keywords, "($not (unread = true))");
1507 else
1508 array_push($query_keywords, "($not (unread = false))");
1509
1510 } else {
1511 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1512 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1513 if (!$not) array_push($search_words, $k);
1514 }
1515 break;
1516 default:
1517 if (strpos($k, "@") === 0) {
1518
1519 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1520 $orig_ts = strtotime(substr($k, 1));
1521 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1522
1523 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1524
1525 array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
1526 } else {
1527
1528 if (DB_TYPE == "pgsql") {
1529 $k = mb_strtolower($k);
1530 array_push($search_query_leftover, $not ? "!$k" : $k);
1531 } else {
1532 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1533 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1534 }
1535
1536 if (!$not) array_push($search_words, $k);
1537 }
1538 }
1539 }
1540
1541 if (count($search_query_leftover) > 0) {
1542 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1543
1544 if (DB_TYPE == "pgsql") {
1545 array_push($query_keywords,
1546 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1547 }
1548
1549 }
1550
1551 $search_query_part = implode("AND", $query_keywords);
1552
1553 return array($search_query_part, $search_words);
1554 }
1555
1556 function iframe_whitelisted($entry) {
1557 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1558
1559 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
1560
1561 if ($src) {
1562 foreach ($whitelist as $w) {
1563 if ($src == $w || $src == "www.$w")
1564 return true;
1565 }
1566 }
1567
1568 return false;
1569 }
1570
1571 // check for locally cached (media) URLs and rewrite to local versions
1572 // this is called separately after sanitize() and plugin render article hooks to allow
1573 // plugins work on original source URLs used before caching
1574
1575 function rewrite_cached_urls($str) {
1576 $charset_hack = '<head>
1577 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1578 </head>';
1579
1580 $res = trim($str); if (!$res) return '';
1581
1582 $doc = new DOMDocument();
1583 $doc->loadHTML($charset_hack . $res);
1584 $xpath = new DOMXPath($doc);
1585
1586 $entries = $xpath->query('(//img[@src]|//video[@poster]|//video/source[@src]|//audio/source[@src])');
1587
1588 $need_saving = false;
1589
1590 foreach ($entries as $entry) {
1591
1592 if ($entry->hasAttribute('src') || $entry->hasAttribute('poster')) {
1593
1594 // should be already absolutized because this is called after sanitize()
1595 $src = $entry->hasAttribute('poster') ? $entry->getAttribute('poster') : $entry->getAttribute('src');
1596 $cached_filename = CACHE_DIR . '/images/' . sha1($src);
1597
1598 if (file_exists($cached_filename)) {
1599
1600 // this is strictly cosmetic
1601 if ($entry->tagName == 'img') {
1602 $suffix = ".png";
1603 } else if ($entry->parentNode && $entry->parentNode->tagName == "video") {
1604 $suffix = ".mp4";
1605 } else if ($entry->parentNode && $entry->parentNode->tagName == "audio") {
1606 $suffix = ".ogg";
1607 } else {
1608 $suffix = "";
1609 }
1610
1611 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1612
1613 if ($entry->hasAttribute('poster'))
1614 $entry->setAttribute('poster', $src);
1615 else
1616 $entry->setAttribute('src', $src);
1617
1618 $need_saving = true;
1619 }
1620 }
1621 }
1622
1623 if ($need_saving) {
1624 $doc->removeChild($doc->firstChild); //remove doctype
1625 $res = $doc->saveHTML();
1626 }
1627
1628 return $res;
1629 }
1630
1631 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1632 if (!$owner) $owner = $_SESSION["uid"];
1633
1634 $res = trim($str); if (!$res) return '';
1635
1636 $charset_hack = '<head>
1637 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1638 </head>';
1639
1640 $res = trim($res); if (!$res) return '';
1641
1642 libxml_use_internal_errors(true);
1643
1644 $doc = new DOMDocument();
1645 $doc->loadHTML($charset_hack . $res);
1646 $xpath = new DOMXPath($doc);
1647
1648 $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
1649
1650 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1651
1652 foreach ($entries as $entry) {
1653
1654 if ($entry->hasAttribute('href')) {
1655 $entry->setAttribute('href',
1656 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1657
1658 $entry->setAttribute('rel', 'noopener noreferrer');
1659 }
1660
1661 if ($entry->hasAttribute('src')) {
1662 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1663
1664 // cache stuff has gone to rewrite_cached_urls()
1665
1666 $entry->setAttribute('src', $src);
1667 }
1668
1669 if ($entry->nodeName == 'img') {
1670 $entry->setAttribute('referrerpolicy', 'no-referrer');
1671
1672 $entry->removeAttribute('width');
1673 $entry->removeAttribute('height');
1674
1675 if ($entry->hasAttribute('src')) {
1676 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
1677
1678 if (is_prefix_https() && !$is_https_url) {
1679
1680 if ($entry->hasAttribute('srcset')) {
1681 $entry->removeAttribute('srcset');
1682 }
1683
1684 if ($entry->hasAttribute('sizes')) {
1685 $entry->removeAttribute('sizes');
1686 }
1687 }
1688 }
1689 }
1690
1691 if ($entry->hasAttribute('src') &&
1692 ($owner && get_pref("STRIP_IMAGES", $owner)) || $force_remove_images || $_SESSION["bw_limit"]) {
1693
1694 $p = $doc->createElement('p');
1695
1696 $a = $doc->createElement('a');
1697 $a->setAttribute('href', $entry->getAttribute('src'));
1698
1699 $a->appendChild(new DOMText($entry->getAttribute('src')));
1700 $a->setAttribute('target', '_blank');
1701 $a->setAttribute('rel', 'noopener noreferrer');
1702
1703 $p->appendChild($a);
1704
1705 if ($entry->nodeName == 'source') {
1706
1707 if ($entry->parentNode && $entry->parentNode->parentNode)
1708 $entry->parentNode->parentNode->replaceChild($p, $entry->parentNode);
1709
1710 } else if ($entry->nodeName == 'img') {
1711
1712 if ($entry->parentNode)
1713 $entry->parentNode->replaceChild($p, $entry);
1714
1715 }
1716 }
1717
1718 if (strtolower($entry->nodeName) == "a") {
1719 $entry->setAttribute("target", "_blank");
1720 $entry->setAttribute("rel", "noopener noreferrer");
1721 }
1722 }
1723
1724 $entries = $xpath->query('//iframe');
1725 foreach ($entries as $entry) {
1726 if (!iframe_whitelisted($entry)) {
1727 $entry->setAttribute('sandbox', 'allow-scripts');
1728 } else {
1729 if (is_prefix_https()) {
1730 $entry->setAttribute("src",
1731 str_replace("http://", "https://",
1732 $entry->getAttribute("src")));
1733 }
1734 }
1735 }
1736
1737 $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
1738 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1739 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1740 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1741 'dt', 'em', 'footer', 'figure', 'figcaption',
1742 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1743 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1744 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1745 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1746 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1747 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1748
1749 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1750
1751 $disallowed_attributes = array('id', 'style', 'class');
1752
1753 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1754 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1755 if (is_array($retval)) {
1756 $doc = $retval[0];
1757 $allowed_elements = $retval[1];
1758 $disallowed_attributes = $retval[2];
1759 } else {
1760 $doc = $retval;
1761 }
1762 }
1763
1764 $doc->removeChild($doc->firstChild); //remove doctype
1765 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1766
1767 if ($highlight_words) {
1768 foreach ($highlight_words as $word) {
1769
1770 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1771
1772 $elements = $xpath->query("//*/text()");
1773
1774 foreach ($elements as $child) {
1775
1776 $fragment = $doc->createDocumentFragment();
1777 $text = $child->textContent;
1778
1779 while (($pos = mb_stripos($text, $word)) !== false) {
1780 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1781 $word = mb_substr($text, $pos, mb_strlen($word));
1782 $highlight = $doc->createElement('span');
1783 $highlight->appendChild(new DomText($word));
1784 $highlight->setAttribute('class', 'highlight');
1785 $fragment->appendChild($highlight);
1786 $text = mb_substr($text, $pos + mb_strlen($word));
1787 }
1788
1789 if (!empty($text)) $fragment->appendChild(new DomText($text));
1790
1791 $child->parentNode->replaceChild($fragment, $child);
1792 }
1793 }
1794 }
1795
1796 $res = $doc->saveHTML();
1797
1798 /* strip everything outside of <body>...</body> */
1799
1800 $res_frag = array();
1801 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1802 return $res_frag[1];
1803 } else {
1804 return $res;
1805 }
1806 }
1807
1808 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1809 $xpath = new DOMXPath($doc);
1810 $entries = $xpath->query('//*');
1811
1812 foreach ($entries as $entry) {
1813 if (!in_array($entry->nodeName, $allowed_elements)) {
1814 $entry->parentNode->removeChild($entry);
1815 }
1816
1817 if ($entry->hasAttributes()) {
1818 $attrs_to_remove = array();
1819
1820 foreach ($entry->attributes as $attr) {
1821
1822 if (strpos($attr->nodeName, 'on') === 0) {
1823 array_push($attrs_to_remove, $attr);
1824 }
1825
1826 if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1827 array_push($attrs_to_remove, $attr);
1828 }
1829
1830 if (in_array($attr->nodeName, $disallowed_attributes)) {
1831 array_push($attrs_to_remove, $attr);
1832 }
1833 }
1834
1835 foreach ($attrs_to_remove as $attr) {
1836 $entry->removeAttributeNode($attr);
1837 }
1838 }
1839 }
1840
1841 return $doc;
1842 }
1843
1844 function trim_array($array) {
1845 $tmp = $array;
1846 array_walk($tmp, 'trim');
1847 return $tmp;
1848 }
1849
1850 function tag_is_valid($tag) {
1851 if (!$tag || is_numeric($tag) || mb_strlen($tag) > 250)
1852 return false;
1853
1854 return true;
1855 }
1856
1857 function render_login_form() {
1858 header('Cache-Control: public');
1859
1860 require_once "login_form.php";
1861 exit;
1862 }
1863
1864 function T_sprintf() {
1865 $args = func_get_args();
1866 return vsprintf(__(array_shift($args)), $args);
1867 }
1868
1869 function print_checkpoint($n, $s) {
1870 $ts = microtime(true);
1871 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1872 return $ts;
1873 }
1874
1875 function sanitize_tag($tag) {
1876 $tag = trim($tag);
1877
1878 $tag = mb_strtolower($tag, 'utf-8');
1879
1880 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1881
1882 if (DB_TYPE == "mysql") {
1883 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1884 }
1885
1886 return $tag;
1887 }
1888
1889 function is_server_https() {
1890 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1891 }
1892
1893 function is_prefix_https() {
1894 return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
1895 }
1896
1897 // this returns SELF_URL_PATH sans ending slash
1898 function get_self_url_prefix() {
1899 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1900 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1901 } else {
1902 return SELF_URL_PATH;
1903 }
1904 }
1905
1906 function encrypt_password($pass, $salt = '', $mode2 = false) {
1907 if ($salt && $mode2) {
1908 return "MODE2:" . hash('sha256', $salt . $pass);
1909 } else if ($salt) {
1910 return "SHA1X:" . sha1("$salt:$pass");
1911 } else {
1912 return "SHA1:" . sha1($pass);
1913 }
1914 } // function encrypt_password
1915
1916 function load_filters($feed_id, $owner_uid) {
1917 $filters = array();
1918
1919 $feed_id = (int) $feed_id;
1920 $cat_id = (int)Feeds::getFeedCategory($feed_id);
1921
1922 if ($cat_id == 0)
1923 $null_cat_qpart = "cat_id IS NULL OR";
1924 else
1925 $null_cat_qpart = "";
1926
1927 $pdo = Db::pdo();
1928
1929 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1930 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1931 $sth->execute([$owner_uid]);
1932
1933 $check_cats = array_merge(
1934 Feeds::getParentCategories($cat_id, $owner_uid),
1935 [$cat_id]);
1936
1937 $check_cats_str = join(",", $check_cats);
1938 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1939
1940 while ($line = $sth->fetch()) {
1941 $filter_id = $line["id"];
1942
1943 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1944
1945 $sth2 = $pdo->prepare("SELECT
1946 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1947 FROM ttrss_filters2_rules AS r,
1948 ttrss_filter_types AS t
1949 WHERE
1950 (match_on IS NOT NULL OR
1951 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1952 (feed_id IS NULL OR feed_id = ?))) AND
1953 filter_type = t.id AND filter_id = ?");
1954 $sth2->execute([$feed_id, $filter_id]);
1955
1956 $rules = array();
1957 $actions = array();
1958
1959 while ($rule_line = $sth2->fetch()) {
1960 # print_r($rule_line);
1961
1962 if ($rule_line["match_on"]) {
1963 $match_on = json_decode($rule_line["match_on"], true);
1964
1965 if (in_array("0", $match_on) || in_array($feed_id, $match_on) || count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1966
1967 $rule = array();
1968 $rule["reg_exp"] = $rule_line["reg_exp"];
1969 $rule["type"] = $rule_line["type_name"];
1970 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1971
1972 array_push($rules, $rule);
1973 } else if (!$match_any_rule) {
1974 // this filter contains a rule that doesn't match to this feed/category combination
1975 // thus filter has to be rejected
1976
1977 $rules = [];
1978 break;
1979 }
1980
1981 } else {
1982
1983 $rule = array();
1984 $rule["reg_exp"] = $rule_line["reg_exp"];
1985 $rule["type"] = $rule_line["type_name"];
1986 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1987
1988 array_push($rules, $rule);
1989 }
1990 }
1991
1992 if (count($rules) > 0) {
1993 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1994 FROM ttrss_filters2_actions AS a,
1995 ttrss_filter_actions AS t
1996 WHERE
1997 action_id = t.id AND filter_id = ?");
1998 $sth2->execute([$filter_id]);
1999
2000 while ($action_line = $sth2->fetch()) {
2001 # print_r($action_line);
2002
2003 $action = array();
2004 $action["type"] = $action_line["type_name"];
2005 $action["param"] = $action_line["action_param"];
2006
2007 array_push($actions, $action);
2008 }
2009 }
2010
2011 $filter = array();
2012 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
2013 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
2014 $filter["rules"] = $rules;
2015 $filter["actions"] = $actions;
2016
2017 if (count($rules) > 0 && count($actions) > 0) {
2018 array_push($filters, $filter);
2019 }
2020 }
2021
2022 return $filters;
2023 }
2024
2025 function get_score_pic($score) {
2026 if ($score > 100) {
2027 return "score_high.png";
2028 } else if ($score > 0) {
2029 return "score_half_high.png";
2030 } else if ($score < -100) {
2031 return "score_low.png";
2032 } else if ($score < 0) {
2033 return "score_half_low.png";
2034 } else {
2035 return "score_neutral.png";
2036 }
2037 }
2038
2039 function init_plugins() {
2040 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
2041
2042 return true;
2043 }
2044
2045 function add_feed_category($feed_cat, $parent_cat_id = false) {
2046
2047 if (!$feed_cat) return false;
2048
2049 $feed_cat = mb_substr($feed_cat, 0, 250);
2050 if (!$parent_cat_id) $parent_cat_id = null;
2051
2052 $pdo = Db::pdo();
2053 $tr_in_progress = false;
2054
2055 try {
2056 $pdo->beginTransaction();
2057 } catch (Exception $e) {
2058 $tr_in_progress = true;
2059 }
2060
2061 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
2062 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
2063 AND title = :title AND owner_uid = :uid");
2064 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
2065
2066 if (!$sth->fetch()) {
2067
2068 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
2069 VALUES (?, ?, ?)");
2070 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
2071
2072 if (!$tr_in_progress) $pdo->commit();
2073
2074 return true;
2075 }
2076
2077 $pdo->commit();
2078
2079 return false;
2080 }
2081
2082 /**
2083 * Fixes incomplete URLs by prepending "http://".
2084 * Also replaces feed:// with http://, and
2085 * prepends a trailing slash if the url is a domain name only.
2086 *
2087 * @param string $url Possibly incomplete URL
2088 *
2089 * @return string Fixed URL.
2090 */
2091 function fix_url($url) {
2092
2093 // support schema-less urls
2094 if (strpos($url, '//') === 0) {
2095 $url = 'https:' . $url;
2096 }
2097
2098 if (strpos($url, '://') === false) {
2099 $url = 'http://' . $url;
2100 } else if (substr($url, 0, 5) == 'feed:') {
2101 $url = 'http:' . substr($url, 5);
2102 }
2103
2104 //prepend slash if the URL has no slash in it
2105 // "http://www.example" -> "http://www.example/"
2106 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
2107 $url .= '/';
2108 }
2109
2110 //convert IDNA hostname to punycode if possible
2111 if (function_exists("idn_to_ascii")) {
2112 $parts = parse_url($url);
2113 if (mb_detect_encoding($parts['host']) != 'ASCII')
2114 {
2115 $parts['host'] = idn_to_ascii($parts['host']);
2116 $url = build_url($parts);
2117 }
2118 }
2119
2120 if ($url != "http:///")
2121 return $url;
2122 else
2123 return '';
2124 }
2125
2126 function validate_feed_url($url) {
2127 $parts = parse_url($url);
2128
2129 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
2130
2131 }
2132
2133 /* function save_email_address($email) {
2134 // FIXME: implement persistent storage of emails
2135
2136 if (!$_SESSION['stored_emails'])
2137 $_SESSION['stored_emails'] = array();
2138
2139 if (!in_array($email, $_SESSION['stored_emails']))
2140 array_push($_SESSION['stored_emails'], $email);
2141 } */
2142
2143
2144 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2145
2146 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2147
2148 $is_cat = bool_to_sql_bool($is_cat);
2149
2150 $pdo = Db::pdo();
2151
2152 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2153 WHERE feed_id = ? AND is_cat = ?
2154 AND owner_uid = ?");
2155 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2156
2157 if ($row = $sth->fetch()) {
2158 return $row["access_key"];
2159 } else {
2160 $key = uniqid_short();
2161
2162 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2163 (access_key, feed_id, is_cat, owner_uid)
2164 VALUES (?, ?, ?, ?)");
2165
2166 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2167
2168 return $key;
2169 }
2170 }
2171
2172 function get_feeds_from_html($url, $content)
2173 {
2174 $url = fix_url($url);
2175 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
2176
2177 libxml_use_internal_errors(true);
2178
2179 $doc = new DOMDocument();
2180 $doc->loadHTML($content);
2181 $xpath = new DOMXPath($doc);
2182 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2183 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2184 $feedUrls = array();
2185 foreach ($entries as $entry) {
2186 if ($entry->hasAttribute('href')) {
2187 $title = $entry->getAttribute('title');
2188 if ($title == '') {
2189 $title = $entry->getAttribute('type');
2190 }
2191 $feedUrl = rewrite_relative_url(
2192 $baseUrl, $entry->getAttribute('href')
2193 );
2194 $feedUrls[$feedUrl] = $title;
2195 }
2196 }
2197 return $feedUrls;
2198 }
2199
2200 function is_html($content) {
2201 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2202 }
2203
2204 function url_is_html($url, $login = false, $pass = false) {
2205 return is_html(fetch_file_contents($url, false, $login, $pass));
2206 }
2207
2208 function build_url($parts) {
2209 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2210 }
2211
2212 function cleanup_url_path($path) {
2213 $path = str_replace("/./", "/", $path);
2214 $path = str_replace("//", "/", $path);
2215
2216 return $path;
2217 }
2218
2219 /**
2220 * Converts a (possibly) relative URL to a absolute one.
2221 *
2222 * @param string $url Base URL (i.e. from where the document is)
2223 * @param string $rel_url Possibly relative URL in the document
2224 *
2225 * @return string Absolute URL
2226 */
2227 function rewrite_relative_url($url, $rel_url) {
2228 if (strpos($rel_url, "://") !== false) {
2229 return $rel_url;
2230 } else if (strpos($rel_url, "//") === 0) {
2231 # protocol-relative URL (rare but they exist)
2232 return $rel_url;
2233 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2234 # magnet:, feed:, etc
2235 return $rel_url;
2236 } else if (strpos($rel_url, "/") === 0) {
2237 $parts = parse_url($url);
2238 $parts['path'] = $rel_url;
2239 $parts['path'] = cleanup_url_path($parts['path']);
2240
2241 return build_url($parts);
2242
2243 } else {
2244 $parts = parse_url($url);
2245 if (!isset($parts['path'])) {
2246 $parts['path'] = '/';
2247 }
2248 $dir = $parts['path'];
2249 if (substr($dir, -1) !== '/') {
2250 $dir = dirname($parts['path']);
2251 $dir !== '/' && $dir .= '/';
2252 }
2253 $parts['path'] = $dir . $rel_url;
2254 $parts['path'] = cleanup_url_path($parts['path']);
2255
2256 return build_url($parts);
2257 }
2258 }
2259
2260 function cleanup_tags($days = 14, $limit = 1000) {
2261
2262 $days = (int) $days;
2263
2264 if (DB_TYPE == "pgsql") {
2265 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2266 } else if (DB_TYPE == "mysql") {
2267 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2268 }
2269
2270 $tags_deleted = 0;
2271
2272 $pdo = Db::pdo();
2273
2274 while ($limit > 0) {
2275 $limit_part = 500;
2276
2277 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2278 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2279 WHERE post_int_id = int_id AND $interval_query AND
2280 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2281 $sth->execute([$limit]);
2282
2283 $ids = array();
2284
2285 while ($line = $sth->fetch()) {
2286 array_push($ids, $line['id']);
2287 }
2288
2289 if (count($ids) > 0) {
2290 $ids = join(",", $ids);
2291
2292 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2293 $tags_deleted = $usth->rowCount();
2294 } else {
2295 break;
2296 }
2297
2298 $limit -= $limit_part;
2299 }
2300
2301 return $tags_deleted;
2302 }
2303
2304 function print_user_stylesheet() {
2305 $value = get_pref('USER_STYLESHEET');
2306
2307 if ($value) {
2308 print "<style type=\"text/css\">";
2309 print str_replace("<br/>", "\n", $value);
2310 print "</style>";
2311 }
2312
2313 }
2314
2315 function filter_to_sql($filter, $owner_uid) {
2316 $query = array();
2317
2318 $pdo = Db::pdo();
2319
2320 if (DB_TYPE == "pgsql")
2321 $reg_qpart = "~";
2322 else
2323 $reg_qpart = "REGEXP";
2324
2325 foreach ($filter["rules"] AS $rule) {
2326 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2327 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2328 $rule['reg_exp']) !== FALSE;
2329
2330 if ($regexp_valid) {
2331
2332 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2333
2334 switch ($rule["type"]) {
2335 case "title":
2336 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2337 $rule['reg_exp'] . "')";
2338 break;
2339 case "content":
2340 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2341 $rule['reg_exp'] . "')";
2342 break;
2343 case "both":
2344 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2345 $rule['reg_exp'] . "') OR LOWER(" .
2346 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2347 break;
2348 case "tag":
2349 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2350 $rule['reg_exp'] . "')";
2351 break;
2352 case "link":
2353 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2354 $rule['reg_exp'] . "')";
2355 break;
2356 case "author":
2357 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2358 $rule['reg_exp'] . "')";
2359 break;
2360 }
2361
2362 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2363
2364 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2365 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2366 }
2367
2368 if (isset($rule["cat_id"])) {
2369
2370 if ($rule["cat_id"] > 0) {
2371 $children = Feeds::getChildCategories($rule["cat_id"], $owner_uid);
2372 array_push($children, $rule["cat_id"]);
2373 $children = array_map("intval", $children);
2374
2375 $children = join(",", $children);
2376
2377 $cat_qpart = "cat_id IN ($children)";
2378 } else {
2379 $cat_qpart = "cat_id IS NULL";
2380 }
2381
2382 $qpart .= " AND $cat_qpart";
2383 }
2384
2385 $qpart .= " AND feed_id IS NOT NULL";
2386
2387 array_push($query, "($qpart)");
2388
2389 }
2390 }
2391
2392 if (count($query) > 0) {
2393 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2394 } else {
2395 $fullquery = "(false)";
2396 }
2397
2398 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2399
2400 return $fullquery;
2401 }
2402
2403 if (!function_exists('gzdecode')) {
2404 function gzdecode($string) { // no support for 2nd argument
2405 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2406 base64_encode($string));
2407 }
2408 }
2409
2410 function get_random_bytes($length) {
2411 if (function_exists('openssl_random_pseudo_bytes')) {
2412 return openssl_random_pseudo_bytes($length);
2413 } else {
2414 $output = "";
2415
2416 for ($i = 0; $i < $length; $i++)
2417 $output .= chr(mt_rand(0, 255));
2418
2419 return $output;
2420 }
2421 }
2422
2423 function read_stdin() {
2424 $fp = fopen("php://stdin", "r");
2425
2426 if ($fp) {
2427 $line = trim(fgets($fp));
2428 fclose($fp);
2429 return $line;
2430 }
2431
2432 return null;
2433 }
2434
2435 function implements_interface($class, $interface) {
2436 return in_array($interface, class_implements($class));
2437 }
2438
2439 function get_minified_js($files) {
2440
2441 $rv = '';
2442
2443 foreach ($files as $js) {
2444 if (!isset($_GET['debug'])) {
2445 $cached_file = CACHE_DIR . "/js/".basename($js);
2446
2447 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2448
2449 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2450
2451 if ($header && $contents) {
2452 list($htag, $hversion) = explode(":", $header);
2453
2454 if ($htag == "tt-rss" && $hversion == VERSION) {
2455 $rv .= $contents;
2456 continue;
2457 }
2458 }
2459 }
2460
2461 $minified = JShrink\Minifier::minify(file_get_contents("js/$js"));
2462 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
2463 $rv .= $minified;
2464
2465 } else {
2466 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2467 }
2468 }
2469
2470 return $rv;
2471 }
2472
2473 function calculate_dep_timestamp() {
2474 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2475
2476 $max_ts = -1;
2477
2478 foreach ($files as $file) {
2479 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2480 }
2481
2482 return $max_ts;
2483 }
2484
2485 function T_js_decl($s1, $s2) {
2486 if ($s1 && $s2) {
2487 $s1 = preg_replace("/\n/", "", $s1);
2488 $s2 = preg_replace("/\n/", "", $s2);
2489
2490 $s1 = preg_replace("/\"/", "\\\"", $s1);
2491 $s2 = preg_replace("/\"/", "\\\"", $s2);
2492
2493 return "T_messages[\"$s1\"] = \"$s2\";\n";
2494 }
2495 }
2496
2497 function init_js_translations() {
2498
2499 print 'var T_messages = new Object();
2500
2501 function __(msg) {
2502 if (T_messages[msg]) {
2503 return T_messages[msg];
2504 } else {
2505 return msg;
2506 }
2507 }
2508
2509 function ngettext(msg1, msg2, n) {
2510 return __((parseInt(n) > 1) ? msg2 : msg1);
2511 }';
2512
2513 $l10n = _get_reader();
2514
2515 for ($i = 0; $i < $l10n->total; $i++) {
2516 $orig = $l10n->get_original_string($i);
2517 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2518 $key = explode(chr(0), $orig);
2519 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2520 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2521 } else {
2522 $translation = __($orig);
2523 print T_js_decl($orig, $translation);
2524 }
2525 }
2526 }
2527
2528 function get_theme_path($theme) {
2529 if ($theme == "default.php")
2530 return "css/default.css";
2531
2532 $check = "themes/$theme";
2533 if (file_exists($check)) return $check;
2534
2535 $check = "themes.local/$theme";
2536 if (file_exists($check)) return $check;
2537 }
2538
2539 function theme_valid($theme) {
2540 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2541
2542 if (in_array($theme, $bundled_themes)) return true;
2543
2544 $file = "themes/" . basename($theme);
2545
2546 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2547
2548 if (file_exists($file) && is_readable($file)) {
2549 $fh = fopen($file, "r");
2550
2551 if ($fh) {
2552 $header = fgets($fh);
2553 fclose($fh);
2554
2555 return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
2556 }
2557 }
2558
2559 return false;
2560 }
2561
2562 /**
2563 * @SuppressWarnings(unused)
2564 */
2565 function error_json($code) {
2566 require_once "errors.php";
2567
2568 @$message = $ERRORS[$code];
2569
2570 return json_encode(array("error" =>
2571 array("code" => $code, "message" => $message)));
2572
2573 }
2574
2575 /*function abs_to_rel_path($dir) {
2576 $tmp = str_replace(dirname(__DIR__), "", $dir);
2577
2578 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2579
2580 return $tmp;
2581 }*/
2582
2583 function get_upload_error_message($code) {
2584
2585 $errors = array(
2586 0 => __('There is no error, the file uploaded with success'),
2587 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2588 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2589 3 => __('The uploaded file was only partially uploaded'),
2590 4 => __('No file was uploaded'),
2591 6 => __('Missing a temporary folder'),
2592 7 => __('Failed to write file to disk.'),
2593 8 => __('A PHP extension stopped the file upload.'),
2594 );
2595
2596 return $errors[$code];
2597 }
2598
2599 function base64_img($filename) {
2600 if (file_exists($filename)) {
2601 $ext = pathinfo($filename, PATHINFO_EXTENSION);
2602
2603 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2604 } else {
2605 return "";
2606 }
2607 }
2608
2609 /* this is essentially a wrapper for readfile() which allows plugins to hook
2610 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2611
2612 hook function should return true if request was handled (or at least attempted to)
2613
2614 note that this can be called without user context so the plugin to handle this
2615 should be loaded systemwide in config.php */
2616 function send_local_file($filename) {
2617 if (file_exists($filename)) {
2618
2619 if (is_writable($filename)) touch($filename);
2620
2621 $tmppluginhost = new PluginHost();
2622
2623 $tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
2624 $tmppluginhost->load_data();
2625
2626 foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
2627 if ($plugin->hook_send_local_file($filename)) return true;
2628 }
2629
2630 $mimetype = mime_content_type($filename);
2631
2632 // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
2633 // video files are detected as octet-stream by mime_content_type()
2634
2635 if ($mimetype == "application/octet-stream")
2636 $mimetype = "video/mp4";
2637
2638 header("Content-type: $mimetype");
2639
2640 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2641 header("Last-Modified: $stamp", true);
2642
2643 return readfile($filename);
2644 } else {
2645 return false;
2646 }
2647 }
2648
2649 function check_mysql_tables() {
2650 $pdo = Db::pdo();
2651
2652 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2653 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2654 $sth->execute([DB_NAME]);
2655
2656 $bad_tables = [];
2657
2658 while ($line = $sth->fetch()) {
2659 array_push($bad_tables, $line);
2660 }
2661
2662 return $bad_tables;
2663 }
2664
2665 function validate_field($string, $allowed, $default = "") {
2666 if (in_array($string, $allowed))
2667 return $string;
2668 else
2669 return $default;
2670 }
2671
2672 function arr_qmarks($arr) {
2673 return str_repeat('?,', count($arr) - 1) . '?';
2674 }