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