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