]> git.wh0rd.org - tt-rss.git/blame - include/functions.php
another attempt to enforce session ID regeneration on login
[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 716
f730d7bb
AD
717 /* if a session is started here there's a stale login cookie we need to clean */
718
65e98f40 719 if (session_status() != PHP_SESSION_NONE) {
f730d7bb
AD
720 $_SESSION["login_error_msg"] = __("Stale session cookie found, try logging in again");
721 return false;
65e98f40
AD
722 }
723
65e98f40 724 session_regenerate_id(true);
77aebd7e 725 session_start();
70400171 726
0d421af8 727 $_SESSION["uid"] = $user_id;
3472c4c5 728 $_SESSION["version"] = VERSION_STATIC;
9dadbdbb 729 $_SESSION["auth_module"] = $auth_module;
0d421af8 730
8adb3ec4
AD
731 $pdo = DB::pdo();
732 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
733 WHERE id = ?");
734 $sth->execute([$user_id]);
735 $row = $sth->fetch();
8d505d78 736
8adb3ec4
AD
737 $_SESSION["name"] = $row["login"];
738 $_SESSION["access_level"] = $row["access_level"];
3ceb893f 739 $_SESSION["csrf_token"] = uniqid_short();
8d505d78 740
8adb3ec4
AD
741 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
742 $usth->execute([$user_id]);
8d505d78 743
131b01b3 744 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
837ec70e 745 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
8adb3ec4 746 $_SESSION["pwd_hash"] = $row["pwd_hash"];
91c5f229
AD
747
748 $_SESSION["last_version_check"] = time();
8d505d78 749
a42c55f0 750 initialize_user_prefs($_SESSION["uid"]);
8d505d78 751
131b01b3
AD
752 return true;
753 }
8d505d78 754
131b01b3 755 return false;
503eb349 756
131b01b3 757 } else {
503eb349 758
131b01b3
AD
759 $_SESSION["uid"] = 1;
760 $_SESSION["name"] = "admin";
787e5ebc 761 $_SESSION["access_level"] = 10;
21e42e5f 762
0d421af8
AD
763 $_SESSION["hide_hello"] = true;
764 $_SESSION["hide_logout"] = true;
765
d5fd183d
AD
766 $_SESSION["auth_module"] = false;
767
21e42e5f 768 if (!$_SESSION["csrf_token"]) {
3ceb893f 769 $_SESSION["csrf_token"] = uniqid_short();
21e42e5f 770 }
f557cd78 771
0bbba72d 772 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
8d505d78 773
a42c55f0 774 initialize_user_prefs($_SESSION["uid"]);
8d505d78 775
c8437f35
AD
776 return true;
777 }
c8437f35
AD
778 }
779
e6532439
AD
780 // this is used for user http parameters unless HTML code is actually needed
781 function clean($param) {
782 if (is_array($param)) {
56c22162 783 return array_map("strip_tags", $param);
e6532439
AD
784 } else if (is_string($param)) {
785 return strip_tags($param);
786 } else {
787 return $param;
788 }
789 }
790
e6cb77a0
AD
791 function make_password($length = 8) {
792
85db6213
AD
793 $password = "";
794 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
795
7f4a4045 796 $i = 0;
85db6213
AD
797
798 while ($i < $length) {
799 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
800
801 if (!strstr($password, $char)) {
802 $password .= $char;
803 $i++;
804 }
805 }
806 return $password;
e6cb77a0
AD
807 }
808
809 // this is called after user is created to initialize default feeds, labels
810 // or whatever else
8d505d78 811
e6cb77a0
AD
812 // user preferences are checked on every login, not here
813
a42c55f0 814 function initialize_user($uid) {
e6cb77a0 815
7f4a4045 816 $pdo = DB::pdo();
8adb3ec4
AD
817
818 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
819 values (?, 'Tiny Tiny RSS: Forum',
f0855b88 820 'http://tt-rss.org/forum/rss.php')");
8adb3ec4 821 $sth->execute([$uid]);
3b0feb9b 822 }
e6cb77a0 823
b8aa49bc 824 function logout_user() {
ec5687a6 825 @session_destroy();
5ccc1cf5
AD
826 if (isset($_COOKIE[session_name()])) {
827 setcookie(session_name(), '', time()-42000, '/');
828 }
ec5687a6 829 session_commit();
b8aa49bc
AD
830 }
831
8484ce22
AD
832 function validate_csrf($csrf_token) {
833 return $csrf_token == $_SESSION['csrf_token'];
834 }
835
5cbd1fe8
AD
836 function load_user_plugins($owner_uid, $pluginhost = false) {
837
838 if (!$pluginhost) $pluginhost = PluginHost::getInstance();
839
6d45a152 840 if ($owner_uid && SCHEMA_VERSION >= 100) {
a42c55f0 841 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
de612e7a 842
5cbd1fe8 843 $pluginhost->load($plugins, PluginHost::KIND_USER, $owner_uid);
e9c04fd4 844
6322ac79 845 if (get_schema_version() > 100) {
5cbd1fe8 846 $pluginhost->load_data();
e9c04fd4 847 }
de612e7a
AD
848 }
849 }
850
6322ac79 851 function login_sequence() {
7f4a4045 852 $pdo = Db::pdo();
8adb3ec4 853
97acbaf1 854 if (SINGLE_USER_MODE) {
25db6c51 855 @session_start();
a42c55f0 856 authenticate_user("admin", null);
68349f55 857 startup_gettext();
a42c55f0 858 load_user_plugins($_SESSION["uid"]);
97acbaf1 859 } else {
6322ac79 860 if (!validate_session()) $_SESSION["uid"] = false;
d0eef2a3
AD
861
862 if (!$_SESSION["uid"]) {
97acbaf1 863
a42c55f0 864 if (AUTH_AUTO_LOGIN && authenticate_user(null, null)) {
7f4a4045 865 $_SESSION["ref_schema_version"] = get_schema_version(true);
97acbaf1 866 } else {
a42c55f0 867 authenticate_user(null, null, true);
97acbaf1
AD
868 }
869
d0eef2a3 870 if (!$_SESSION["uid"]) {
ec5687a6 871 logout_user();
9ce7a554 872
6322ac79 873 render_login_form();
d0eef2a3
AD
874 exit;
875 }
4ad99f23 876
97acbaf1
AD
877 } else {
878 /* bump login timestamp */
8adb3ec4
AD
879 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
880 $sth->execute([$_SESSION['uid']]);
881
06b0777f 882 $_SESSION["last_login_update"] = time();
01a87dff
AD
883 }
884
de612e7a 885 if ($_SESSION["uid"]) {
7b149552 886 startup_gettext();
a42c55f0 887 load_user_plugins($_SESSION["uid"]);
b1b1d25f
AD
888
889 /* cleanup ccache */
890
f0dbfedc 891 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
7f4a4045 892 AND
b1b1d25f
AD
893 (SELECT COUNT(id) FROM ttrss_feeds WHERE
894 ttrss_feeds.id = feed_id) = 0");
895
a21f7495 896 $sth->execute([$_SESSION['uid']]);
8adb3ec4 897
f0dbfedc 898 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
7f4a4045 899 AND
b1b1d25f
AD
900 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
901 ttrss_feed_categories.id = feed_id) = 0");
902
7f4a4045 903 $sth->execute([$_SESSION['uid']]);
de612e7a 904 }
b1b1d25f 905
b8aa49bc 906 }
afc3cf55 907 }
3547842a 908
411fe209 909 function truncate_string($str, $max_len, $suffix = '&hellip;') {
878a0083 910 if (mb_strlen($str, "utf-8") > $max_len) {
411fe209 911 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
3547842a
AD
912 } else {
913 return $str;
914 }
915 }
54a60e1a 916
cc43e19b
AD
917 // is not utf8 clean
918 function truncate_middle($str, $max_len, $suffix = '&hellip;') {
919 if (strlen($str) > $max_len) {
920 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
921 } else {
922 return $str;
923 }
924 }
925
ab4b768f
AD
926 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
927
928 try {
929 $source_tz = new DateTimeZone($source_tz);
930 } catch (Exception $e) {
931 $source_tz = new DateTimeZone('UTC');
932 }
933
934 try {
935 $dest_tz = new DateTimeZone($dest_tz);
936 } catch (Exception $e) {
937 $dest_tz = new DateTimeZone('UTC');
938 }
939
940 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
941 return $dt->format('U') + $dest_tz->getOffset($dt);
942 }
943
a42c55f0 944 function make_local_datetime($timestamp, $long, $owner_uid = false,
b6714c77 945 $no_smart_dt = false, $eta_min = false) {
324944f3
AD
946
947 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
948 if (!$timestamp) $timestamp = '1970-01-01 0:00';
949
7d96bfcd 950 global $utc_tz;
5ddef5ba
AD
951 global $user_tz;
952
953 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
324944f3 954
90e5f4f1
AD
955 $timestamp = substr($timestamp, 0, 19);
956
7d96bfcd
AD
957 # We store date in UTC internally
958 $dt = new DateTime($timestamp, $utc_tz);
959
5ddef5ba 960 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
7d96bfcd 961
6bfc97da 962 if ($user_tz_string != 'Automatic') {
324944f3 963
6bfc97da
AD
964 try {
965 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
966 } catch (Exception $e) {
967 $user_tz = $utc_tz;
968 }
969
970 $tz_offset = $user_tz->getOffset($dt);
971 } else {
96f0cbe3 972 $tz_offset = (int) -$_SESSION["clientTzOffset"];
6bfc97da 973 }
5ddef5ba 974
7d96bfcd 975 $user_timestamp = $dt->format('U') + $tz_offset;
324944f3 976
1dc52ae7 977 if (!$no_smart_dt) {
a42c55f0 978 return smart_date_time($user_timestamp,
b6714c77 979 $tz_offset, $owner_uid, $eta_min);
324944f3
AD
980 } else {
981 if ($long)
a42c55f0 982 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
324944f3 983 else
a42c55f0 984 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
324944f3
AD
985
986 return date($format, $user_timestamp);
987 }
988 }
989
b6714c77 990 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
2a5c136e
AD
991 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
992
97aa917c 993 if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
46973af5
AD
994 return T_sprintf("%d min", date("i", time() + $tz_offset - $timestamp));
995 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() + $tz_offset)) {
be773442 996 return date("G:i", $timestamp);
2a5c136e 997 } else if (date("Y", $timestamp) == date("Y", time() + $tz_offset)) {
a42c55f0 998 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
2a5c136e 999 return date($format, $timestamp);
be773442 1000 } else {
a42c55f0 1001 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
2a5c136e 1002 return date($format, $timestamp);
be773442
AD
1003 }
1004 }
1005
e3c99f3b 1006 function sql_bool_to_bool($s) {
0002e598 1007 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
e3c99f3b 1008 }
8d505d78 1009
badac687 1010 function bool_to_sql_bool($s) {
76fc7a2d 1011 return $s ? 1 : 0;
badac687 1012 }
e3c99f3b 1013
fcfa9ef1
AD
1014 // Session caching removed due to causing wrong redirects to upgrade
1015 // script when get_schema_version() is called on an obsolete session
1016 // created on a previous schema version.
a42c55f0 1017 function get_schema_version($nocache = false) {
7d96bfcd
AD
1018 global $schema_version;
1019
8adb3ec4
AD
1020 $pdo = DB::pdo();
1021
e2cf81e2 1022 if (!$schema_version && !$nocache) {
8adb3ec4
AD
1023 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
1024 $version = $row["schema_version"];
7d96bfcd 1025 $schema_version = $version;
199db684 1026 return $version;
7d96bfcd
AD
1027 } else {
1028 return $schema_version;
1029 }
e4c51a6c
AD
1030 }
1031
6322ac79 1032 function sanity_check() {
31303c6b 1033 require_once 'errors.php';
cacc1877 1034 global $ERRORS;
ebb948c2 1035
6043fb7e 1036 $error_code = 0;
a42c55f0 1037 $schema_version = get_schema_version(true);
6043fb7e
AD
1038
1039 if ($schema_version != SCHEMA_VERSION) {
1040 $error_code = 5;
1041 }
1042
ebb948c2 1043 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
6043fb7e
AD
1044 }
1045
27981ca3 1046 function file_is_locked($filename) {
8ff2a86c
AD
1047 if (file_exists(LOCK_DIRECTORY . "/$filename")) {
1048 if (function_exists('flock')) {
1049 $fp = @fopen(LOCK_DIRECTORY . "/$filename", "r");
1050 if ($fp) {
1051 if (flock($fp, LOCK_EX | LOCK_NB)) {
1052 flock($fp, LOCK_UN);
1053 fclose($fp);
1054 return false;
1055 }
31a6d42d 1056 fclose($fp);
8ff2a86c
AD
1057 return true;
1058 } else {
31a6d42d
AD
1059 return false;
1060 }
27981ca3 1061 }
8ff2a86c
AD
1062 return true; // consider the file always locked and skip the test
1063 } else {
1064 return false;
27981ca3 1065 }
27981ca3
AD
1066 }
1067
8ff2a86c 1068
fcb4c0c9 1069 function make_lockfile($filename) {
cfa43e02 1070 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
fcb4c0c9 1071
a44bfcfd 1072 if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
58fc7095
AD
1073 $stat_h = fstat($fp);
1074 $stat_f = stat(LOCK_DIRECTORY . "/$filename");
1075
1fcebfb3
AD
1076 if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
1077 if ($stat_h["ino"] != $stat_f["ino"] ||
1078 $stat_h["dev"] != $stat_f["dev"]) {
1079
1080 return false;
1081 }
58fc7095
AD
1082 }
1083
4c59adb1
AD
1084 if (function_exists('posix_getpid')) {
1085 fwrite($fp, posix_getpid() . "\n");
1086 }
fcb4c0c9
AD
1087 return $fp;
1088 } else {
1089 return false;
1090 }
1091 }
1092
bf7fcde8 1093 function make_stampfile($filename) {
cfa43e02 1094 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
bf7fcde8 1095
8e00ae9b 1096 if (flock($fp, LOCK_EX | LOCK_NB)) {
bf7fcde8 1097 fwrite($fp, time() . "\n");
8e00ae9b 1098 flock($fp, LOCK_UN);
bf7fcde8
AD
1099 fclose($fp);
1100 return true;
1101 } else {
1102 return false;
1103 }
1104 }
1105
894ebcf5 1106 function sql_random_function() {
8c0496f7 1107 if (DB_TYPE == "mysql") {
894ebcf5
AD
1108 return "RAND()";
1109 } else {
1110 return "RANDOM()";
1111 }
1112 }
1113
ff5cc7d7 1114 function getFeedUnread($feed, $is_cat = false) {
86a8351c 1115 return Feeds::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
ff5cc7d7
AD
1116 }
1117
ff5cc7d7 1118 function checkbox_to_sql_bool($val) {
8ff3cbb3 1119 return ($val == "on") ? 1 : 0;
ff5cc7d7
AD
1120 }
1121
3ceb893f
AD
1122 function uniqid_short() {
1123 return uniqid(base_convert(rand(), 10, 36));
1124 }
1125
aeb1abed
AD
1126 function make_init_params() {
1127 $params = array();
1128
1129 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1130 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1131 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1132 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1133
1134 $params[strtolower($param)] = (int) get_pref($param);
1135 }
1136
1137 $params["icons_url"] = ICONS_URL;
1138 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
1139 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1140 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1141 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1142 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
7c0eb1b6 1143 $params["is_default_pw"] = Pref_Prefs::isdefaultpassword();
aeb1abed
AD
1144 $params["label_base_index"] = (int) LABEL_BASE_INDEX;
1145
1146 $theme = get_pref( "USER_CSS_THEME", false, false);
1147 $params["theme"] = theme_valid("$theme") ? $theme : "";
1148
1149 $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
1150
1151 $params["php_platform"] = PHP_OS;
1152 $params["php_version"] = PHP_VERSION;
1153
1154 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1155
8adb3ec4
AD
1156 $pdo = Db::pdo();
1157
1158 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1159 ttrss_feeds WHERE owner_uid = ?");
1160 $sth->execute([$_SESSION['uid']]);
1161 $row = $sth->fetch();
aeb1abed 1162
8adb3ec4
AD
1163 $max_feed_id = $row["mid"];
1164 $num_feeds = $row["nf"];
aeb1abed
AD
1165
1166 $params["max_feed_id"] = (int) $max_feed_id;
1167 $params["num_feeds"] = (int) $num_feeds;
1168
1169 $params["hotkeys"] = get_hotkeys_map();
1170
1171 $params["csrf_token"] = $_SESSION["csrf_token"];
1172 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1173
1174 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE;
1175
1176 $params["icon_alert"] = base64_img("images/alert.png");
1177 $params["icon_information"] = base64_img("images/information.png");
1178 $params["icon_cross"] = base64_img("images/cross.png");
1179 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1180
5e78b0c2
AD
1181 $params["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1182
aeb1abed
AD
1183 return $params;
1184 }
1185
1186 function get_hotkeys_info() {
1187 $hotkeys = array(
1188 __("Navigation") => array(
1189 "next_feed" => __("Open next feed"),
1190 "prev_feed" => __("Open previous feed"),
1191 "next_article" => __("Open next article"),
1192 "prev_article" => __("Open previous article"),
1193 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1194 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1195 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1196 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1197 "search_dialog" => __("Show search dialog")),
1198 __("Article") => array(
1199 "toggle_mark" => __("Toggle starred"),
1200 "toggle_publ" => __("Toggle published"),
1201 "toggle_unread" => __("Toggle unread"),
1202 "edit_tags" => __("Edit tags"),
1203 "open_in_new_window" => __("Open in new window"),
1204 "catchup_below" => __("Mark below as read"),
1205 "catchup_above" => __("Mark above as read"),
1206 "article_scroll_down" => __("Scroll down"),
1207 "article_scroll_up" => __("Scroll up"),
1208 "select_article_cursor" => __("Select article under cursor"),
1209 "email_article" => __("Email article"),
1210 "close_article" => __("Close/collapse article"),
1211 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1212 "toggle_widescreen" => __("Toggle widescreen mode"),
1213 "toggle_embed_original" => __("Toggle embed original")),
1214 __("Article selection") => array(
1215 "select_all" => __("Select all articles"),
1216 "select_unread" => __("Select unread"),
1217 "select_marked" => __("Select starred"),
1218 "select_published" => __("Select published"),
1219 "select_invert" => __("Invert selection"),
1220 "select_none" => __("Deselect everything")),
1221 __("Feed") => array(
1222 "feed_refresh" => __("Refresh current feed"),
1223 "feed_unhide_read" => __("Un/hide read feeds"),
1224 "feed_subscribe" => __("Subscribe to feed"),
1225 "feed_edit" => __("Edit feed"),
1226 "feed_catchup" => __("Mark as read"),
1227 "feed_reverse" => __("Reverse headlines"),
1228 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1229 "feed_debug_update" => __("Debug feed update"),
1230 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1231 "catchup_all" => __("Mark all feeds as read"),
1232 "cat_toggle_collapse" => __("Un/collapse current category"),
1233 "toggle_combined_mode" => __("Toggle combined mode"),
1234 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1235 __("Go to") => array(
1236 "goto_all" => __("All articles"),
1237 "goto_fresh" => __("Fresh"),
1238 "goto_marked" => __("Starred"),
1239 "goto_published" => __("Published"),
1240 "goto_tagcloud" => __("Tag cloud"),
1241 "goto_prefs" => __("Preferences")),
1242 __("Other") => array(
1243 "create_label" => __("Create label"),
1244 "create_filter" => __("Create filter"),
1245 "collapse_sidebar" => __("Un/collapse sidebar"),
1246 "help_dialog" => __("Show help dialog"))
1247 );
1248
1249 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
1250 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1251 }
1252
1253 return $hotkeys;
1254 }
1255
1256 function get_hotkeys_map() {
1257 $hotkeys = array(
1258 // "navigation" => array(
1259 "k" => "next_feed",
1260 "j" => "prev_feed",
1261 "n" => "next_article",
1262 "p" => "prev_article",
1263 "(38)|up" => "prev_article",
1264 "(40)|down" => "next_article",
1265 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1266 // "^(40)|Ctrl-down" => "next_article_noscroll",
1267 "(191)|/" => "search_dialog",
1268 // "article" => array(
1269 "s" => "toggle_mark",
1270 "*s" => "toggle_publ",
1271 "u" => "toggle_unread",
1272 "*t" => "edit_tags",
1273 "o" => "open_in_new_window",
1274 "c p" => "catchup_below",
1275 "c n" => "catchup_above",
1276 "*n" => "article_scroll_down",
1277 "*p" => "article_scroll_up",
1278 "*(38)|Shift+up" => "article_scroll_up",
1279 "*(40)|Shift+down" => "article_scroll_down",
1280 "a *w" => "toggle_widescreen",
1281 "a e" => "toggle_embed_original",
1282 "e" => "email_article",
1283 "a q" => "close_article",
1284 // "article_selection" => array(
1285 "a a" => "select_all",
1286 "a u" => "select_unread",
1287 "a *u" => "select_marked",
1288 "a p" => "select_published",
1289 "a i" => "select_invert",
1290 "a n" => "select_none",
1291 // "feed" => array(
1292 "f r" => "feed_refresh",
1293 "f a" => "feed_unhide_read",
1294 "f s" => "feed_subscribe",
1295 "f e" => "feed_edit",
1296 "f q" => "feed_catchup",
1297 "f x" => "feed_reverse",
1298 "f g" => "feed_toggle_vgroup",
1299 "f *d" => "feed_debug_update",
1300 "f *g" => "feed_debug_viewfeed",
1301 "f *c" => "toggle_combined_mode",
1302 "f c" => "toggle_cdm_expanded",
1303 "*q" => "catchup_all",
1304 "x" => "cat_toggle_collapse",
1305 // "goto" => array(
1306 "g a" => "goto_all",
1307 "g f" => "goto_fresh",
1308 "g s" => "goto_marked",
1309 "g p" => "goto_published",
1310 "g t" => "goto_tagcloud",
1311 "g *p" => "goto_prefs",
1312 // "other" => array(
1313 "(9)|Tab" => "select_article_cursor", // tab
1314 "c l" => "create_label",
1315 "c f" => "create_filter",
1316 "c s" => "collapse_sidebar",
1317 "^(191)|Ctrl+/" => "help_dialog",
1318 );
1319
1320 if (get_pref('COMBINED_DISPLAY_MODE')) {
1321 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1322 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1323 }
1324
1325 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
1326 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1327 }
1328
1329 $prefixes = array();
1330
1331 foreach (array_keys($hotkeys) as $hotkey) {
1332 $pair = explode(" ", $hotkey, 2);
1333
1334 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1335 array_push($prefixes, $pair[0]);
1336 }
1337 }
1338
1339 return array($prefixes, $hotkeys);
1340 }
1341
1342 function check_for_update() {
1343 if (defined("GIT_VERSION_TIMESTAMP")) {
1344 $content = @fetch_file_contents(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1345
1346 if ($content) {
1347 $content = json_decode($content, true);
1348
1349 if ($content && isset($content["changeset"])) {
1350 if ((int)GIT_VERSION_TIMESTAMP < (int)$content["changeset"]["timestamp"] &&
1351 GIT_VERSION_HEAD != $content["changeset"]["id"]) {
1352
1353 return $content["changeset"]["id"];
1354 }
1355 }
1356 }
1357 }
1358
1359 return "";
1360 }
1361
1362 function make_runtime_info($disable_update_check = false) {
1363 $data = array();
1364
8adb3ec4
AD
1365 $pdo = Db::pdo();
1366
1367 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1368 ttrss_feeds WHERE owner_uid = ?");
1369 $sth->execute([$_SESSION['uid']]);
1370 $row = $sth->fetch();
aeb1abed 1371
8adb3ec4
AD
1372 $max_feed_id = $row['mid'];
1373 $num_feeds = $row['nf'];
aeb1abed
AD
1374
1375 $data["max_feed_id"] = (int) $max_feed_id;
1376 $data["num_feeds"] = (int) $num_feeds;
1377
1378 $data['last_article_id'] = Article::getLastArticleId();
1379 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1380
1381 $data['dep_ts'] = calculate_dep_timestamp();
1382 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1383
5e78b0c2 1384 $data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
aeb1abed
AD
1385
1386 if (CHECK_FOR_UPDATES && !$disable_update_check && $_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
1387 $update_result = @check_for_update();
1388
1389 $data["update_result"] = $update_result;
1390
1391 $_SESSION["last_version_check"] = time();
1392 }
1393
1394 if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
1395
1396 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1397
1398 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1399
1400 $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
1401
1402 if ($stamp) {
1403 $stamp_delta = time() - $stamp;
1404
1405 if ($stamp_delta > 1800) {
1406 $stamp_check = 0;
1407 } else {
1408 $stamp_check = 1;
1409 $_SESSION["daemon_stamp_check"] = time();
1410 }
1411
1412 $data['daemon_stamp_ok'] = $stamp_check;
1413
1414 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1415
1416 $data['daemon_stamp'] = $stamp_fmt;
1417 }
1418 }
1419 }
1420
1421 return $data;
1422 }
1423
1424 function search_to_sql($search, $search_language) {
1425
1426 $keywords = str_getcsv(trim($search), " ");
1427 $query_keywords = array();
1428 $search_words = array();
1429 $search_query_leftover = array();
1430
cab58c44 1431 $pdo = Db::pdo();
f0dbfedc 1432
aeb1abed 1433 if ($search_language)
cab58c44 1434 $search_language = $pdo->quote(mb_strtolower($search_language));
aeb1abed 1435 else
9274109c 1436 $search_language = $pdo->quote("english");
aeb1abed
AD
1437
1438 foreach ($keywords as $k) {
1439 if (strpos($k, "-") === 0) {
1440 $k = substr($k, 1);
1441 $not = "NOT";
1442 } else {
1443 $not = "";
1444 }
1445
1446 $commandpair = explode(":", mb_strtolower($k), 2);
1447
1448 switch ($commandpair[0]) {
1449 case "title":
1450 if ($commandpair[1]) {
a2d77092
AD
1451 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1452 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
aeb1abed
AD
1453 } else {
1454 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1e78803c 1455 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1456 array_push($search_words, $k);
1457 }
1458 break;
1459 case "author":
1460 if ($commandpair[1]) {
a2d77092
AD
1461 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1462 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
aeb1abed
AD
1463 } else {
1464 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1e78803c 1465 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1466 array_push($search_words, $k);
1467 }
1468 break;
1469 case "note":
1470 if ($commandpair[1]) {
1471 if ($commandpair[1] == "true")
1472 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1473 else if ($commandpair[1] == "false")
1474 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1475 else
a2d77092
AD
1476 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1477 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
aeb1abed 1478 } else {
1e78803c
AD
1479 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1480 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1481 if (!$not) array_push($search_words, $k);
1482 }
1483 break;
1484 case "star":
1485
1486 if ($commandpair[1]) {
1487 if ($commandpair[1] == "true")
1488 array_push($query_keywords, "($not (marked = true))");
1489 else
1490 array_push($query_keywords, "($not (marked = false))");
1491 } else {
1e78803c
AD
1492 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1493 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1494 if (!$not) array_push($search_words, $k);
1495 }
1496 break;
1497 case "pub":
1498 if ($commandpair[1]) {
1499 if ($commandpair[1] == "true")
1500 array_push($query_keywords, "($not (published = true))");
1501 else
1502 array_push($query_keywords, "($not (published = false))");
1503
1504 } else {
1505 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1e78803c 1506 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1507 if (!$not) array_push($search_words, $k);
1508 }
1509 break;
1510 case "unread":
1511 if ($commandpair[1]) {
1512 if ($commandpair[1] == "true")
1513 array_push($query_keywords, "($not (unread = true))");
1514 else
1515 array_push($query_keywords, "($not (unread = false))");
1516
1517 } else {
1e78803c
AD
1518 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1519 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1520 if (!$not) array_push($search_words, $k);
1521 }
1522 break;
1523 default:
1524 if (strpos($k, "@") === 0) {
1525
1526 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1527 $orig_ts = strtotime(substr($k, 1));
1528 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1529
1530 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1531
1532 array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
1533 } else {
1534
1535 if (DB_TYPE == "pgsql") {
1536 $k = mb_strtolower($k);
1537 array_push($search_query_leftover, $not ? "!$k" : $k);
1538 } else {
1e78803c
AD
1539 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1540 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1541 }
1542
1543 if (!$not) array_push($search_words, $k);
1544 }
1545 }
1546 }
1547
1548 if (count($search_query_leftover) > 0) {
cab58c44 1549 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
aeb1abed
AD
1550
1551 if (DB_TYPE == "pgsql") {
1552 array_push($query_keywords,
1e78803c 1553 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
aeb1abed
AD
1554 }
1555
1556 }
1557
1558 $search_query_part = implode("AND", $query_keywords);
1559
1560 return array($search_query_part, $search_words);
1561 }
1562
1563 function iframe_whitelisted($entry) {
1564 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1565
1566 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
1567
1568 if ($src) {
1569 foreach ($whitelist as $w) {
1570 if ($src == $w || $src == "www.$w")
1571 return true;
1572 }
1573 }
1574
1575 return false;
1576 }
1577
2aef804f
AD
1578 // check for locally cached (media) URLs and rewrite to local versions
1579 // this is called separately after sanitize() and plugin render article hooks to allow
1580 // plugins work on original source URLs used before caching
aeb1abed 1581
2aef804f 1582 function rewrite_cached_urls($str) {
aeb1abed
AD
1583 $charset_hack = '<head>
1584 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1585 </head>';
1586
2aef804f 1587 $res = trim($str); if (!$res) return '';
aeb1abed
AD
1588
1589 $doc = new DOMDocument();
1590 $doc->loadHTML($charset_hack . $res);
1591 $xpath = new DOMXPath($doc);
1592
02bb26a9 1593 $entries = $xpath->query('(//img[@src]|//video[@poster]|//video/source[@src]|//audio/source[@src])');
aeb1abed 1594
2aef804f 1595 $need_saving = false;
aeb1abed
AD
1596
1597 foreach ($entries as $entry) {
1598
02bb26a9 1599 if ($entry->hasAttribute('src') || $entry->hasAttribute('poster')) {
2aef804f
AD
1600
1601 // should be already absolutized because this is called after sanitize()
02bb26a9 1602 $src = $entry->hasAttribute('poster') ? $entry->getAttribute('poster') : $entry->getAttribute('src');
aeb1abed
AD
1603 $cached_filename = CACHE_DIR . '/images/' . sha1($src);
1604
1605 if (file_exists($cached_filename)) {
1606
1607 // this is strictly cosmetic
1608 if ($entry->tagName == 'img') {
1609 $suffix = ".png";
1610 } else if ($entry->parentNode && $entry->parentNode->tagName == "video") {
1611 $suffix = ".mp4";
1612 } else if ($entry->parentNode && $entry->parentNode->tagName == "audio") {
1613 $suffix = ".ogg";
1614 } else {
1615 $suffix = "";
1616 }
1617
1618 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1619
02bb26a9
AD
1620 if ($entry->hasAttribute('poster'))
1621 $entry->setAttribute('poster', $src);
1622 else
1623 $entry->setAttribute('src', $src);
1624
2aef804f 1625 $need_saving = true;
aeb1abed 1626 }
2aef804f
AD
1627 }
1628 }
1629
1630 if ($need_saving) {
1631 $doc->removeChild($doc->firstChild); //remove doctype
1632 $res = $doc->saveHTML();
1633 }
1634
1635 return $res;
1636 }
1637
1638 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1639 if (!$owner) $owner = $_SESSION["uid"];
1640
1641 $res = trim($str); if (!$res) return '';
1642
1643 $charset_hack = '<head>
1644 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1645 </head>';
1646
1647 $res = trim($res); if (!$res) return '';
1648
1649 libxml_use_internal_errors(true);
1650
1651 $doc = new DOMDocument();
1652 $doc->loadHTML($charset_hack . $res);
1653 $xpath = new DOMXPath($doc);
1654
1655 $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
1656
1657 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1658
1659 foreach ($entries as $entry) {
1660
1661 if ($entry->hasAttribute('href')) {
1662 $entry->setAttribute('href',
1663 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1664
1665 $entry->setAttribute('rel', 'noopener noreferrer');
1666 }
1667
1668 if ($entry->hasAttribute('src')) {
1669 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1670
1671 // cache stuff has gone to rewrite_cached_urls()
aeb1abed
AD
1672
1673 $entry->setAttribute('src', $src);
1674 }
1675
1676 if ($entry->nodeName == 'img') {
7651b6e2 1677 $entry->setAttribute('referrerpolicy', 'no-referrer');
aeb1abed 1678
8babb8e7
AD
1679 $entry->removeAttribute('width');
1680 $entry->removeAttribute('height');
1681
aeb1abed
AD
1682 if ($entry->hasAttribute('src')) {
1683 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
1684
9f7bd151 1685 if (is_prefix_https() && !$is_https_url) {
aeb1abed
AD
1686
1687 if ($entry->hasAttribute('srcset')) {
1688 $entry->removeAttribute('srcset');
1689 }
1690
1691 if ($entry->hasAttribute('sizes')) {
1692 $entry->removeAttribute('sizes');
1693 }
1694 }
1695 }
d2e1e60e
AD
1696 }
1697
1698 if ($entry->hasAttribute('src') &&
1699 ($owner && get_pref("STRIP_IMAGES", $owner)) || $force_remove_images || $_SESSION["bw_limit"]) {
1700
1701 $p = $doc->createElement('p');
1702
1703 $a = $doc->createElement('a');
1704 $a->setAttribute('href', $entry->getAttribute('src'));
1705
1706 $a->appendChild(new DOMText($entry->getAttribute('src')));
1707 $a->setAttribute('target', '_blank');
1708 $a->setAttribute('rel', 'noopener noreferrer');
aeb1abed 1709
d2e1e60e 1710 $p->appendChild($a);
aeb1abed 1711
d2e1e60e 1712 if ($entry->nodeName == 'source') {
aeb1abed 1713
d2e1e60e
AD
1714 if ($entry->parentNode && $entry->parentNode->parentNode)
1715 $entry->parentNode->parentNode->replaceChild($p, $entry->parentNode);
aeb1abed 1716
d2e1e60e 1717 } else if ($entry->nodeName == 'img') {
aeb1abed 1718
d2e1e60e
AD
1719 if ($entry->parentNode)
1720 $entry->parentNode->replaceChild($p, $entry);
aeb1abed 1721
aeb1abed
AD
1722 }
1723 }
1724
1725 if (strtolower($entry->nodeName) == "a") {
1726 $entry->setAttribute("target", "_blank");
1727 $entry->setAttribute("rel", "noopener noreferrer");
1728 }
1729 }
1730
1731 $entries = $xpath->query('//iframe');
1732 foreach ($entries as $entry) {
1733 if (!iframe_whitelisted($entry)) {
1734 $entry->setAttribute('sandbox', 'allow-scripts');
1735 } else {
9f7bd151 1736 if (is_prefix_https()) {
aeb1abed
AD
1737 $entry->setAttribute("src",
1738 str_replace("http://", "https://",
1739 $entry->getAttribute("src")));
1740 }
1741 }
1742 }
1743
905ff10d 1744 $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
aeb1abed
AD
1745 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1746 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1747 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1748 'dt', 'em', 'footer', 'figure', 'figcaption',
6eeeec48 1749 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
aeb1abed
AD
1750 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1751 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1752 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1753 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1754 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1755
1756 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1757
1758 $disallowed_attributes = array('id', 'style', 'class');
1759
1760 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1761 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1762 if (is_array($retval)) {
1763 $doc = $retval[0];
1764 $allowed_elements = $retval[1];
1765 $disallowed_attributes = $retval[2];
1766 } else {
1767 $doc = $retval;
1768 }
1769 }
1770
1771 $doc->removeChild($doc->firstChild); //remove doctype
1772 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1773
1774 if ($highlight_words) {
1775 foreach ($highlight_words as $word) {
1776
1777 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1778
1779 $elements = $xpath->query("//*/text()");
1780
1781 foreach ($elements as $child) {
1782
1783 $fragment = $doc->createDocumentFragment();
1784 $text = $child->textContent;
1785
1786 while (($pos = mb_stripos($text, $word)) !== false) {
1787 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1788 $word = mb_substr($text, $pos, mb_strlen($word));
1789 $highlight = $doc->createElement('span');
1790 $highlight->appendChild(new DomText($word));
1791 $highlight->setAttribute('class', 'highlight');
1792 $fragment->appendChild($highlight);
1793 $text = mb_substr($text, $pos + mb_strlen($word));
1794 }
1795
1796 if (!empty($text)) $fragment->appendChild(new DomText($text));
1797
1798 $child->parentNode->replaceChild($fragment, $child);
1799 }
1800 }
1801 }
1802
1803 $res = $doc->saveHTML();
1804
1805 /* strip everything outside of <body>...</body> */
1806
1807 $res_frag = array();
1808 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1809 return $res_frag[1];
1810 } else {
1811 return $res;
1812 }
1813 }
1814
1815 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1816 $xpath = new DOMXPath($doc);
1817 $entries = $xpath->query('//*');
1818
1819 foreach ($entries as $entry) {
1820 if (!in_array($entry->nodeName, $allowed_elements)) {
1821 $entry->parentNode->removeChild($entry);
1822 }
1823
1824 if ($entry->hasAttributes()) {
1825 $attrs_to_remove = array();
1826
1827 foreach ($entry->attributes as $attr) {
1828
1829 if (strpos($attr->nodeName, 'on') === 0) {
1830 array_push($attrs_to_remove, $attr);
1831 }
1832
1833 if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1834 array_push($attrs_to_remove, $attr);
1835 }
1836
1837 if (in_array($attr->nodeName, $disallowed_attributes)) {
1838 array_push($attrs_to_remove, $attr);
1839 }
1840 }
1841
1842 foreach ($attrs_to_remove as $attr) {
1843 $entry->removeAttributeNode($attr);
1844 }
1845 }
1846 }
1847
1848 return $doc;
1849 }
1850
1851 function trim_array($array) {
1852 $tmp = $array;
1853 array_walk($tmp, 'trim');
1854 return $tmp;
1855 }
1856
1857 function tag_is_valid($tag) {
2eaf2a1f
AD
1858 if (!$tag || is_numeric($tag) || mb_strlen($tag) > 250)
1859 return false;
aeb1abed
AD
1860
1861 return true;
1862 }
1863
1864 function render_login_form() {
1865 header('Cache-Control: public');
1866
1867 require_once "login_form.php";
1868 exit;
1869 }
1870
1871 function T_sprintf() {
1872 $args = func_get_args();
1873 return vsprintf(__(array_shift($args)), $args);
1874 }
1875
1876 function print_checkpoint($n, $s) {
1877 $ts = microtime(true);
1878 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1879 return $ts;
1880 }
1881
1882 function sanitize_tag($tag) {
1883 $tag = trim($tag);
1884
1885 $tag = mb_strtolower($tag, 'utf-8');
1886
1887 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1888
1889 if (DB_TYPE == "mysql") {
1890 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1891 }
1892
1893 return $tag;
1894 }
1895
9f7bd151 1896 function is_server_https() {
e234ac8d 1897 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
9f7bd151
AD
1898 }
1899
1900 function is_prefix_https() {
1901 return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
1902 }
1903
b2d42e96 1904 // this returns SELF_URL_PATH sans ending slash
aeb1abed
AD
1905 function get_self_url_prefix() {
1906 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1907 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1908 } else {
1909 return SELF_URL_PATH;
1910 }
1911 }
1912
aeb1abed
AD
1913 function encrypt_password($pass, $salt = '', $mode2 = false) {
1914 if ($salt && $mode2) {
1915 return "MODE2:" . hash('sha256', $salt . $pass);
1916 } else if ($salt) {
1917 return "SHA1X:" . sha1("$salt:$pass");
1918 } else {
1919 return "SHA1:" . sha1($pass);
1920 }
1921 } // function encrypt_password
1922
1923 function load_filters($feed_id, $owner_uid) {
1924 $filters = array();
1925
c949a928 1926 $feed_id = (int) $feed_id;
0086a897 1927 $cat_id = (int)Feeds::getFeedCategory($feed_id);
aeb1abed
AD
1928
1929 if ($cat_id == 0)
1930 $null_cat_qpart = "cat_id IS NULL OR";
1931 else
1932 $null_cat_qpart = "";
1933
8adb3ec4
AD
1934 $pdo = Db::pdo();
1935
1936 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1937 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1938 $sth->execute([$owner_uid]);
aeb1abed 1939
02f3992a 1940 $check_cats = array_merge(
aeb1abed 1941 Feeds::getParentCategories($cat_id, $owner_uid),
02f3992a
AD
1942 [$cat_id]);
1943
1944 $check_cats_str = join(",", $check_cats);
1945 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
aeb1abed 1946
8adb3ec4 1947 while ($line = $sth->fetch()) {
aeb1abed
AD
1948 $filter_id = $line["id"];
1949
7f4a4045 1950 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
02f3992a 1951
8adb3ec4 1952 $sth2 = $pdo->prepare("SELECT
02f3992a 1953 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
aeb1abed
AD
1954 FROM ttrss_filters2_rules AS r,
1955 ttrss_filter_types AS t
1956 WHERE
7f4a4045 1957 (match_on IS NOT NULL OR
02f3992a 1958 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
8adb3ec4
AD
1959 (feed_id IS NULL OR feed_id = ?))) AND
1960 filter_type = t.id AND filter_id = ?");
1961 $sth2->execute([$feed_id, $filter_id]);
aeb1abed
AD
1962
1963 $rules = array();
1964 $actions = array();
1965
8adb3ec4 1966 while ($rule_line = $sth2->fetch()) {
aeb1abed
AD
1967 # print_r($rule_line);
1968
7f4a4045
AD
1969 if ($rule_line["match_on"]) {
1970 $match_on = json_decode($rule_line["match_on"], true);
aeb1abed 1971
7f4a4045 1972 if (in_array("0", $match_on) || in_array($feed_id, $match_on) || count(array_intersect($check_cats_fullids, $match_on)) > 0) {
aeb1abed 1973
7f4a4045
AD
1974 $rule = array();
1975 $rule["reg_exp"] = $rule_line["reg_exp"];
1976 $rule["type"] = $rule_line["type_name"];
1977 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
02f3992a 1978
7f4a4045
AD
1979 array_push($rules, $rule);
1980 } else if (!$match_any_rule) {
1981 // this filter contains a rule that doesn't match to this feed/category combination
1982 // thus filter has to be rejected
aeb1abed 1983
7f4a4045
AD
1984 $rules = [];
1985 break;
1986 }
aeb1abed 1987
7f4a4045 1988 } else {
0bf7e007 1989
7f4a4045
AD
1990 $rule = array();
1991 $rule["reg_exp"] = $rule_line["reg_exp"];
1992 $rule["type"] = $rule_line["type_name"];
1993 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
02f3992a 1994
7f4a4045
AD
1995 array_push($rules, $rule);
1996 }
aeb1abed
AD
1997 }
1998
02f3992a 1999 if (count($rules) > 0) {
7f4a4045
AD
2000 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
2001 FROM ttrss_filters2_actions AS a,
2002 ttrss_filter_actions AS t
2003 WHERE
2004 action_id = t.id AND filter_id = ?");
2005 $sth2->execute([$filter_id]);
02f3992a 2006
7f4a4045
AD
2007 while ($action_line = $sth2->fetch()) {
2008 # print_r($action_line);
02f3992a 2009
7f4a4045
AD
2010 $action = array();
2011 $action["type"] = $action_line["type_name"];
2012 $action["param"] = $action_line["action_param"];
02f3992a 2013
7f4a4045
AD
2014 array_push($actions, $action);
2015 }
2016 }
aeb1abed
AD
2017
2018 $filter = array();
2019 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
2020 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
2021 $filter["rules"] = $rules;
2022 $filter["actions"] = $actions;
2023
2024 if (count($rules) > 0 && count($actions) > 0) {
2025 array_push($filters, $filter);
2026 }
2027 }
2028
2029 return $filters;
2030 }
2031
2032 function get_score_pic($score) {
2033 if ($score > 100) {
2034 return "score_high.png";
2035 } else if ($score > 0) {
2036 return "score_half_high.png";
2037 } else if ($score < -100) {
2038 return "score_low.png";
2039 } else if ($score < 0) {
2040 return "score_half_low.png";
2041 } else {
2042 return "score_neutral.png";
2043 }
2044 }
2045
aeb1abed
AD
2046 function init_plugins() {
2047 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
2048
2049 return true;
2050 }
2051
2052 function add_feed_category($feed_cat, $parent_cat_id = false) {
2053
2054 if (!$feed_cat) return false;
2055
aeb1abed 2056 $feed_cat = mb_substr($feed_cat, 0, 250);
ecf6baaa 2057 if (!$parent_cat_id) $parent_cat_id = null;
aeb1abed 2058
8adb3ec4 2059 $pdo = Db::pdo();
c949a928
AD
2060 $tr_in_progress = false;
2061
2062 try {
2063 $pdo->beginTransaction();
2064 } catch (Exception $e) {
2065 $tr_in_progress = true;
2066 }
8adb3ec4
AD
2067
2068 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
f0dbfedc 2069 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
ecf6baaa 2070 AND title = :title AND owner_uid = :uid");
b78a6f08 2071 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
aeb1abed 2072
ecf6baaa 2073 if (!$sth->fetch()) {
aeb1abed 2074
b78a6f08
AD
2075 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
2076 VALUES (?, ?, ?)");
2077 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
aeb1abed 2078
c949a928 2079 if (!$tr_in_progress) $pdo->commit();
aeb1abed
AD
2080
2081 return true;
2082 }
2083
7f4a4045 2084 $pdo->commit();
fdda3e4e 2085
aeb1abed
AD
2086 return false;
2087 }
2088
2089 /**
2090 * Fixes incomplete URLs by prepending "http://".
2091 * Also replaces feed:// with http://, and
2092 * prepends a trailing slash if the url is a domain name only.
2093 *
2094 * @param string $url Possibly incomplete URL
2095 *
2096 * @return string Fixed URL.
2097 */
2098 function fix_url($url) {
2099
2100 // support schema-less urls
2101 if (strpos($url, '//') === 0) {
2102 $url = 'https:' . $url;
2103 }
2104
2105 if (strpos($url, '://') === false) {
2106 $url = 'http://' . $url;
2107 } else if (substr($url, 0, 5) == 'feed:') {
2108 $url = 'http:' . substr($url, 5);
2109 }
2110
2111 //prepend slash if the URL has no slash in it
2112 // "http://www.example" -> "http://www.example/"
2113 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
2114 $url .= '/';
2115 }
2116
2117 //convert IDNA hostname to punycode if possible
2118 if (function_exists("idn_to_ascii")) {
2119 $parts = parse_url($url);
2120 if (mb_detect_encoding($parts['host']) != 'ASCII')
2121 {
2122 $parts['host'] = idn_to_ascii($parts['host']);
2123 $url = build_url($parts);
2124 }
2125 }
2126
2127 if ($url != "http:///")
2128 return $url;
2129 else
2130 return '';
2131 }
2132
2133 function validate_feed_url($url) {
2134 $parts = parse_url($url);
2135
2136 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
2137
2138 }
2139
2140 /* function save_email_address($email) {
2141 // FIXME: implement persistent storage of emails
2142
2143 if (!$_SESSION['stored_emails'])
2144 $_SESSION['stored_emails'] = array();
2145
2146 if (!in_array($email, $_SESSION['stored_emails']))
2147 array_push($_SESSION['stored_emails'], $email);
2148 } */
2149
2150
2151 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2152
2153 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2154
1271407e
AD
2155 $is_cat = bool_to_sql_bool($is_cat);
2156
a21f7495 2157 $pdo = Db::pdo();
aeb1abed 2158
1271407e
AD
2159 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2160 WHERE feed_id = ? AND is_cat = ?
a21f7495 2161 AND owner_uid = ?");
ed5cd6ea 2162 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
aeb1abed 2163
a21f7495
AD
2164 if ($row = $sth->fetch()) {
2165 return $row["access_key"];
aeb1abed 2166 } else {
a21f7495 2167 $key = uniqid_short();
aeb1abed 2168
a21f7495 2169 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
aeb1abed 2170 (access_key, feed_id, is_cat, owner_uid)
a21f7495
AD
2171 VALUES (?, ?, ?, ?)");
2172
7d960ce7 2173 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
aeb1abed
AD
2174
2175 return $key;
2176 }
aeb1abed
AD
2177 }
2178
2179 function get_feeds_from_html($url, $content)
2180 {
2181 $url = fix_url($url);
2182 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
2183
2184 libxml_use_internal_errors(true);
2185
2186 $doc = new DOMDocument();
2187 $doc->loadHTML($content);
2188 $xpath = new DOMXPath($doc);
2189 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2190 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2191 $feedUrls = array();
2192 foreach ($entries as $entry) {
2193 if ($entry->hasAttribute('href')) {
2194 $title = $entry->getAttribute('title');
2195 if ($title == '') {
2196 $title = $entry->getAttribute('type');
2197 }
2198 $feedUrl = rewrite_relative_url(
2199 $baseUrl, $entry->getAttribute('href')
2200 );
2201 $feedUrls[$feedUrl] = $title;
2202 }
2203 }
2204 return $feedUrls;
2205 }
2206
2207 function is_html($content) {
2208 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2209 }
2210
2211 function url_is_html($url, $login = false, $pass = false) {
2212 return is_html(fetch_file_contents($url, false, $login, $pass));
2213 }
2214
2215 function build_url($parts) {
2216 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2217 }
2218
2219 function cleanup_url_path($path) {
2220 $path = str_replace("/./", "/", $path);
2221 $path = str_replace("//", "/", $path);
2222
2223 return $path;
2224 }
2225
2226 /**
2227 * Converts a (possibly) relative URL to a absolute one.
2228 *
2229 * @param string $url Base URL (i.e. from where the document is)
2230 * @param string $rel_url Possibly relative URL in the document
2231 *
2232 * @return string Absolute URL
2233 */
2234 function rewrite_relative_url($url, $rel_url) {
2235 if (strpos($rel_url, "://") !== false) {
2236 return $rel_url;
2237 } else if (strpos($rel_url, "//") === 0) {
2238 # protocol-relative URL (rare but they exist)
2239 return $rel_url;
2240 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2241 # magnet:, feed:, etc
2242 return $rel_url;
2243 } else if (strpos($rel_url, "/") === 0) {
2244 $parts = parse_url($url);
2245 $parts['path'] = $rel_url;
2246 $parts['path'] = cleanup_url_path($parts['path']);
2247
2248 return build_url($parts);
2249
2250 } else {
2251 $parts = parse_url($url);
2252 if (!isset($parts['path'])) {
2253 $parts['path'] = '/';
2254 }
2255 $dir = $parts['path'];
2256 if (substr($dir, -1) !== '/') {
2257 $dir = dirname($parts['path']);
2258 $dir !== '/' && $dir .= '/';
2259 }
2260 $parts['path'] = $dir . $rel_url;
2261 $parts['path'] = cleanup_url_path($parts['path']);
2262
2263 return build_url($parts);
2264 }
2265 }
2266
2267 function cleanup_tags($days = 14, $limit = 1000) {
2268
7f4a4045 2269 $days = (int) $days;
a21f7495 2270
aeb1abed
AD
2271 if (DB_TYPE == "pgsql") {
2272 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2273 } else if (DB_TYPE == "mysql") {
2274 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2275 }
2276
2277 $tags_deleted = 0;
2278
7f4a4045 2279 $pdo = Db::pdo();
a21f7495 2280
7f4a4045 2281 while ($limit > 0) {
aeb1abed
AD
2282 $limit_part = 500;
2283
a21f7495 2284 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
aeb1abed
AD
2285 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2286 WHERE post_int_id = int_id AND $interval_query AND
a21f7495
AD
2287 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2288 $sth->execute([$limit]);
aeb1abed
AD
2289
2290 $ids = array();
2291
a21f7495 2292 while ($line = $sth->fetch()) {
aeb1abed
AD
2293 array_push($ids, $line['id']);
2294 }
2295
2296 if (count($ids) > 0) {
2297 $ids = join(",", $ids);
2298
a21f7495
AD
2299 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2300 $tags_deleted = $usth->rowCount();
aeb1abed
AD
2301 } else {
2302 break;
2303 }
2304
2305 $limit -= $limit_part;
2306 }
2307
2308 return $tags_deleted;
2309 }
2310
2311 function print_user_stylesheet() {
2312 $value = get_pref('USER_STYLESHEET');
2313
2314 if ($value) {
2315 print "<style type=\"text/css\">";
2316 print str_replace("<br/>", "\n", $value);
2317 print "</style>";
2318 }
2319
2320 }
2321
2322 function filter_to_sql($filter, $owner_uid) {
2323 $query = array();
2324
e4befe6b
AD
2325 $pdo = Db::pdo();
2326
aeb1abed
AD
2327 if (DB_TYPE == "pgsql")
2328 $reg_qpart = "~";
2329 else
2330 $reg_qpart = "REGEXP";
2331
2332 foreach ($filter["rules"] AS $rule) {
2333 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2334 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2335 $rule['reg_exp']) !== FALSE;
2336
2337 if ($regexp_valid) {
2338
e4befe6b 2339 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
aeb1abed
AD
2340
2341 switch ($rule["type"]) {
2342 case "title":
2343 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2344 $rule['reg_exp'] . "')";
2345 break;
2346 case "content":
2347 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2348 $rule['reg_exp'] . "')";
2349 break;
2350 case "both":
2351 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2352 $rule['reg_exp'] . "') OR LOWER(" .
2353 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2354 break;
2355 case "tag":
2356 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2357 $rule['reg_exp'] . "')";
2358 break;
2359 case "link":
2360 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2361 $rule['reg_exp'] . "')";
2362 break;
2363 case "author":
2364 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2365 $rule['reg_exp'] . "')";
2366 break;
2367 }
2368
2369 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2370
2371 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
e4befe6b 2372 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
aeb1abed
AD
2373 }
2374
2375 if (isset($rule["cat_id"])) {
2376
2377 if ($rule["cat_id"] > 0) {
2378 $children = Feeds::getChildCategories($rule["cat_id"], $owner_uid);
2379 array_push($children, $rule["cat_id"]);
bed2d6e0 2380 $children = array_map("intval", $children);
aeb1abed
AD
2381
2382 $children = join(",", $children);
2383
2384 $cat_qpart = "cat_id IN ($children)";
2385 } else {
2386 $cat_qpart = "cat_id IS NULL";
2387 }
2388
2389 $qpart .= " AND $cat_qpart";
2390 }
2391
2392 $qpart .= " AND feed_id IS NOT NULL";
2393
2394 array_push($query, "($qpart)");
2395
2396 }
2397 }
2398
2399 if (count($query) > 0) {
2400 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2401 } else {
2402 $fullquery = "(false)";
2403 }
2404
2405 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2406
2407 return $fullquery;
2408 }
2409
2410 if (!function_exists('gzdecode')) {
2411 function gzdecode($string) { // no support for 2nd argument
2412 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2413 base64_encode($string));
2414 }
2415 }
2416
2417 function get_random_bytes($length) {
2418 if (function_exists('openssl_random_pseudo_bytes')) {
2419 return openssl_random_pseudo_bytes($length);
2420 } else {
2421 $output = "";
2422
2423 for ($i = 0; $i < $length; $i++)
2424 $output .= chr(mt_rand(0, 255));
2425
2426 return $output;
2427 }
2428 }
2429
2430 function read_stdin() {
2431 $fp = fopen("php://stdin", "r");
2432
2433 if ($fp) {
2434 $line = trim(fgets($fp));
2435 fclose($fp);
2436 return $line;
2437 }
2438
2439 return null;
2440 }
2441
aeb1abed
AD
2442 function implements_interface($class, $interface) {
2443 return in_array($interface, class_implements($class));
2444 }
2445
2446 function get_minified_js($files) {
aeb1abed
AD
2447
2448 $rv = '';
2449
2450 foreach ($files as $js) {
2451 if (!isset($_GET['debug'])) {
c4a08e4f 2452 $cached_file = CACHE_DIR . "/js/".basename($js);
aeb1abed 2453
c4a08e4f 2454 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
aeb1abed
AD
2455
2456 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2457
2458 if ($header && $contents) {
2459 list($htag, $hversion) = explode(":", $header);
2460
2461 if ($htag == "tt-rss" && $hversion == VERSION) {
2462 $rv .= $contents;
2463 continue;
2464 }
2465 }
2466 }
2467
c4a08e4f 2468 $minified = JShrink\Minifier::minify(file_get_contents("js/$js"));
aeb1abed
AD
2469 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
2470 $rv .= $minified;
2471
2472 } else {
c4a08e4f 2473 $rv .= file_get_contents("js/$js"); // no cache in debug mode
aeb1abed
AD
2474 }
2475 }
2476
2477 return $rv;
2478 }
2479
2480 function calculate_dep_timestamp() {
2481 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2482
2483 $max_ts = -1;
2484
2485 foreach ($files as $file) {
2486 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2487 }
2488
2489 return $max_ts;
2490 }
2491
2492 function T_js_decl($s1, $s2) {
2493 if ($s1 && $s2) {
2494 $s1 = preg_replace("/\n/", "", $s1);
2495 $s2 = preg_replace("/\n/", "", $s2);
2496
2497 $s1 = preg_replace("/\"/", "\\\"", $s1);
2498 $s2 = preg_replace("/\"/", "\\\"", $s2);
2499
2500 return "T_messages[\"$s1\"] = \"$s2\";\n";
2501 }
2502 }
2503
2504 function init_js_translations() {
2505
2506 print 'var T_messages = new Object();
b2d42e96 2507
aeb1abed
AD
2508 function __(msg) {
2509 if (T_messages[msg]) {
2510 return T_messages[msg];
2511 } else {
2512 return msg;
2513 }
2514 }
b2d42e96 2515
aeb1abed
AD
2516 function ngettext(msg1, msg2, n) {
2517 return __((parseInt(n) > 1) ? msg2 : msg1);
2518 }';
2519
2520 $l10n = _get_reader();
2521
2522 for ($i = 0; $i < $l10n->total; $i++) {
2523 $orig = $l10n->get_original_string($i);
2524 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2525 $key = explode(chr(0), $orig);
2526 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2527 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2528 } else {
2529 $translation = __($orig);
2530 print T_js_decl($orig, $translation);
2531 }
2532 }
2533 }
2534
aeb1abed 2535 function get_theme_path($theme) {
bfebf57c
AD
2536 if ($theme == "default.php")
2537 return "css/default.css";
2538
aeb1abed
AD
2539 $check = "themes/$theme";
2540 if (file_exists($check)) return $check;
2541
2542 $check = "themes.local/$theme";
2543 if (file_exists($check)) return $check;
2544 }
2545
2546 function theme_valid($theme) {
2547 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2548
2549 if (in_array($theme, $bundled_themes)) return true;
2550
2551 $file = "themes/" . basename($theme);
2552
2553 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2554
2555 if (file_exists($file) && is_readable($file)) {
2556 $fh = fopen($file, "r");
2557
2558 if ($fh) {
2559 $header = fgets($fh);
2560 fclose($fh);
2561
2562 return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
2563 }
2564 }
2565
2566 return false;
2567 }
2568
2569 /**
2570 * @SuppressWarnings(unused)
2571 */
2572 function error_json($code) {
2573 require_once "errors.php";
2574
2575 @$message = $ERRORS[$code];
2576
2577 return json_encode(array("error" =>
2578 array("code" => $code, "message" => $message)));
2579
2580 }
2581
904aff76 2582 /*function abs_to_rel_path($dir) {
aeb1abed
AD
2583 $tmp = str_replace(dirname(__DIR__), "", $dir);
2584
2585 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2586
2587 return $tmp;
904aff76 2588 }*/
aeb1abed
AD
2589
2590 function get_upload_error_message($code) {
2591
2592 $errors = array(
2593 0 => __('There is no error, the file uploaded with success'),
2594 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2595 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2596 3 => __('The uploaded file was only partially uploaded'),
2597 4 => __('No file was uploaded'),
2598 6 => __('Missing a temporary folder'),
2599 7 => __('Failed to write file to disk.'),
2600 8 => __('A PHP extension stopped the file upload.'),
2601 );
2602
2603 return $errors[$code];
2604 }
2605
2606 function base64_img($filename) {
2607 if (file_exists($filename)) {
2608 $ext = pathinfo($filename, PATHINFO_EXTENSION);
2609
2610 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2611 } else {
2612 return "";
2613 }
2614 }
2615
8b73bd28
AD
2616 /* this is essentially a wrapper for readfile() which allows plugins to hook
2617 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2618
2619 hook function should return true if request was handled (or at least attempted to)
2620
2621 note that this can be called without user context so the plugin to handle this
2622 should be loaded systemwide in config.php */
2623 function send_local_file($filename) {
2624 if (file_exists($filename)) {
f3068c63
AD
2625
2626 if (is_writable($filename)) touch($filename);
2627
8b73bd28
AD
2628 $tmppluginhost = new PluginHost();
2629
2630 $tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
2631 $tmppluginhost->load_data();
2632
2633 foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
2634 if ($plugin->hook_send_local_file($filename)) return true;
2635 }
2636
2637 $mimetype = mime_content_type($filename);
88adf3da
AD
2638
2639 // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
2640 // video files are detected as octet-stream by mime_content_type()
2641
2642 if ($mimetype == "application/octet-stream")
2643 $mimetype = "video/mp4";
2644
8b73bd28
AD
2645 header("Content-type: $mimetype");
2646
2647 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2648 header("Last-Modified: $stamp", true);
2649
a1b86519 2650 return readfile($filename);
8b73bd28
AD
2651 } else {
2652 return false;
2653 }
2654 }
2655
0b68b162 2656 function check_mysql_tables() {
a21f7495 2657 $pdo = Db::pdo();
0b68b162 2658
a21f7495
AD
2659 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2660 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2661 $sth->execute([DB_NAME]);
0b68b162
AD
2662
2663 $bad_tables = [];
2664
a21f7495 2665 while ($line = $sth->fetch()) {
0b68b162
AD
2666 array_push($bad_tables, $line);
2667 }
2668
2669 return $bad_tables;
2670 }
2671
2cf93c04
AD
2672 function validate_field($string, $allowed, $default = "") {
2673 if (in_array($string, $allowed))
2674 return $string;
2675 else
2676 return $default;
2677 }
2678
7f4a4045
AD
2679 function arr_qmarks($arr) {
2680 return str_repeat('?,', count($arr) - 1) . '?';
2681 }