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