]> git.wh0rd.org - tt-rss.git/blob - include/functions.php
a30a3cf4f0e08ccc63b035a5cff46705c80e8e28
[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_combined_mode" => __("Toggle combined mode")),
1168 __("Go to") => array(
1169 "goto_all" => __("All articles"),
1170 "goto_fresh" => __("Fresh"),
1171 "goto_marked" => __("Starred"),
1172 "goto_published" => __("Published"),
1173 "goto_tagcloud" => __("Tag cloud"),
1174 "goto_prefs" => __("Preferences")),
1175 __("Other") => array(
1176 "create_label" => __("Create label"),
1177 "create_filter" => __("Create filter"),
1178 "collapse_sidebar" => __("Un/collapse sidebar"),
1179 "help_dialog" => __("Show help dialog"))
1180 );
1181
1182 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
1183 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1184 }
1185
1186 return $hotkeys;
1187 }
1188
1189 function get_hotkeys_map() {
1190 $hotkeys = array(
1191 // "navigation" => array(
1192 "k" => "next_feed",
1193 "j" => "prev_feed",
1194 "n" => "next_article",
1195 "p" => "prev_article",
1196 "(38)|up" => "prev_article",
1197 "(40)|down" => "next_article",
1198 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1199 // "^(40)|Ctrl-down" => "next_article_noscroll",
1200 "(191)|/" => "search_dialog",
1201 // "article" => array(
1202 "s" => "toggle_mark",
1203 "*s" => "toggle_publ",
1204 "u" => "toggle_unread",
1205 "*t" => "edit_tags",
1206 "o" => "open_in_new_window",
1207 "c p" => "catchup_below",
1208 "c n" => "catchup_above",
1209 "*n" => "article_scroll_down",
1210 "*p" => "article_scroll_up",
1211 "*(38)|Shift+up" => "article_scroll_up",
1212 "*(40)|Shift+down" => "article_scroll_down",
1213 "a *w" => "toggle_widescreen",
1214 "a e" => "toggle_embed_original",
1215 "e" => "email_article",
1216 "a q" => "close_article",
1217 // "article_selection" => array(
1218 "a a" => "select_all",
1219 "a u" => "select_unread",
1220 "a *u" => "select_marked",
1221 "a p" => "select_published",
1222 "a i" => "select_invert",
1223 "a n" => "select_none",
1224 // "feed" => array(
1225 "f r" => "feed_refresh",
1226 "f a" => "feed_unhide_read",
1227 "f s" => "feed_subscribe",
1228 "f e" => "feed_edit",
1229 "f q" => "feed_catchup",
1230 "f x" => "feed_reverse",
1231 "f g" => "feed_toggle_vgroup",
1232 "f *d" => "feed_debug_update",
1233 "f *g" => "feed_debug_viewfeed",
1234 "f *c" => "toggle_combined_mode",
1235 "*q" => "catchup_all",
1236 "x" => "cat_toggle_collapse",
1237 // "goto" => array(
1238 "g a" => "goto_all",
1239 "g f" => "goto_fresh",
1240 "g s" => "goto_marked",
1241 "g p" => "goto_published",
1242 "g t" => "goto_tagcloud",
1243 "g *p" => "goto_prefs",
1244 // "other" => array(
1245 "(9)|Tab" => "select_article_cursor", // tab
1246 "c l" => "create_label",
1247 "c f" => "create_filter",
1248 "c s" => "collapse_sidebar",
1249 "^(191)|Ctrl+/" => "help_dialog",
1250 );
1251
1252 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1253 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1254
1255 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
1256 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1257 }
1258
1259 $prefixes = array();
1260
1261 foreach (array_keys($hotkeys) as $hotkey) {
1262 $pair = explode(" ", $hotkey, 2);
1263
1264 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1265 array_push($prefixes, $pair[0]);
1266 }
1267 }
1268
1269 return array($prefixes, $hotkeys);
1270 }
1271
1272 function check_for_update() {
1273 if (defined("GIT_VERSION_TIMESTAMP")) {
1274 $content = @fetch_file_contents(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1275
1276 if ($content) {
1277 $content = json_decode($content, true);
1278
1279 if ($content && isset($content["changeset"])) {
1280 if ((int)GIT_VERSION_TIMESTAMP < (int)$content["changeset"]["timestamp"] &&
1281 GIT_VERSION_HEAD != $content["changeset"]["id"]) {
1282
1283 return $content["changeset"]["id"];
1284 }
1285 }
1286 }
1287 }
1288
1289 return "";
1290 }
1291
1292 function make_runtime_info($disable_update_check = false) {
1293 $data = array();
1294
1295 $pdo = Db::pdo();
1296
1297 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1298 ttrss_feeds WHERE owner_uid = ?");
1299 $sth->execute([$_SESSION['uid']]);
1300 $row = $sth->fetch();
1301
1302 $max_feed_id = $row['mid'];
1303 $num_feeds = $row['nf'];
1304
1305 $data["max_feed_id"] = (int) $max_feed_id;
1306 $data["num_feeds"] = (int) $num_feeds;
1307
1308 $data['last_article_id'] = Article::getLastArticleId();
1309
1310 $data['dep_ts'] = calculate_dep_timestamp();
1311 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1312
1313 $data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1314
1315 if (CHECK_FOR_UPDATES && !$disable_update_check && $_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
1316 $update_result = @check_for_update();
1317
1318 $data["update_result"] = $update_result;
1319
1320 $_SESSION["last_version_check"] = time();
1321 }
1322
1323 if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
1324
1325 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1326
1327 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1328
1329 $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
1330
1331 if ($stamp) {
1332 $stamp_delta = time() - $stamp;
1333
1334 if ($stamp_delta > 1800) {
1335 $stamp_check = 0;
1336 } else {
1337 $stamp_check = 1;
1338 $_SESSION["daemon_stamp_check"] = time();
1339 }
1340
1341 $data['daemon_stamp_ok'] = $stamp_check;
1342
1343 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1344
1345 $data['daemon_stamp'] = $stamp_fmt;
1346 }
1347 }
1348 }
1349
1350 return $data;
1351 }
1352
1353 function search_to_sql($search, $search_language) {
1354
1355 $keywords = str_getcsv(trim($search), " ");
1356 $query_keywords = array();
1357 $search_words = array();
1358 $search_query_leftover = array();
1359
1360 $pdo = Db::pdo();
1361
1362 if ($search_language)
1363 $search_language = $pdo->quote(mb_strtolower($search_language));
1364 else
1365 $search_language = $pdo->quote("english");
1366
1367 foreach ($keywords as $k) {
1368 if (strpos($k, "-") === 0) {
1369 $k = substr($k, 1);
1370 $not = "NOT";
1371 } else {
1372 $not = "";
1373 }
1374
1375 $commandpair = explode(":", mb_strtolower($k), 2);
1376
1377 switch ($commandpair[0]) {
1378 case "title":
1379 if ($commandpair[1]) {
1380 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1381 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1382 } else {
1383 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1384 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1385 array_push($search_words, $k);
1386 }
1387 break;
1388 case "author":
1389 if ($commandpair[1]) {
1390 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1391 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1392 } else {
1393 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1394 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1395 array_push($search_words, $k);
1396 }
1397 break;
1398 case "note":
1399 if ($commandpair[1]) {
1400 if ($commandpair[1] == "true")
1401 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1402 else if ($commandpair[1] == "false")
1403 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1404 else
1405 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1406 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1407 } else {
1408 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1409 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1410 if (!$not) array_push($search_words, $k);
1411 }
1412 break;
1413 case "star":
1414
1415 if ($commandpair[1]) {
1416 if ($commandpair[1] == "true")
1417 array_push($query_keywords, "($not (marked = true))");
1418 else
1419 array_push($query_keywords, "($not (marked = false))");
1420 } else {
1421 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1422 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1423 if (!$not) array_push($search_words, $k);
1424 }
1425 break;
1426 case "pub":
1427 if ($commandpair[1]) {
1428 if ($commandpair[1] == "true")
1429 array_push($query_keywords, "($not (published = true))");
1430 else
1431 array_push($query_keywords, "($not (published = false))");
1432
1433 } else {
1434 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1435 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1436 if (!$not) array_push($search_words, $k);
1437 }
1438 break;
1439 case "unread":
1440 if ($commandpair[1]) {
1441 if ($commandpair[1] == "true")
1442 array_push($query_keywords, "($not (unread = true))");
1443 else
1444 array_push($query_keywords, "($not (unread = false))");
1445
1446 } else {
1447 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1448 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1449 if (!$not) array_push($search_words, $k);
1450 }
1451 break;
1452 default:
1453 if (strpos($k, "@") === 0) {
1454
1455 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1456 $orig_ts = strtotime(substr($k, 1));
1457 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1458
1459 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1460
1461 array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
1462 } else {
1463
1464 if (DB_TYPE == "pgsql") {
1465 $k = mb_strtolower($k);
1466 array_push($search_query_leftover, $not ? "!$k" : $k);
1467 } else {
1468 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1469 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1470 }
1471
1472 if (!$not) array_push($search_words, $k);
1473 }
1474 }
1475 }
1476
1477 if (count($search_query_leftover) > 0) {
1478 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1479
1480 if (DB_TYPE == "pgsql") {
1481 array_push($query_keywords,
1482 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1483 }
1484
1485 }
1486
1487 $search_query_part = implode("AND", $query_keywords);
1488
1489 return array($search_query_part, $search_words);
1490 }
1491
1492 function iframe_whitelisted($entry) {
1493 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1494
1495 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
1496
1497 if ($src) {
1498 foreach ($whitelist as $w) {
1499 if ($src == $w || $src == "www.$w")
1500 return true;
1501 }
1502 }
1503
1504 return false;
1505 }
1506
1507 // check for locally cached (media) URLs and rewrite to local versions
1508 // this is called separately after sanitize() and plugin render article hooks to allow
1509 // plugins work on original source URLs used before caching
1510
1511 function rewrite_cached_urls($str) {
1512 $charset_hack = '<head>
1513 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1514 </head>';
1515
1516 $res = trim($str); if (!$res) return '';
1517
1518 $doc = new DOMDocument();
1519 $doc->loadHTML($charset_hack . $res);
1520 $xpath = new DOMXPath($doc);
1521
1522 $entries = $xpath->query('(//img[@src]|//video[@poster]|//video/source[@src]|//audio/source[@src])');
1523
1524 $need_saving = false;
1525
1526 foreach ($entries as $entry) {
1527
1528 if ($entry->hasAttribute('src') || $entry->hasAttribute('poster')) {
1529
1530 // should be already absolutized because this is called after sanitize()
1531 $src = $entry->hasAttribute('poster') ? $entry->getAttribute('poster') : $entry->getAttribute('src');
1532 $cached_filename = CACHE_DIR . '/images/' . sha1($src);
1533
1534 if (file_exists($cached_filename)) {
1535
1536 // this is strictly cosmetic
1537 if ($entry->tagName == 'img') {
1538 $suffix = ".png";
1539 } else if ($entry->parentNode && $entry->parentNode->tagName == "video") {
1540 $suffix = ".mp4";
1541 } else if ($entry->parentNode && $entry->parentNode->tagName == "audio") {
1542 $suffix = ".ogg";
1543 } else {
1544 $suffix = "";
1545 }
1546
1547 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1548
1549 if ($entry->hasAttribute('poster'))
1550 $entry->setAttribute('poster', $src);
1551 else
1552 $entry->setAttribute('src', $src);
1553
1554 $need_saving = true;
1555 }
1556 }
1557 }
1558
1559 if ($need_saving) {
1560 $doc->removeChild($doc->firstChild); //remove doctype
1561 $res = $doc->saveHTML();
1562 }
1563
1564 return $res;
1565 }
1566
1567 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1568 if (!$owner) $owner = $_SESSION["uid"];
1569
1570 $res = trim($str); if (!$res) return '';
1571
1572 $charset_hack = '<head>
1573 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1574 </head>';
1575
1576 $res = trim($res); if (!$res) return '';
1577
1578 libxml_use_internal_errors(true);
1579
1580 $doc = new DOMDocument();
1581 $doc->loadHTML($charset_hack . $res);
1582 $xpath = new DOMXPath($doc);
1583
1584 $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
1585
1586 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1587
1588 foreach ($entries as $entry) {
1589
1590 if ($entry->hasAttribute('href')) {
1591 $entry->setAttribute('href',
1592 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1593
1594 $entry->setAttribute('rel', 'noopener noreferrer');
1595 }
1596
1597 if ($entry->hasAttribute('src')) {
1598 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1599
1600 // cache stuff has gone to rewrite_cached_urls()
1601
1602 $entry->setAttribute('src', $src);
1603 }
1604
1605 if ($entry->nodeName == 'img') {
1606 $entry->setAttribute('referrerpolicy', 'no-referrer');
1607
1608 $entry->removeAttribute('width');
1609 $entry->removeAttribute('height');
1610
1611 if ($entry->hasAttribute('src')) {
1612 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
1613
1614 if (is_prefix_https() && !$is_https_url) {
1615
1616 if ($entry->hasAttribute('srcset')) {
1617 $entry->removeAttribute('srcset');
1618 }
1619
1620 if ($entry->hasAttribute('sizes')) {
1621 $entry->removeAttribute('sizes');
1622 }
1623 }
1624 }
1625 }
1626
1627 if ($entry->hasAttribute('src') &&
1628 ($owner && get_pref("STRIP_IMAGES", $owner)) || $force_remove_images || $_SESSION["bw_limit"]) {
1629
1630 $p = $doc->createElement('p');
1631
1632 $a = $doc->createElement('a');
1633 $a->setAttribute('href', $entry->getAttribute('src'));
1634
1635 $a->appendChild(new DOMText($entry->getAttribute('src')));
1636 $a->setAttribute('target', '_blank');
1637 $a->setAttribute('rel', 'noopener noreferrer');
1638
1639 $p->appendChild($a);
1640
1641 if ($entry->nodeName == 'source') {
1642
1643 if ($entry->parentNode && $entry->parentNode->parentNode)
1644 $entry->parentNode->parentNode->replaceChild($p, $entry->parentNode);
1645
1646 } else if ($entry->nodeName == 'img') {
1647
1648 if ($entry->parentNode)
1649 $entry->parentNode->replaceChild($p, $entry);
1650
1651 }
1652 }
1653
1654 if (strtolower($entry->nodeName) == "a") {
1655 $entry->setAttribute("target", "_blank");
1656 $entry->setAttribute("rel", "noopener noreferrer");
1657 }
1658 }
1659
1660 $entries = $xpath->query('//iframe');
1661 foreach ($entries as $entry) {
1662 if (!iframe_whitelisted($entry)) {
1663 $entry->setAttribute('sandbox', 'allow-scripts');
1664 } else {
1665 if (is_prefix_https()) {
1666 $entry->setAttribute("src",
1667 str_replace("http://", "https://",
1668 $entry->getAttribute("src")));
1669 }
1670 }
1671 }
1672
1673 $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
1674 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1675 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1676 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1677 'dt', 'em', 'footer', 'figure', 'figcaption',
1678 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1679 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1680 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1681 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1682 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1683 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1684
1685 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1686
1687 $disallowed_attributes = array('id', 'style', 'class');
1688
1689 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1690 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1691 if (is_array($retval)) {
1692 $doc = $retval[0];
1693 $allowed_elements = $retval[1];
1694 $disallowed_attributes = $retval[2];
1695 } else {
1696 $doc = $retval;
1697 }
1698 }
1699
1700 $doc->removeChild($doc->firstChild); //remove doctype
1701 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1702
1703 if ($highlight_words) {
1704 foreach ($highlight_words as $word) {
1705
1706 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1707
1708 $elements = $xpath->query("//*/text()");
1709
1710 foreach ($elements as $child) {
1711
1712 $fragment = $doc->createDocumentFragment();
1713 $text = $child->textContent;
1714
1715 while (($pos = mb_stripos($text, $word)) !== false) {
1716 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1717 $word = mb_substr($text, $pos, mb_strlen($word));
1718 $highlight = $doc->createElement('span');
1719 $highlight->appendChild(new DomText($word));
1720 $highlight->setAttribute('class', 'highlight');
1721 $fragment->appendChild($highlight);
1722 $text = mb_substr($text, $pos + mb_strlen($word));
1723 }
1724
1725 if (!empty($text)) $fragment->appendChild(new DomText($text));
1726
1727 $child->parentNode->replaceChild($fragment, $child);
1728 }
1729 }
1730 }
1731
1732 $res = $doc->saveHTML();
1733
1734 /* strip everything outside of <body>...</body> */
1735
1736 $res_frag = array();
1737 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1738 return $res_frag[1];
1739 } else {
1740 return $res;
1741 }
1742 }
1743
1744 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1745 $xpath = new DOMXPath($doc);
1746 $entries = $xpath->query('//*');
1747
1748 foreach ($entries as $entry) {
1749 if (!in_array($entry->nodeName, $allowed_elements)) {
1750 $entry->parentNode->removeChild($entry);
1751 }
1752
1753 if ($entry->hasAttributes()) {
1754 $attrs_to_remove = array();
1755
1756 foreach ($entry->attributes as $attr) {
1757
1758 if (strpos($attr->nodeName, 'on') === 0) {
1759 array_push($attrs_to_remove, $attr);
1760 }
1761
1762 if (strpos($attr->nodeName, "data-") === 0) {
1763 array_push($attrs_to_remove, $attr);
1764 }
1765
1766 if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1767 array_push($attrs_to_remove, $attr);
1768 }
1769
1770 if (in_array($attr->nodeName, $disallowed_attributes)) {
1771 array_push($attrs_to_remove, $attr);
1772 }
1773 }
1774
1775 foreach ($attrs_to_remove as $attr) {
1776 $entry->removeAttributeNode($attr);
1777 }
1778 }
1779 }
1780
1781 return $doc;
1782 }
1783
1784 function trim_array($array) {
1785 $tmp = $array;
1786 array_walk($tmp, 'trim');
1787 return $tmp;
1788 }
1789
1790 function tag_is_valid($tag) {
1791 if (!$tag || is_numeric($tag) || mb_strlen($tag) > 250)
1792 return false;
1793
1794 return true;
1795 }
1796
1797 function render_login_form() {
1798 header('Cache-Control: public');
1799
1800 require_once "login_form.php";
1801 exit;
1802 }
1803
1804 function T_sprintf() {
1805 $args = func_get_args();
1806 return vsprintf(__(array_shift($args)), $args);
1807 }
1808
1809 function print_checkpoint($n, $s) {
1810 $ts = microtime(true);
1811 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1812 return $ts;
1813 }
1814
1815 function sanitize_tag($tag) {
1816 $tag = trim($tag);
1817
1818 $tag = mb_strtolower($tag, 'utf-8');
1819
1820 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1821
1822 if (DB_TYPE == "mysql") {
1823 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1824 }
1825
1826 return $tag;
1827 }
1828
1829 function is_server_https() {
1830 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1831 }
1832
1833 function is_prefix_https() {
1834 return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
1835 }
1836
1837 // this returns SELF_URL_PATH sans ending slash
1838 function get_self_url_prefix() {
1839 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1840 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1841 } else {
1842 return SELF_URL_PATH;
1843 }
1844 }
1845
1846 function encrypt_password($pass, $salt = '', $mode2 = false) {
1847 if ($salt && $mode2) {
1848 return "MODE2:" . hash('sha256', $salt . $pass);
1849 } else if ($salt) {
1850 return "SHA1X:" . sha1("$salt:$pass");
1851 } else {
1852 return "SHA1:" . sha1($pass);
1853 }
1854 } // function encrypt_password
1855
1856 function load_filters($feed_id, $owner_uid) {
1857 $filters = array();
1858
1859 $feed_id = (int) $feed_id;
1860 $cat_id = (int)Feeds::getFeedCategory($feed_id);
1861
1862 if ($cat_id == 0)
1863 $null_cat_qpart = "cat_id IS NULL OR";
1864 else
1865 $null_cat_qpart = "";
1866
1867 $pdo = Db::pdo();
1868
1869 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1870 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1871 $sth->execute([$owner_uid]);
1872
1873 $check_cats = array_merge(
1874 Feeds::getParentCategories($cat_id, $owner_uid),
1875 [$cat_id]);
1876
1877 $check_cats_str = join(",", $check_cats);
1878 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1879
1880 while ($line = $sth->fetch()) {
1881 $filter_id = $line["id"];
1882
1883 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1884
1885 $sth2 = $pdo->prepare("SELECT
1886 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1887 FROM ttrss_filters2_rules AS r,
1888 ttrss_filter_types AS t
1889 WHERE
1890 (match_on IS NOT NULL OR
1891 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1892 (feed_id IS NULL OR feed_id = ?))) AND
1893 filter_type = t.id AND filter_id = ?");
1894 $sth2->execute([$feed_id, $filter_id]);
1895
1896 $rules = array();
1897 $actions = array();
1898
1899 while ($rule_line = $sth2->fetch()) {
1900 # print_r($rule_line);
1901
1902 if ($rule_line["match_on"]) {
1903 $match_on = json_decode($rule_line["match_on"], true);
1904
1905 if (in_array("0", $match_on) || in_array($feed_id, $match_on) || count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1906
1907 $rule = array();
1908 $rule["reg_exp"] = $rule_line["reg_exp"];
1909 $rule["type"] = $rule_line["type_name"];
1910 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1911
1912 array_push($rules, $rule);
1913 } else if (!$match_any_rule) {
1914 // this filter contains a rule that doesn't match to this feed/category combination
1915 // thus filter has to be rejected
1916
1917 $rules = [];
1918 break;
1919 }
1920
1921 } else {
1922
1923 $rule = array();
1924 $rule["reg_exp"] = $rule_line["reg_exp"];
1925 $rule["type"] = $rule_line["type_name"];
1926 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1927
1928 array_push($rules, $rule);
1929 }
1930 }
1931
1932 if (count($rules) > 0) {
1933 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1934 FROM ttrss_filters2_actions AS a,
1935 ttrss_filter_actions AS t
1936 WHERE
1937 action_id = t.id AND filter_id = ?");
1938 $sth2->execute([$filter_id]);
1939
1940 while ($action_line = $sth2->fetch()) {
1941 # print_r($action_line);
1942
1943 $action = array();
1944 $action["type"] = $action_line["type_name"];
1945 $action["param"] = $action_line["action_param"];
1946
1947 array_push($actions, $action);
1948 }
1949 }
1950
1951 $filter = array();
1952 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1953 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1954 $filter["rules"] = $rules;
1955 $filter["actions"] = $actions;
1956
1957 if (count($rules) > 0 && count($actions) > 0) {
1958 array_push($filters, $filter);
1959 }
1960 }
1961
1962 return $filters;
1963 }
1964
1965 function get_score_pic($score) {
1966 if ($score > 100) {
1967 return "score_high.png";
1968 } else if ($score > 0) {
1969 return "score_half_high.png";
1970 } else if ($score < -100) {
1971 return "score_low.png";
1972 } else if ($score < 0) {
1973 return "score_half_low.png";
1974 } else {
1975 return "score_neutral.png";
1976 }
1977 }
1978
1979 function init_plugins() {
1980 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
1981
1982 return true;
1983 }
1984
1985 function add_feed_category($feed_cat, $parent_cat_id = false) {
1986
1987 if (!$feed_cat) return false;
1988
1989 $feed_cat = mb_substr($feed_cat, 0, 250);
1990 if (!$parent_cat_id) $parent_cat_id = null;
1991
1992 $pdo = Db::pdo();
1993 $tr_in_progress = false;
1994
1995 try {
1996 $pdo->beginTransaction();
1997 } catch (Exception $e) {
1998 $tr_in_progress = true;
1999 }
2000
2001 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
2002 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
2003 AND title = :title AND owner_uid = :uid");
2004 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
2005
2006 if (!$sth->fetch()) {
2007
2008 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
2009 VALUES (?, ?, ?)");
2010 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
2011
2012 if (!$tr_in_progress) $pdo->commit();
2013
2014 return true;
2015 }
2016
2017 $pdo->commit();
2018
2019 return false;
2020 }
2021
2022 /**
2023 * Fixes incomplete URLs by prepending "http://".
2024 * Also replaces feed:// with http://, and
2025 * prepends a trailing slash if the url is a domain name only.
2026 *
2027 * @param string $url Possibly incomplete URL
2028 *
2029 * @return string Fixed URL.
2030 */
2031 function fix_url($url) {
2032
2033 // support schema-less urls
2034 if (strpos($url, '//') === 0) {
2035 $url = 'https:' . $url;
2036 }
2037
2038 if (strpos($url, '://') === false) {
2039 $url = 'http://' . $url;
2040 } else if (substr($url, 0, 5) == 'feed:') {
2041 $url = 'http:' . substr($url, 5);
2042 }
2043
2044 //prepend slash if the URL has no slash in it
2045 // "http://www.example" -> "http://www.example/"
2046 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
2047 $url .= '/';
2048 }
2049
2050 //convert IDNA hostname to punycode if possible
2051 if (function_exists("idn_to_ascii")) {
2052 $parts = parse_url($url);
2053 if (mb_detect_encoding($parts['host']) != 'ASCII')
2054 {
2055 $parts['host'] = idn_to_ascii($parts['host']);
2056 $url = build_url($parts);
2057 }
2058 }
2059
2060 if ($url != "http:///")
2061 return $url;
2062 else
2063 return '';
2064 }
2065
2066 function validate_feed_url($url) {
2067 $parts = parse_url($url);
2068
2069 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
2070
2071 }
2072
2073 /* function save_email_address($email) {
2074 // FIXME: implement persistent storage of emails
2075
2076 if (!$_SESSION['stored_emails'])
2077 $_SESSION['stored_emails'] = array();
2078
2079 if (!in_array($email, $_SESSION['stored_emails']))
2080 array_push($_SESSION['stored_emails'], $email);
2081 } */
2082
2083
2084 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2085
2086 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2087
2088 $is_cat = bool_to_sql_bool($is_cat);
2089
2090 $pdo = Db::pdo();
2091
2092 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2093 WHERE feed_id = ? AND is_cat = ?
2094 AND owner_uid = ?");
2095 $sth->execute([$feed_id, $is_cat, $owner_uid]);
2096
2097 if ($row = $sth->fetch()) {
2098 return $row["access_key"];
2099 } else {
2100 $key = uniqid_short();
2101
2102 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2103 (access_key, feed_id, is_cat, owner_uid)
2104 VALUES (?, ?, ?, ?)");
2105
2106 $sth->execute([$key, $feed_id, $is_cat, $owner_uid]);
2107
2108 return $key;
2109 }
2110 }
2111
2112 function get_feeds_from_html($url, $content)
2113 {
2114 $url = fix_url($url);
2115 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
2116
2117 libxml_use_internal_errors(true);
2118
2119 $doc = new DOMDocument();
2120 $doc->loadHTML($content);
2121 $xpath = new DOMXPath($doc);
2122 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2123 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2124 $feedUrls = array();
2125 foreach ($entries as $entry) {
2126 if ($entry->hasAttribute('href')) {
2127 $title = $entry->getAttribute('title');
2128 if ($title == '') {
2129 $title = $entry->getAttribute('type');
2130 }
2131 $feedUrl = rewrite_relative_url(
2132 $baseUrl, $entry->getAttribute('href')
2133 );
2134 $feedUrls[$feedUrl] = $title;
2135 }
2136 }
2137 return $feedUrls;
2138 }
2139
2140 function is_html($content) {
2141 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2142 }
2143
2144 function url_is_html($url, $login = false, $pass = false) {
2145 return is_html(fetch_file_contents($url, false, $login, $pass));
2146 }
2147
2148 function build_url($parts) {
2149 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2150 }
2151
2152 function cleanup_url_path($path) {
2153 $path = str_replace("/./", "/", $path);
2154 $path = str_replace("//", "/", $path);
2155
2156 return $path;
2157 }
2158
2159 /**
2160 * Converts a (possibly) relative URL to a absolute one.
2161 *
2162 * @param string $url Base URL (i.e. from where the document is)
2163 * @param string $rel_url Possibly relative URL in the document
2164 *
2165 * @return string Absolute URL
2166 */
2167 function rewrite_relative_url($url, $rel_url) {
2168 if (strpos($rel_url, "://") !== false) {
2169 return $rel_url;
2170 } else if (strpos($rel_url, "//") === 0) {
2171 # protocol-relative URL (rare but they exist)
2172 return $rel_url;
2173 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2174 # magnet:, feed:, etc
2175 return $rel_url;
2176 } else if (strpos($rel_url, "/") === 0) {
2177 $parts = parse_url($url);
2178 $parts['path'] = $rel_url;
2179 $parts['path'] = cleanup_url_path($parts['path']);
2180
2181 return build_url($parts);
2182
2183 } else {
2184 $parts = parse_url($url);
2185 if (!isset($parts['path'])) {
2186 $parts['path'] = '/';
2187 }
2188 $dir = $parts['path'];
2189 if (substr($dir, -1) !== '/') {
2190 $dir = dirname($parts['path']);
2191 $dir !== '/' && $dir .= '/';
2192 }
2193 $parts['path'] = $dir . $rel_url;
2194 $parts['path'] = cleanup_url_path($parts['path']);
2195
2196 return build_url($parts);
2197 }
2198 }
2199
2200 function cleanup_tags($days = 14, $limit = 1000) {
2201
2202 $days = (int) $days;
2203
2204 if (DB_TYPE == "pgsql") {
2205 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2206 } else if (DB_TYPE == "mysql") {
2207 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2208 }
2209
2210 $tags_deleted = 0;
2211
2212 $pdo = Db::pdo();
2213
2214 while ($limit > 0) {
2215 $limit_part = 500;
2216
2217 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2218 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2219 WHERE post_int_id = int_id AND $interval_query AND
2220 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2221 $sth->execute([$limit]);
2222
2223 $ids = array();
2224
2225 while ($line = $sth->fetch()) {
2226 array_push($ids, $line['id']);
2227 }
2228
2229 if (count($ids) > 0) {
2230 $ids = join(",", $ids);
2231
2232 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2233 $tags_deleted = $usth->rowCount();
2234 } else {
2235 break;
2236 }
2237
2238 $limit -= $limit_part;
2239 }
2240
2241 return $tags_deleted;
2242 }
2243
2244 function print_user_stylesheet() {
2245 $value = get_pref('USER_STYLESHEET');
2246
2247 if ($value) {
2248 print "<style type=\"text/css\">";
2249 print str_replace("<br/>", "\n", $value);
2250 print "</style>";
2251 }
2252
2253 }
2254
2255 function filter_to_sql($filter, $owner_uid) {
2256 $query = array();
2257
2258 $pdo = Db::pdo();
2259
2260 if (DB_TYPE == "pgsql")
2261 $reg_qpart = "~";
2262 else
2263 $reg_qpart = "REGEXP";
2264
2265 foreach ($filter["rules"] AS $rule) {
2266 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2267 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2268 $rule['reg_exp']) !== FALSE;
2269
2270 if ($regexp_valid) {
2271
2272 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2273
2274 switch ($rule["type"]) {
2275 case "title":
2276 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2277 $rule['reg_exp'] . "')";
2278 break;
2279 case "content":
2280 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2281 $rule['reg_exp'] . "')";
2282 break;
2283 case "both":
2284 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2285 $rule['reg_exp'] . "') OR LOWER(" .
2286 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2287 break;
2288 case "tag":
2289 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2290 $rule['reg_exp'] . "')";
2291 break;
2292 case "link":
2293 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2294 $rule['reg_exp'] . "')";
2295 break;
2296 case "author":
2297 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2298 $rule['reg_exp'] . "')";
2299 break;
2300 }
2301
2302 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2303
2304 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2305 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2306 }
2307
2308 if (isset($rule["cat_id"])) {
2309
2310 if ($rule["cat_id"] > 0) {
2311 $children = Feeds::getChildCategories($rule["cat_id"], $owner_uid);
2312 array_push($children, $rule["cat_id"]);
2313 $children = array_map("intval", $children);
2314
2315 $children = join(",", $children);
2316
2317 $cat_qpart = "cat_id IN ($children)";
2318 } else {
2319 $cat_qpart = "cat_id IS NULL";
2320 }
2321
2322 $qpart .= " AND $cat_qpart";
2323 }
2324
2325 $qpart .= " AND feed_id IS NOT NULL";
2326
2327 array_push($query, "($qpart)");
2328
2329 }
2330 }
2331
2332 if (count($query) > 0) {
2333 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2334 } else {
2335 $fullquery = "(false)";
2336 }
2337
2338 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2339
2340 return $fullquery;
2341 }
2342
2343 if (!function_exists('gzdecode')) {
2344 function gzdecode($string) { // no support for 2nd argument
2345 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2346 base64_encode($string));
2347 }
2348 }
2349
2350 function get_random_bytes($length) {
2351 if (function_exists('openssl_random_pseudo_bytes')) {
2352 return openssl_random_pseudo_bytes($length);
2353 } else {
2354 $output = "";
2355
2356 for ($i = 0; $i < $length; $i++)
2357 $output .= chr(mt_rand(0, 255));
2358
2359 return $output;
2360 }
2361 }
2362
2363 function read_stdin() {
2364 $fp = fopen("php://stdin", "r");
2365
2366 if ($fp) {
2367 $line = trim(fgets($fp));
2368 fclose($fp);
2369 return $line;
2370 }
2371
2372 return null;
2373 }
2374
2375 function implements_interface($class, $interface) {
2376 return in_array($interface, class_implements($class));
2377 }
2378
2379 function get_minified_js($files) {
2380
2381 $rv = '';
2382
2383 foreach ($files as $js) {
2384 if (!isset($_GET['debug'])) {
2385 $cached_file = CACHE_DIR . "/js/".basename($js);
2386
2387 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2388
2389 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2390
2391 if ($header && $contents) {
2392 list($htag, $hversion) = explode(":", $header);
2393
2394 if ($htag == "tt-rss" && $hversion == VERSION) {
2395 $rv .= $contents;
2396 continue;
2397 }
2398 }
2399 }
2400
2401 $minified = JShrink\Minifier::minify(file_get_contents("js/$js"));
2402 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
2403 $rv .= $minified;
2404
2405 } else {
2406 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2407 }
2408 }
2409
2410 return $rv;
2411 }
2412
2413 function calculate_dep_timestamp() {
2414 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2415
2416 $max_ts = -1;
2417
2418 foreach ($files as $file) {
2419 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2420 }
2421
2422 return $max_ts;
2423 }
2424
2425 function T_js_decl($s1, $s2) {
2426 if ($s1 && $s2) {
2427 $s1 = preg_replace("/\n/", "", $s1);
2428 $s2 = preg_replace("/\n/", "", $s2);
2429
2430 $s1 = preg_replace("/\"/", "\\\"", $s1);
2431 $s2 = preg_replace("/\"/", "\\\"", $s2);
2432
2433 return "T_messages[\"$s1\"] = \"$s2\";\n";
2434 }
2435 }
2436
2437 function init_js_translations() {
2438
2439 print 'var T_messages = new Object();
2440
2441 function __(msg) {
2442 if (T_messages[msg]) {
2443 return T_messages[msg];
2444 } else {
2445 return msg;
2446 }
2447 }
2448
2449 function ngettext(msg1, msg2, n) {
2450 return __((parseInt(n) > 1) ? msg2 : msg1);
2451 }';
2452
2453 $l10n = _get_reader();
2454
2455 for ($i = 0; $i < $l10n->total; $i++) {
2456 $orig = $l10n->get_original_string($i);
2457 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2458 $key = explode(chr(0), $orig);
2459 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2460 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2461 } else {
2462 $translation = __($orig);
2463 print T_js_decl($orig, $translation);
2464 }
2465 }
2466 }
2467
2468 function get_theme_path($theme) {
2469 if ($theme == "default.php")
2470 return "css/default.css";
2471
2472 $check = "themes/$theme";
2473 if (file_exists($check)) return $check;
2474
2475 $check = "themes.local/$theme";
2476 if (file_exists($check)) return $check;
2477 }
2478
2479 function theme_valid($theme) {
2480 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2481
2482 if (in_array($theme, $bundled_themes)) return true;
2483
2484 $file = "themes/" . basename($theme);
2485
2486 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2487
2488 if (file_exists($file) && is_readable($file)) {
2489 $fh = fopen($file, "r");
2490
2491 if ($fh) {
2492 $header = fgets($fh);
2493 fclose($fh);
2494
2495 return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
2496 }
2497 }
2498
2499 return false;
2500 }
2501
2502 /**
2503 * @SuppressWarnings(unused)
2504 */
2505 function error_json($code) {
2506 require_once "errors.php";
2507
2508 @$message = $ERRORS[$code];
2509
2510 return json_encode(array("error" =>
2511 array("code" => $code, "message" => $message)));
2512
2513 }
2514
2515 /*function abs_to_rel_path($dir) {
2516 $tmp = str_replace(dirname(__DIR__), "", $dir);
2517
2518 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2519
2520 return $tmp;
2521 }*/
2522
2523 function get_upload_error_message($code) {
2524
2525 $errors = array(
2526 0 => __('There is no error, the file uploaded with success'),
2527 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2528 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2529 3 => __('The uploaded file was only partially uploaded'),
2530 4 => __('No file was uploaded'),
2531 6 => __('Missing a temporary folder'),
2532 7 => __('Failed to write file to disk.'),
2533 8 => __('A PHP extension stopped the file upload.'),
2534 );
2535
2536 return $errors[$code];
2537 }
2538
2539 function base64_img($filename) {
2540 if (file_exists($filename)) {
2541 $ext = pathinfo($filename, PATHINFO_EXTENSION);
2542
2543 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2544 } else {
2545 return "";
2546 }
2547 }
2548
2549 /* this is essentially a wrapper for readfile() which allows plugins to hook
2550 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2551
2552 hook function should return true if request was handled (or at least attempted to)
2553
2554 note that this can be called without user context so the plugin to handle this
2555 should be loaded systemwide in config.php */
2556 function send_local_file($filename) {
2557 if (file_exists($filename)) {
2558
2559 if (is_writable($filename)) touch($filename);
2560
2561 $tmppluginhost = new PluginHost();
2562
2563 $tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
2564 $tmppluginhost->load_data();
2565
2566 foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
2567 if ($plugin->hook_send_local_file($filename)) return true;
2568 }
2569
2570 $mimetype = mime_content_type($filename);
2571
2572 // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
2573 // video files are detected as octet-stream by mime_content_type()
2574
2575 if ($mimetype == "application/octet-stream")
2576 $mimetype = "video/mp4";
2577
2578 header("Content-type: $mimetype");
2579
2580 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2581 header("Last-Modified: $stamp", true);
2582
2583 return readfile($filename);
2584 } else {
2585 return false;
2586 }
2587 }
2588
2589 function check_mysql_tables() {
2590 $pdo = Db::pdo();
2591
2592 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2593 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2594 $sth->execute([DB_NAME]);
2595
2596 $bad_tables = [];
2597
2598 while ($line = $sth->fetch()) {
2599 array_push($bad_tables, $line);
2600 }
2601
2602 return $bad_tables;
2603 }
2604
2605 function validate_field($string, $allowed, $default = "") {
2606 if (in_array($string, $allowed))
2607 return $string;
2608 else
2609 return $default;
2610 }
2611
2612 function arr_qmarks($arr) {
2613 return str_repeat('?,', count($arr) - 1) . '?';
2614 }