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