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