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