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