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