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